You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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):
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
Proposed direction (for discussion, not prescriptive): (1) inventory all managed threadvars in source/units + source/shared; (2) prefer a single mechanism over ~20 ad-hoc call sites — e.g. a thread-cleanup callback registry that ShutdownThreadRuntime drains, so each unit registers its clear proc once; (3) route the perf(regexp): memoize the decoded subject across regex VM calls #805/perf(string): ASCII byte-op fast paths for the UTF-16 string helpers #806 memos (ClearRegExpInputMemo / ClearAsciiMemo) through it too, completing those PRs' main-thread-only finalization; (4) optionally add a heaptrc worker-spawn→exit zero-leak check to lock it in.
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 —ShutdownThreadRuntimeinGoccia.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.mddeprioritizes 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/GarbageCollectorshutdown).Managed threadvars not routed through it (leak at each worker-thread exit):
threadvar FStaticMembers: TArray<TGocciaMemberDefinition>(some alsoFPrototypeMembers): CSV, GlobalFFI, GlobalArrayBuffer, Console, GlobalObject, GlobalBigInt, GlobalString, GlobalArray, GlobalNumber, GlobalRegExp, GlobalReflect, GlobalURL, GlobalPromise, GlobalSymbol, JSON; plusGoccia.Values.WeakMapValue(FPrototypeMembers).Goccia.RegExp.VMdecode memo) and perf(string): ASCII byte-op fast paths for the UTF-16 string helpers #806 (TextSemanticsis-ASCII memo). These currently clear via a unitfinalizationsection, 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 existingDisposableStackpattern (which does both). Nothing is retained past the thread that created it.Scope notes
source/units+source/shared; (2) prefer a single mechanism over ~20 ad-hoc call sites — e.g. a thread-cleanup callback registry thatShutdownThreadRuntimedrains, so each unit registers its clear proc once; (3) route the perf(regexp): memoize the decoded subject across regex VM calls #805/perf(string): ASCII byte-op fast paths for the UTF-16 string helpers #806 memos (ClearRegExpInputMemo/ClearAsciiMemo) through it too, completing those PRs' main-thread-only finalization; (4) optionally add a heaptrc worker-spawn→exit zero-leak check to lock it in.DisposableStack, Semver, TimeZone, ImportMeta caches already route throughShutdownThreadRuntime.source/units/Goccia.Builtins.*.pas,source/shared/TextSemantics.pas,source/units/Goccia.RegExp.VM.pas,source/units/Goccia.Threading.pas.