|
| 1 | +# Nonwaiting JIT trace-reader SMR tranche |
| 2 | + |
| 3 | +Date: 2026-07-19 |
| 4 | + |
| 5 | +This b1.2.1 checkpoint removes the looping GC2 SMR admission API from the JIT |
| 6 | +reflection, recorder lookup, assembler lookup and optional GDB-JIT metadata |
| 7 | +files. It is a bounded tranche, not a claim that trace publication, flush, |
| 8 | +retirement or the repository-wide wait inventory is complete. |
| 9 | + |
| 10 | +## Runtime policy |
| 11 | + |
| 12 | +The `jit.util` trace-reflection functions are observational under concurrent |
| 13 | +mutation. They already used a one-shot recorder-token acquisition and returned |
| 14 | +no result when a peer owned it. They now apply the same rule to SMR: failure to |
| 15 | +enter the current trace-body generation is a transient no-result observation. |
| 16 | +If the function acquired the recorder token, it releases that token before |
| 17 | +returning. This prevents an exclusive reclaimer that also needs the token from |
| 18 | +forming a token/SMR dependency cycle. |
| 19 | + |
| 20 | +Reflection does not allocate while retaining trace-body SMR. `tracesnap` |
| 21 | +copies its at-most-255 immutable map entries to bounded C-stack storage before |
| 22 | +building the result table. `tracemc` first sizes the current observation, |
| 23 | +grows the owner TG's managed scratch buffer outside SMR, and then performs a |
| 24 | +second one-shot lookup which copies only a body that fits. String interning is |
| 25 | +therefore outside both trace SMR and the temporary recorder-token claim; an |
| 26 | +allocation error cannot leak a trace pin or strand a reclaimer. The second |
| 27 | +admitted interval also converts the mcode address to an integer scalar before |
| 28 | +release; no expired trace or mcode pointer value is evaluated afterward. |
| 29 | +`traceexitstub` applies the same rule to its borrowed exit-stub address. |
| 30 | +`tracek` reduces scalar constants to copied IR words; for `IR_KGC`, whose word |
| 31 | +still names a reclaimable child, it additionally acquires an exact GC2 |
| 32 | +allocation lease under trace SMR, publishes the copied TValue as a Lua stack |
| 33 | +root after leaving trace SMR, and only then releases the child lease. A |
| 34 | +transient or stale child admission is another no-result observation. |
| 35 | + |
| 36 | +Recorder and assembler lookups have a stronger semantic obligation, but both |
| 37 | +are speculative. A failed one-shot SMR admission aborts that recording turn |
| 38 | +with the admission-specific `LJ_TRERR_SMRRETRY`, allowing execution to continue |
| 39 | +in the interpreter and a later hot turn to record again. The new error was |
| 40 | +appended to the catalog so every pre-existing numeric trace-error value remains |
| 41 | +unchanged. It gets the same immediate-hotcount policy as `LJ_TRERR_RETRY`, but |
| 42 | +only `SMRRETRY` suppresses the TRACE-abort event: ordinary optimizer, stale-body |
| 43 | +and table retries remain observable through the existing API. The assembler's |
| 44 | +target-return-PC shortcut is optional and simply retains the snapshot PC when |
| 45 | +admission loses. |
| 46 | + |
| 47 | +The hot-side probe likewise remains interpreted when admission loses. After a |
| 48 | +snapshot has already been restored, a `JLOOP` target revalidation collision |
| 49 | +returns to bytecode redispatch. Neither optimization can make an exiting |
| 50 | +mutator wait for a trace reclaimer. |
| 51 | + |
| 52 | +GDB-JIT metadata is optional. If a side trace's parent cannot be admitted, its |
| 53 | +debug entry is omitted instead of waiting or publishing an object with a |
| 54 | +guessed parent stack adjustment. Normal trace execution and retirement are |
| 55 | +unchanged. |
| 56 | + |
| 57 | +Debug trace-PC recovery similarly reports no bytecode position on contention. |
| 58 | +The x86/x64 final link assembler retries the recording turn. Bytecode dumping |
| 59 | +recovers patched root instructions from the prototype's immutable sidecar; |
| 60 | +only a compatibility fallback tries trace-body SMR, and that admission is |
| 61 | +one-shot with an already bounded live-bytecode resample. |
| 62 | + |
| 63 | +There are now no `lj_gc2_smr_read_enter()` call sites in `lib_jit.c`, |
| 64 | +`lj_record.c`, `lj_asm.c`, `lj_asm_x86.h`, `lj_bcwrite.c`, `lj_debug.c` or |
| 65 | +`lj_gdbjit.c`. A monotonic source gate protects this completed domain. Blocking |
| 66 | +admissions remain in `lj_trace.c` and GC/safepoint lifecycle files and must be |
| 67 | +converted with their matching flush/retirement descriptors; this tranche does |
| 68 | +not hide that debt behind an allowlist. |
| 69 | + |
| 70 | +## Terminal cleanup |
| 71 | + |
| 72 | +Failed assembler copies now retire as raw unpublished scratch descriptors: they |
| 73 | +carry no semantic snapshot/prototype/exit-table graph and use one-shot raw marks |
| 74 | +around token-owned list publication. Successful assembly, ordinary cancel and |
| 75 | +owner-abort paths publish INTERP+IDLE and release the recorder token before |
| 76 | +dispatch repair. First-area `MCODEAL` and max-trace exhaustion likewise defer a |
| 77 | +full flush until after that terminal release. An asynchronously aborted |
| 78 | +`MCODELM` restart falls through full slot cleanup instead of leaking `J->cur`, |
| 79 | +and down-recursion retries only when its restarted recorder remains non-IDLE. |
| 80 | + |
| 81 | +The normal active `TRACE_START` path still installs its recording dispatch |
| 82 | +overlay while retaining the token. Any wait/dependency hidden in that active |
| 83 | +overlay update is explicit remaining debt; silent-IDLE start exits no longer |
| 84 | +share it. |
| 85 | + |
| 86 | +## Evidence |
| 87 | + |
| 88 | +The recorder-token fixture closes SMR in `LJ_GC2_SMR_META_EXCLUSIVE` and calls |
| 89 | +every trace-numbered `jit.util` reader. Each call returns without a result, |
| 90 | +does not increment the reader count and leaves the recorder token unowned. A |
| 91 | +regression to the looping admission API hangs this deliberately closed scope |
| 92 | +and is terminated by the focused suite timeout. |
| 93 | + |
| 94 | +The immutable-start-instruction fixture also publishes a real patched `JLOOP`, |
| 95 | +closes SMR exclusively, dumps the owning function, reloads the bytecode and |
| 96 | +executes it. This proves that bytecode serialization uses the prototype |
| 97 | +sidecar rather than waiting for or dereferencing the trace vector. Serialization |
| 98 | +now acquire-loads every mutable `BCIns` independently and always runs a captured |
| 99 | +JIT opcode through sidecar/unpatch recovery. It no longer races a bulk `memcpy` |
| 100 | +or a stale `PROTO_ILOOP`/prototype-trace fast-path decision against publication |
| 101 | +and flush. |
| 102 | + |
| 103 | +The same fixture exposed a stale XPOLL assertion after mutable-global tracing |
| 104 | +became conservative. A safe runnable side trace may now end at the interpreter |
| 105 | +instead of using `LJ_TRLINK_ROOT`; the test now checks the actual invariant, an |
| 106 | +exact snapshot on a non-loop XPOLL in any runnable side-trace topology. |
| 107 | + |
| 108 | +## Remaining event debt |
| 109 | + |
| 110 | +The exact closed-gate `SMRRETRY` path cannot safely call arbitrary TRACE-abort |
| 111 | +handlers while it still owns the recorder token, because a handler may enter a |
| 112 | +reader needed by the already-admitted writer. Other abort events retain their |
| 113 | +historical visibility. They still run token-held and therefore retain a more |
| 114 | +general close-after-check dependency race if a metadata-exclusive owner closes |
| 115 | +after event preparation begins. Moving those callbacks requires preserving |
| 116 | +`jit.dump` and user abort-observation ordering and is intentionally recorded as |
| 117 | +follow-up debt rather than hidden by suppressing ordinary `RETRY` events. |
0 commit comments