Migrate explicit per-thread cache clears into the thread-cleanup registry
Date: 2026-06-28
Area: engine
Issue: #893
ADR 0078 added Goccia.ThreadCleanupRegistry and routed the ~64 member-definition threadvars through it, but to avoid destabilising working teardown in #891 it left seven pre-existing per-thread cache/memo clears as explicit calls inside Goccia.Threading.ShutdownThreadRuntime: ClearImportMetaCache, ShutdownAtomicsWaitersForCurrentThread, ClearDisposableStackSlotMap, ClearSemverHosts, ClearTimeZoneCache, ClearRegExpInputMemo, and ClearAsciiMemo. That left two cleanup idioms coexisting on the worker-exit path — explicit calls plus the registry drain — and coupled Goccia.Threading's uses clause to seven cache-owning units.
Decision
Each of the seven units registers its clear with Goccia.ThreadCleanupRegistry from its own initialization section, the same shape the 64 member-definition threadvars already use, and ShutdownThreadRuntime drops the seven explicit calls — keeping only the RunThreadvarCleanups drain followed by the ordered object-lifecycle shutdowns (MicrotaskQueue / CallStack / GarbageCollector), which stay explicit because their order matters. The drain still runs before those shutdowns, so a cache that touches the collector (ClearImportMetaCache unpins) is released while the GC is alive. Goccia.Threading no longer uses any of the seven units.
Units whose finalization existed only to clear a threadvar drop it and rely on the registry's own finalization for main-thread cleanup (ImportMeta, DisposableStack, Semver, RegExp.VM). Units whose finalization does more keep that part: Temporal.TimeZone retains the Windows ICU-lock teardown, and Atomics retains its all-threads shutdown (below).
Three cases needed care:
-
Atomics is not a managed threadvar.
GAtomicsWaitersis a shared, lock-guarded global;ShutdownAtomicsWaitersForCurrentThreadremoves only the calling thread's entries (keyed by owner thread id). It is registered for the worker path, but the unit's ownfinalizationkeeps the all-threadsShutdownAtomicsWaitersplusDoneCriticalSectionfor the main thread — preserving the per-thread/all-threads distinction. Because the registry finalizes after this unit (it is an earlier-initialised dependency), the registry's main-thread drain would otherwise call the per-thread proc after the lock is destroyed; a pre-lockif not Assigned(GAtomicsWaiters)guard makes that a safe no-op. The registry's callback contract is broadened to cover "a thread's own entries in a shared lock-guarded structure," not only managed threadvars. -
TextSemantics is generic shared infrastructure (
source/shared/) with no engine dependency, used by the JSON, numeric-text and CLI-config tools as well as the engine. To keep it engine-free, its is-ASCII memo (#806) clear is registered from the engine'sGoccia.RegExp.VMrather than self-registered — every engine binary links the regex VM, so the worker path is covered. Its own main-threadfinalizationis retained, because binaries that linkTextSemanticsbut notRegExp.VM(the shared tools andTextSemantics.Test) populate the memo and would otherwise never release it. -
ImportMeta keeps
ClearImportMetaCacheexported: besides the registry, the engine calls it directly during its own teardown.
Consequences
- Worker-thread cleanup is one mechanism (the registry drain);
Goccia.Threadingis decoupled from the cache-owning units. Adding a new per-thread cache no longer means editingShutdownThreadRuntime. - Main-thread cleanup is preserved on whichever path is correct per unit: the registry finalization for the self-registering engine units, and the unit's own finalization for
Atomics(all-threads), the Windows ICU lock, andTextSemantics(binary-independent). - A new gate in
Goccia.ThreadCleanupLeak.Testpins each of the seven registrations viaIsThreadvarCleanupRegistered, so a droppedRegisterThreadvarCleanupfails loudly instead of silently leaking.Goccia.Threading.Teststill proves the drain fires once per worker exit. The full JavaScript suite passes in both modes and all Pascal unit tests pass. - No behaviour or conformance change: this is the cleanliness/embeddability follow-up ADR 0078 deferred, not a runtime change.