Difference between revisions of "User:DavidJCobb/Stack dumping"

3,366 bytes added ,  23:33, 21 March 2017
Corrections.
imported>DavidJCobb
imported>DavidJCobb
(Corrections.)
 
(13 intermediate revisions by the same user not shown)
Line 1: Line 1:
'''Stack dumping''' is a Papyrus event that can cause unpredictable and potentially severe errors in a player's savegame. My understanding of the phenomenon is as follows:
'''Stack dumping''' is a Papyrus warning that indicates that the script engine has been given too many tasks to run at once.
 
{{InDepth|Previously, it was thought that stack dumping indicated the ''termination'' of stacks, i.e. the game engine randomly throwing out paused script tasks. However, Bethesda's own description of the phenomenon in the Fallout 4 CK wiki suggests that this is merely a warning system.}}


== Overview ==
== Overview ==
Whenever the game engine calls a function, a ''call stack'' is generated. When that function calls another function, the callee is added to the caller's call stack. Papyrus can only have a certain number of call stacks running at a time; if too many stacks accumulate, some may be ''suspended'' (paused). Stacks may also be suspended for other reasons:
Whenever the game engine calls a function, a ''call stack'' is generated. A call stack keeps track of the function's state -- which functions have been called (if X calls Y, Y is added to the X call stack), and what values their variables have. Papyrus can only have a certain number of call stacks running at a time; if too many stacks accumulate, some may be ''suspended'' (paused). Stacks may also be suspended for other reasons:


* A function that calls [[Wait - Utility|Utility.Wait(''n'')]] will suspend its call stack for ''n'' seconds.
* A function that calls [[Wait - Utility|Utility.Wait(''n'')]] will suspend its call stack for ''n'' seconds.
Line 9: Line 11:
* Some events' handlers cannot run concurrently with themselves. If the event occurs several times in a short period, only one call to its event handler will run immediately; the other calls will exist as suspended stacks.
* Some events' handlers cannot run concurrently with themselves. If the event occurs several times in a short period, only one call to its event handler will run immediately; the other calls will exist as suspended stacks.


If too many suspended stacks accumulate, the Papyrus engine will not be able to hold them all; suspended stacks will be selected at random and discarded, and information about those stacks will be printed to the Papyrus logs, prefixed by this message:
If too many suspended stacks accumulate, information about those stacks will be printed to the Papyrus logs, prefixed by this message:


<pre>Suspended stack count is over our warning threshold, dumping stacks:</pre>
<pre>Suspended stack count is over our warning threshold, dumping stacks:</pre>


This means that if a mod has a problem that leads to stack dumping, that mod will interfere with itself, with other mods, and with Bethesda-authored scripts (because Papyrus doesn't care where the problem is coming from; it'll dump as many stacks as it can, indiscriminately, until it feels safe). The problem will have unpredictable consequences, which may or may not break other content or otherwise lead to save corruption. As a modder, the best approach to take here is the maximally cautious one, because even if a stack dumping issue doesn't wreck ''your'' savegame, it could wreck someone else's. If your mod has a flaw that leads to stack dumping, fixing that needs to be a high priority.
Save corruption has been observed following messages of this kind, but the exact nature of those problems isn't known. According to Bethesda's description of this phenomenon in the Fallout 4 CK wiki, stack dumping is just a warning system, and no action is taken at that stage. Whether this error is harmless or not, it very often ''does'' indicate a problem with a mod trying to do too much processing at once.


== Specific methods to minimize stack dumping ==
== Specific methods to minimize stack dumping ==
Line 38: Line 40:


== Miscellaneous facts ==
== Miscellaneous facts ==
* RegisterForSingleUpdate(''n'') does ''not'' create a stack and suspend it for ''n'' seconds. I have confirmed this experimentally.
* RegisterForSingleUpdate(''n'') does ''not'' create a stack and suspend it for ''n'' seconds. The stack is only created when it's time to run. I have confirmed this experimentally.


== Code snippets ==
== Code snippets ==
These code snippets have not been tested directly, and the benefits are purely theoretical. The only way to know for sure if they'll help is to devise stress tests for whatever you're trying to do. Run a stress test with code that doesn't use these methods, and a stress test with code that does, and check the logs to see what code dumps stacks first and worst.
=== Process a task OnHit, and wait for X seconds during or after the task ===
This simple example blocks concurrent calls, and replaces a Utility.Wait(X) call with a RegisterForSingleUpdate(X) call. If we used Utility.Wait(X), we'd suspend our stack for X seconds; by using RegisterForSingleUpdate and OnUpdate, we get rid of the stack entirely during those X seconds, and create a new stack to continue processing after the delay.
Even if we didn't prevent concurrency using <code>pbBusy</code>, we'd still get rid of all OnHit stacks at the start of the wait, and create just one OnUpdate stack at the end of the wait; however, with every hit, the wait time would be reset to X seconds. Whether or not we block concurrent calls, hits that occur during the wait time will be ignored.


=== Process a task OnHit, and don't re-process for X seconds ===
<source lang="Papyrus">
<source lang="Papyrus">
Bool Property pbBusy = False Auto Hidden
Bool Property pbBusy = False Auto Hidden ; prevents concurrent execution
Bool Property pbProcessed = False Auto Hidden


Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, Bool abPowerAttack, Bool abSneakAttack, Bool abBashAttack, Bool abHitBlocked)
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, Bool abPowerAttack, Bool abSneakAttack, Bool abBashAttack, Bool abHitBlocked)
   If pbBusy || pbProcessed
   If pbBusy
       Return
       Return
   EndIf
   EndIf
Line 55: Line 61:
   ; Do your processing here.
   ; Do your processing here.
   ;
   ;
  pbProcessed = True
   RegisterForSingleUpdate(x) ; replace "X" with the seconds you want to spend waiting
   RegisterForSingleUpdate(x) ; replace "X" with the seconds you want to spend ignoring further OnHit events
  pbBusy = False
EndEvent
EndEvent


Event OnUpdate()
Event OnUpdate()
   pbProcessed = False
   ;
  ; Continue processing here (or do nothing, if your goal was to wait X seconds after processing a task before allowing more tasks to be processed).
  ;
  pbBusy = False ; We're done!
EndEvent
</source>
 
=== Process a task OnHit, without outright discarding concurrent calls ===
We want to avoid doing processing during concurrent calls, but we may not want to outright discard concurrent calls. If an enemy is hit four times in rapid succession, we might want to process all four hits, but we'll still want to do our actual processing in just one stack, if possible. To accomplish this, we block concurrent calls, but we keep track of how many such calls we've blocked; and in a loop, we run our task that number of times.
 
<source lang="Papyrus">
Bool Property pbBusy = False Auto Hidden ; prevents concurrent execution
Int Property piHitsToProcess = 0 Auto Hidden
 
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, Bool abPowerAttack, Bool abSneakAttack, Bool abBashAttack, Bool abHitBlocked)
  piHitsToProcess += 1 ; We need to process another hit.
  If pbBusy
      Return ; ...but we're busy right now, so we'll get to it later.
  EndIf
  DoProcess()
EndEvent
EndEvent
Event OnUpdate() ; This function exists to start processing hits that happened while we're busy.
  If pbBusy
      Return ; ...but we can't do that if we've become busy again.
  EndIf
  DoProcess()
EndEvent
Function DoProcess()
  If pbBusy
      Return ; We're busy. Return; we'll deal with new calls later.
  EndIf
  pbBusy = True                        ; Block concurrent calls.
  Int iTasksToProcess = piHitsToProcess ; This is how many times we plan to process our task.
  piHitsToProcess = 0                  ; Reset the hit counter, so we can handle hits that happen while we're processing right now.
  Int iIterator = 0
  While iIterator < iTasksToProcess
      ;
      ; Do your processing here.
      ;
      iIterator += 1
  EndWhile
  pbBusy = False ; Stop blocking concurrent calls.
  If piHitsToProcess
      ;
      ; If this runs, it's because more events fired while we were processing.
      ; We need to re-process. Using an update instead of a recursive function
      ; call will shift us to a "fresh" call stack -- we shouldn't run into
      ; errors with too much recursion.
      ;
      RegisterForSingleUpdate(0.1) ; Extending this delay may be a good idea.
  EndIf
EndFunction
</source>
</source>
Anonymous user