Skip to content

Managed threadvars aren't released on worker-thread exit (engine-wide) — per-thread caches leak #885

Description

@frostney

Summary

FPC does not auto-finalize managed threadvars (AnsiString, dynamic arrays / TArray<>) at thread exit. The engine already has a per-worker-thread teardown hook — ShutdownThreadRuntime in Goccia.Threading.pas — that clears several per-thread caches, but most managed threadvars aren't routed through it, so each worker thread leaks its last-held values when it exits.

Why

Bounded (one set of values per thread, overwritten in place — no per-op growth) and no heaptrc/zero-leak CI gate trips on it today, so this is low urgency. But worker-thread exits aren't leak-clean, which matters for embedders that create/destroy many engine threads over a long-lived process, and it's a prerequisite for any future heaptrc zero-leak gate. VISION.md deprioritizes raw throughput, so this is cleanliness / embeddability, not performance.

Current behavior

The hook exists — ShutdownThreadRuntime (Goccia.Threading.pas:228–240) — and already clears, per worker thread: ClearImportMetaCache, ShutdownAtomicsWaitersForCurrentThread, ClearDisposableStackSlotMap, ClearSemverHosts, ClearTimeZoneCache (+ MicrotaskQueue/CallStack/GarbageCollector shutdown).

Managed threadvars not routed through it (leak at each worker-thread exit):

  • ~20 builtin units' threadvar FStaticMembers: TArray<TGocciaMemberDefinition> (some also FPrototypeMembers): CSV, GlobalFFI, GlobalArrayBuffer, Console, GlobalObject, GlobalBigInt, GlobalString, GlobalArray, GlobalNumber, GlobalRegExp, GlobalReflect, GlobalURL, GlobalPromise, GlobalSymbol, JSON; plus Goccia.Values.WeakMapValue (FPrototypeMembers).
  • The two memo caches added in perf(regexp): memoize the decoded subject across regex VM calls #805 (Goccia.RegExp.VM decode memo) and perf(string): ASCII byte-op fast paths for the UTF-16 string helpers #806 (TextSemantics is-ASCII memo). These currently clear via a unit finalization section, which runs only on the main thread at program exit — so their worker-thread copies still leak. (The memos are populated almost entirely on worker threads, so this is the common case for them.)

Expected behavior

Every managed threadvar holding per-thread state is released both on main-thread shutdown (unit finalization) and per worker-thread exit (ShutdownThreadRuntime), matching the existing DisposableStack pattern (which does both). Nothing is retained past the thread that created it.

Scope notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengineTGocciaEngine: language semantics, ECMAScript built-ins, parser, interpreter, bytecode VM

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions