TimeoutGuard
Inlined per-tick time guard. ScriptWeave.instrument inlines if (killId == myEvalId) throw timeout (GETSTATIC INVOKESTATIC guard would itself StackOverflowError. One mechanism handles BOTH dead loops (back-edges) and dead recursion (the checks fire as SOE unwinds through catch handlers).
The budget bounds the INTERVAL BETWEEN HEARTBEATS, not the wall time one eval spends on the lane thread — the two come apart where vanilla re-enters the pump ([endFrame]). A lane whose heartbeat keeps firing is a game that keeps running, whatever a script is doing inside it; frozen means the loop stopped advancing — what `ServerWatchdog` kills a dedicated server over — and nothing short of that counts.
One kill-id field per `GuardLane` — a GETSTATIC-cheap field can't be thread-local, so instead of one shared field that the server and render threads would trample, there is one per lane-thread. Which one a script reads is decided at instrument time from the target lane, so compilation bakes in the right GETSTATIC with no runtime thread sniffing. Those two fields are the only per-lane STATE here; everything else lives in a `GuardState` indexed by the lane's `ordinal`, so this file has exactly one lane branch ([setKillId]).
An ID rather than a flag, because instrumented code OUTLIVES its eval: a [Patches] handler and a script-spawned thread both keep running after the eval that compiled them is gone, and a per-lane flag would kill them whenever any LATER eval on that lane went over budget. The id is baked into the bytes at instrument time, so it travels with the code and can only ever match the eval it came from. What remains is code outliving an eval that WAS killed reading its own id — its own eval's business, and bounded by [exitStep]. Past that clear the id is never raised again, so such code runs unguarded for good — see [Patches].
Types
Where the watchdog found a lane thread standing when it spent that lane's budget. ScriptTimeoutError is an immutable process-wide singleton carrying no stack of its own, so this is the side channel a timeout report reads instead — and it is the better answer anyway: the offending code, not wherever the inlined guard next happened to fire. One immutable pair behind one volatile, so caughtHere reads thread and trace coherently. A materialized trace holds only strings, so retaining it pins no snippet classloader.
One lane-thread's watchdog state — everything except that lane's ABI kill-id field.
Properties
Indexed by the GuardLane ordinal. Fixed length, elements never replaced.
Functions
Arm runs ON the lane thread (from beginFrame/endFrame), which is what lets it hand the watchdog the thread to photograph. disarm deliberately does NOT clear the capture — a report may render after the frame ends, and a stale one can only ever attach to a real later timeout, which arms (and so clears) first.
Lane pump, start of a frame: arm this lane's budget. Frames nest — see endFrame.
The stack caught on THIS thread, or null. Matching on the caller's own thread is what keeps a report from picking up the other lane's stack, and makes an off-lane render (a deferred Outcome text built on the HTTP thread) omit the block rather than attach a wrong one.
Lane pump, end of a frame. Disarms only when the OUTERMOST frame ends; an inner frame re-arms a FULL budget for the one it returns to. Vanilla re-enters the pump — Minecraft#disconnect and #doWorldLoad spin runTick(false) while a step is on the stack — and disarming there would leave that step running unguarded for the rest of its turn. Pair with beginFrame in a try/finally, or the depth leaks and the budget is never disarmed.
Lane pump, around one eval's step: publish evalId as who is on this lane's stack, so the watchdog knows who to name. Returns the id it displaced — hand that back to exitStep. Save/restore rather than clear, because steps NEST for endFrame's reason, and the restore is what carries the enclosing step across a nested frame.
Whether lane's budget is already spent. Read by the pump before it steps the next eval, so an eval that never got its turn can be ended with that reason instead of the offender's. The two reads are coherent without a lock: gen is written only by the lane thread, which is also this method's only caller.