User:DavidJCobb/Stack dumping

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:

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:

  • A function that calls Utility.Wait(n) will suspend its call stack for n seconds.
  • If a function tries to access a resource that another script is currently accessing (a "shared resource"), the function will be suspended until the resource becomes available.
    • One common example of a shared resource is the player object.
  • 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:

Suspended stack count is over our warning threshold, dumping stacks:

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.

Specific methods to minimize stack dumping

  • There are a number of events that can rapidly generate suspended stacks, because they can occur several times in a short period (such that several calls to your event handlers are queued). If you must listen to these events, then minimize the length of your event handlers, so that the resulting stacks are executed as quickly as possible.
    • Don't use Utility.Wait() in these event handlers. It will by definition make your event handlers take longer to execute.
    • Minimize the amount of times you access shared resources (such as the player), in order to shorten your event handler's overall execution time.
      • Remember that your own resources can be shared. If you have a magic effect that coordinates itself via a quest, anyone with that magic effect will be sharing that quest.
    • If you don't need instantaneous processing and aren't using Utility.Wait(), you can "eat" some of the stacks by having your event handlers RegisterForSingleUpdate(n), and doing your actual processing in an OnUpdate() event. If x events occur within n seconds of each other, you'll process all of those events just one time.
      • Effectively, those x events will register for an update and return immediately, clearing x stacks out of memory. Then, n seconds after the xth event fired, one new stack will be created to do your processing.
    • If you need to quickly process the first event in a bunch, then you can "eat" a few of the redundant stacks by using a boolean variable to prevent concurrency. Check if it's true at the start of the function, and return if it is. Otherwise, set it to true, do your processing, and then set it back to false.

Events that can trigger large numbers of suspended stacks

If you try to do advanced processing within these events, you're gonna have a bad time. In a worst-case scenario, the player will rapidly (or even instantly) generate more suspended stacks than Papyrus can handle, leading to stack dumping.

  • OnHit can generate a large number of suspended stacks, because the event is duplicated if the victim is hit with an enchanted weapon.
    • Consider the case of an encounter between a player and five bandits, each of which takes four to seven hits to kill, and each of which has an OnHit event handler. Without an enchanted weapon, the player will generate 20 to 35 OnHit events to kill the bandits. If the player's weapon has two enchantments, she will generate 60 to 105 OnHit events. Meanwhile, the bandits will generate many OnHit events on the player (who can heal herself). The problem can quickly balloon out of control.
      • Incidentally, there's an area in Pinewatch where you can end up fighting five or six bandits all at once. It's also possible to obtain a sword with two enchantments very early in the game, courtesy of Meridia.
    • OnHit can run concurrently with itself, so (ignoring outside influences) OnHit stacks should only be suspended if too many of them are generated too quickly.
  • OnItemAdded and OnItemRemoved can generate a large number of suspended stacks if no inventory event filter is used. The player can transfer large masses of items (e.g. in their home), for example.
    • Even batch operations, like RemoveAllItems(), can cause stack dumping. One stack is generated for every type of item transferred, and inventory events can't run concurrently with themselves, so all but one of those stacks will be suspended the instant it is created.
      • Friendly reminder: built-in, Bethesda-authored content calls RemoveAllItems() on the player. Diplomatic Immunity, The Forsworn Conspiracy, The Mind of Madness, Restoration Ritual Spell, and Unbound all remove the player's inventory at some point.
    • For OnItemAdded and OnItemRemoved, I list some additional methods for avoiding stack dumping here.
    • OnItemAdded and OnItemRemoved don't appear to be able to run concurrently with themselves, so their stacks will be suspended as a matter of course, and it'll take longer for those stacks to clear.

Miscellaneous facts

  • RegisterForSingleUpdate(n) does not create a stack and suspend it for n seconds. I have confirmed this experimentally.

Code snippets

Process a task OnHit, and don't re-process for X seconds

Bool Property pbBusy = False Auto Hidden
Bool Property pbProcessed = False Auto Hidden

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

Event OnUpdate()
   pbProcessed = False
EndEvent