From b6c7144236a04dd622147d9424dad53913412c94 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sat, 27 Jun 2026 20:31:51 +0100 Subject: [PATCH 1/5] fix(engine): release managed threadvars on worker-thread exit FPC does not auto-finalize managed threadvars at thread exit, so every builtin and value type's cached member-definition arrays (FStaticMembers / FPrototypeMembers) and the #805/#806 input memos leaked on each worker-thread exit. Add Goccia.ThreadCleanupRegistry: each leaking unit registers a small ClearThreadvarMembers proc once in its initialization; ShutdownThreadRuntime drains the registry on every worker thread and the registry's finalization drains it on the main thread, so each managed threadvar is released on both teardown paths. The two memos keep their finalization and are cleared on the worker path via explicit ShutdownThreadRuntime calls. Also close two pre-existing main-thread finalization gaps (ImportMeta had none; TimeZone's did not call ClearTimeZoneCache). Co-Authored-By: Claude Opus 4.8 --- .../adr/0077-thread-local-cleanup-registry.md | 14 +++ docs/adr/README.md | 1 + source/shared/TextSemantics.pas | 5 ++ source/units/Goccia.Builtins.CSV.pas | 9 ++ source/units/Goccia.Builtins.Console.pas | 12 ++- source/units/Goccia.Builtins.GlobalArray.pas | 9 ++ .../Goccia.Builtins.GlobalArrayBuffer.pas | 9 ++ source/units/Goccia.Builtins.GlobalBigInt.pas | 9 ++ source/units/Goccia.Builtins.GlobalFFI.pas | 9 ++ source/units/Goccia.Builtins.GlobalNumber.pas | 9 ++ source/units/Goccia.Builtins.GlobalObject.pas | 9 ++ .../units/Goccia.Builtins.GlobalPromise.pas | 9 ++ .../units/Goccia.Builtins.GlobalReflect.pas | 9 ++ source/units/Goccia.Builtins.GlobalRegExp.pas | 10 +++ source/units/Goccia.Builtins.GlobalString.pas | 9 ++ source/units/Goccia.Builtins.GlobalSymbol.pas | 10 +++ source/units/Goccia.Builtins.GlobalURL.pas | 9 ++ source/units/Goccia.Builtins.JSON.pas | 9 ++ source/units/Goccia.Builtins.JSON5.pas | 9 ++ source/units/Goccia.Builtins.JSONL.pas | 9 ++ source/units/Goccia.Builtins.Math.pas | 9 ++ source/units/Goccia.Builtins.TOML.pas | 9 ++ source/units/Goccia.Builtins.TSV.pas | 9 ++ source/units/Goccia.Builtins.YAML.pas | 9 ++ source/units/Goccia.ImportMeta.pas | 6 ++ source/units/Goccia.RegExp.VM.pas | 5 ++ source/units/Goccia.Temporal.TimeZone.pas | 4 + .../units/Goccia.ThreadCleanupLeak.Test.pas | 90 +++++++++++++++++++ source/units/Goccia.ThreadCleanupRegistry.pas | 78 ++++++++++++++++ source/units/Goccia.Threading.Test.pas | 87 ++++++++++++++++++ source/units/Goccia.Threading.pas | 12 +++ .../units/Goccia.Values.ArrayBufferValue.pas | 7 ++ source/units/Goccia.Values.ArrayValue.pas | 7 ++ source/units/Goccia.Values.BigIntValue.pas | 7 ++ .../Goccia.Values.BooleanObjectValue.pas | 7 ++ source/units/Goccia.Values.FFILibrary.pas | 7 ++ source/units/Goccia.Values.FFIPointer.pas | 7 ++ ...occia.Values.FinalizationRegistryValue.pas | 7 ++ source/units/Goccia.Values.HeadersValue.pas | 7 ++ source/units/Goccia.Values.IntlCollator.pas | 7 ++ .../Goccia.Values.IntlDateTimeFormat.pas | 7 ++ .../units/Goccia.Values.IntlDisplayNames.pas | 7 ++ .../Goccia.Values.IntlDurationFormat.pas | 7 ++ source/units/Goccia.Values.IntlListFormat.pas | 7 ++ source/units/Goccia.Values.IntlLocale.pas | 7 ++ .../units/Goccia.Values.IntlNumberFormat.pas | 7 ++ .../units/Goccia.Values.IntlPluralRules.pas | 7 ++ .../Goccia.Values.IntlRelativeTimeFormat.pas | 7 ++ source/units/Goccia.Values.IntlSegmenter.pas | 9 ++ source/units/Goccia.Values.IteratorValue.pas | 8 ++ source/units/Goccia.Values.MapValue.pas | 7 ++ .../units/Goccia.Values.NumberObjectValue.pas | 7 ++ source/units/Goccia.Values.ObjectValue.pas | 7 ++ source/units/Goccia.Values.PromiseValue.pas | 7 ++ source/units/Goccia.Values.ResponseValue.pas | 7 ++ source/units/Goccia.Values.SetValue.pas | 7 ++ .../Goccia.Values.SharedArrayBufferValue.pas | 7 ++ .../units/Goccia.Values.StringObjectValue.pas | 7 ++ source/units/Goccia.Values.SymbolValue.pas | 7 ++ .../units/Goccia.Values.TemporalDuration.pas | 7 ++ .../units/Goccia.Values.TemporalInstant.pas | 7 ++ .../units/Goccia.Values.TemporalPlainDate.pas | 7 ++ .../Goccia.Values.TemporalPlainDateTime.pas | 7 ++ .../Goccia.Values.TemporalPlainMonthDay.pas | 7 ++ .../units/Goccia.Values.TemporalPlainTime.pas | 7 ++ .../Goccia.Values.TemporalPlainYearMonth.pas | 7 ++ .../Goccia.Values.TemporalZonedDateTime.pas | 7 ++ .../units/Goccia.Values.TextDecoderValue.pas | 7 ++ .../units/Goccia.Values.TextEncoderValue.pas | 7 ++ .../units/Goccia.Values.TypedArrayValue.pas | 7 ++ source/units/Goccia.Values.URLValue.pas | 7 ++ source/units/Goccia.Values.WeakMapValue.pas | 7 ++ source/units/Goccia.Values.WeakRefValue.pas | 7 ++ source/units/Goccia.Values.WeakSetValue.pas | 7 ++ 74 files changed, 799 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0077-thread-local-cleanup-registry.md create mode 100644 source/units/Goccia.ThreadCleanupLeak.Test.pas create mode 100644 source/units/Goccia.ThreadCleanupRegistry.pas diff --git a/docs/adr/0077-thread-local-cleanup-registry.md b/docs/adr/0077-thread-local-cleanup-registry.md new file mode 100644 index 000000000..f8f57eb5f --- /dev/null +++ b/docs/adr/0077-thread-local-cleanup-registry.md @@ -0,0 +1,14 @@ +# Thread-local cleanup registry for managed threadvars + +**Date:** 2026-06-27 +**Area:** `engine` +**Issue:** [#885](https://github.com/frostney/GocciaScript/issues/885) +**Pull Request:** [#891](https://github.com/frostney/GocciaScript/pull/891) + +FPC does not auto-finalize managed `threadvar`s (AnsiString, dynamic arrays, interfaces) when a thread exits. The engine caches per-thread state in managed threadvars: ~64 builtin and value units hold their member-definition arrays (`FStaticMembers` / `FPrototypeMembers : TArray`, whose records carry `string` fields), and two units hold input memos (the RegExp input-decode memo, #805, and the is-ASCII memo, #806). Worker threads spawned by `TGocciaThreadPool` rebuild these caches per thread, so each worker leaked its last-held copies on exit. The leak is bounded (one copy per thread, overwritten in place) but matters for embedders that create and destroy many engine threads, and it is a prerequisite for any future heaptrc zero-leak gate. Per [VISION.md](../../VISION.md) this is a cleanliness / embeddability fix, not a performance change. + +Added `Goccia.ThreadCleanupRegistry`: a dependency-free unit exposing `RegisterThreadvarCleanup(proc)` and `RunThreadvarCleanups`. Each leaking unit declares a small `ClearThreadvarMembers` proc that `SetLength`s its own member-definition threadvar(s) to zero and registers it once from the unit's `initialization` section. `Goccia.Threading.ShutdownThreadRuntime` drains the registry on every worker thread before it exits, and the registry unit's own `finalization` drains it on the main thread at process shutdown — so each callback releases the *calling* thread's copy on both paths, with no per-unit `finalization` boilerplate. Registration happens only during unit initialization, which FPC runs single-threaded before the program body spawns any worker, so the callback list is written once and read concurrently afterwards without a lock. Per-unit clear procs are unavoidable: `@threadvar` resolves to the registering thread's instance, so a generic pointer-based clear cannot release another thread's copy — each unit must expose a proc that names its own threadvar. + +Two narrower alternatives were rejected. **Ad-hoc per-unit clears wired directly into `ShutdownThreadRuntime`** (mirroring the existing `DisposableStack` / `Semver` pattern) does not scale to ~64 units: it would couple `Goccia.Threading` to every builtin and value unit through its `uses` clause, require a ~64-line central call list kept in sync forever, and need ~64 separate `finalization` sections that the registry collapses into one. **Centralizing the member-definition arrays in a single per-thread store** was the largest change — it rewrites the member-registration path in all ~64 units, alters *how* members are built rather than only their lifetime (against the issue's lifetime-only scope), carries the most regression risk, and does not fit the two string memos. + +The two memos already finalize on the main thread, so they only needed the worker path: `ShutdownThreadRuntime` calls their exported `ClearRegExpInputMemo` / `ClearAsciiMemo` explicitly, the same shape as the five caches already routed there (ImportMeta, Atomics, DisposableStack, Semver, TimeZone). This change also closes two pre-existing main-thread finalization gaps surfaced while auditing those caches: `Goccia.ImportMeta` had no `finalization`, and `Goccia.Temporal.TimeZone`'s `finalization` did not call `ClearTimeZoneCache`. Migrating the five explicit caches (and the two memos) into the registry for one uniform mechanism, and auditing object-reference threadvars (e.g. the symbol registry), are deferred to follow-ups. A `Goccia.ThreadCleanupLeak.Test` Pascal gate spawns repeated worker cycles and asserts the live heap does not grow per cycle, locking the behaviour in. [core-patterns.md](../core-patterns.md). [garbage-collector.md](../garbage-collector.md). diff --git a/docs/adr/README.md b/docs/adr/README.md index 1be3f122d..0b6f901d7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -86,3 +86,4 @@ Durable architecture and implementation decisions for GocciaScript. New ADRs use - [0074 — Deferred bytecode call-stack frames](0074-deferred-bytecode-call-stack-frames.md) - [0075 — ShadowRealm full test262 conformance](0075-shadowrealm-conformance.md) - [0076 — Same-runner benchmark comparison](0076-same-runner-benchmark-comparison.md) +- [0077 — Thread-local cleanup registry for managed threadvars](0077-thread-local-cleanup-registry.md) diff --git a/source/shared/TextSemantics.pas b/source/shared/TextSemantics.pas index 3ee8c62fd..c677a570f 100644 --- a/source/shared/TextSemantics.pas +++ b/source/shared/TextSemantics.pas @@ -71,6 +71,11 @@ function NormalizeNewlinesToLF(const AText: string): string; function NormalizeUTF8NewlinesToLF(const AText: UTF8String): UTF8String; function StringListToLFText(const ALines: TStrings): string; +{ Release the per-thread is-ASCII memo. Called from this unit's finalization + (main thread) and from ShutdownThreadRuntime (worker threads), because FPC + does not auto-finalize managed threadvars at thread exit. } +procedure ClearAsciiMemo; + implementation uses diff --git a/source/units/Goccia.Builtins.CSV.pas b/source/units/Goccia.Builtins.CSV.pas index 5de2931dc..5e2568a7e 100644 --- a/source/units/Goccia.Builtins.CSV.pas +++ b/source/units/Goccia.Builtins.CSV.pas @@ -48,6 +48,7 @@ implementation uses Goccia.Constants.ErrorNames, Goccia.Constants.PropertyNames, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -58,6 +59,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaCSVBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var @@ -423,4 +429,7 @@ function TGocciaCSVBuiltin.CSVStringify( TGocciaCSVStringifier.Stringify(Data, Delimiter, Headers)); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.Console.pas b/source/units/Goccia.Builtins.Console.pas index e403d9339..9dcf86d76 100644 --- a/source/units/Goccia.Builtins.Console.pas +++ b/source/units/Goccia.Builtins.Console.pas @@ -72,11 +72,18 @@ implementation uses SysUtils, - TimingUtils; + TimingUtils, + + Goccia.ThreadCleanupRegistry; threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaConsole.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var Members: TGocciaMemberCollection; @@ -391,4 +398,7 @@ function TGocciaConsole.ConsoleTable(const AArgs: TGocciaArgumentsCollection; co Result := TGocciaUndefinedLiteralValue.UndefinedValue; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalArray.pas b/source/units/Goccia.Builtins.GlobalArray.pas index 3aa67274e..96be60281 100644 --- a/source/units/Goccia.Builtins.GlobalArray.pas +++ b/source/units/Goccia.Builtins.GlobalArray.pas @@ -36,6 +36,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Utils.Arrays, Goccia.Values.ArrayValue, @@ -56,6 +57,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaGlobalArray.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var Members: TGocciaMemberCollection; @@ -801,4 +807,7 @@ function TGocciaGlobalArray.ArrayOf(const AArgs: TGocciaArgumentsCollection; con end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalArrayBuffer.pas b/source/units/Goccia.Builtins.GlobalArrayBuffer.pas index 885713316..1a208653e 100644 --- a/source/units/Goccia.Builtins.GlobalArrayBuffer.pas +++ b/source/units/Goccia.Builtins.GlobalArrayBuffer.pas @@ -38,6 +38,7 @@ implementation Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Values.DataViewValue, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -47,6 +48,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + const NO_MAX_BYTE_LENGTH = -1; @@ -159,4 +165,7 @@ function TGocciaGlobalArrayBuffer.ArrayBufferIsView(const AArgs: TGocciaArgument Result := TGocciaBooleanLiteralValue.FalseValue; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalBigInt.pas b/source/units/Goccia.Builtins.GlobalBigInt.pas index 0302e60c9..cb21c5acf 100644 --- a/source/units/Goccia.Builtins.GlobalBigInt.pas +++ b/source/units/Goccia.Builtins.GlobalBigInt.pas @@ -42,6 +42,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.ObjectModel.Types, + Goccia.ThreadCleanupRegistry, Goccia.Values.BigIntValue, Goccia.Values.ErrorHelper, Goccia.Values.HoleValue, @@ -53,6 +54,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaGlobalBigInt.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var PrototypeInitializer: TGocciaBigIntValue; @@ -277,4 +283,7 @@ function TGocciaGlobalBigInt.BigIntAsUintN(const AArgs: TGocciaArgumentsCollecti Result := TGocciaBigIntValue.Create(BigIntVal.Value.AsUintN(Bits)); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalFFI.pas b/source/units/Goccia.Builtins.GlobalFFI.pas index 0727ca2ef..fd86499b8 100644 --- a/source/units/Goccia.Builtins.GlobalFFI.pas +++ b/source/units/Goccia.Builtins.GlobalFFI.pas @@ -31,6 +31,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.FFI.DynamicLibrary, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.FFILibrary, Goccia.Values.FFIPointer, @@ -39,6 +40,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + const {$IFDEF DARWIN} SHARED_LIBRARY_SUFFIX = '.dylib'; @@ -105,4 +111,7 @@ function TGocciaGlobalFFI.FFISuffixGetter(const AArgs: TGocciaArgumentsCollectio Result := TGocciaStringLiteralValue.Create(SHARED_LIBRARY_SUFFIX); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalNumber.pas b/source/units/Goccia.Builtins.GlobalNumber.pas index e063e7cd6..e08f23957 100644 --- a/source/units/Goccia.Builtins.GlobalNumber.pas +++ b/source/units/Goccia.Builtins.GlobalNumber.pas @@ -36,12 +36,18 @@ implementation Goccia.Arguments.Validator, Goccia.Constants.NumericLimits, + Goccia.ThreadCleanupRegistry, Goccia.Values.NativeFunction, Goccia.Values.ObjectPropertyDescriptor; threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaGlobalNumber.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var Members: TGocciaMemberCollection; @@ -478,4 +484,7 @@ function TGocciaGlobalNumber.NumberIsSafeInteger(const AArgs: TGocciaArgumentsCo Result := TGocciaBooleanLiteralValue.FalseValue; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalObject.pas b/source/units/Goccia.Builtins.GlobalObject.pas index cb99c0549..602c9b6b3 100644 --- a/source/units/Goccia.Builtins.GlobalObject.pas +++ b/source/units/Goccia.Builtins.GlobalObject.pas @@ -60,6 +60,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayBufferValue, Goccia.Values.ArrayValue, @@ -80,6 +81,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + type TPendingDefineProperty = record Name: string; @@ -1389,4 +1395,7 @@ function TGocciaGlobalObject.ObjectGroupBy(const AArgs: TGocciaArgumentsCollecti end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalPromise.pas b/source/units/Goccia.Builtins.GlobalPromise.pas index 8b9cd2f14..6d8a83631 100644 --- a/source/units/Goccia.Builtins.GlobalPromise.pas +++ b/source/units/Goccia.Builtins.GlobalPromise.pas @@ -58,6 +58,7 @@ implementation Goccia.InstructionLimit, Goccia.MicrotaskQueue, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Timeout, Goccia.Utils, Goccia.Values.Error, @@ -76,6 +77,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + type TPromiseCapability = record Promise: TGocciaValue; @@ -1925,4 +1931,7 @@ function TGocciaGlobalPromise.PromiseTry(const AArgs: TGocciaArgumentsCollection Result := Capability.Promise; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalReflect.pas b/source/units/Goccia.Builtins.GlobalReflect.pas index f8ea267d4..193f2c70c 100644 --- a/source/units/Goccia.Builtins.GlobalReflect.pas +++ b/source/units/Goccia.Builtins.GlobalReflect.pas @@ -43,6 +43,7 @@ implementation Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.Error, @@ -56,6 +57,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + { Helper: validate target is an object, throw TypeError if not } procedure RequireObjectTarget(const ATarget: TGocciaValue; const AMethodName: string); @@ -641,4 +647,7 @@ function TGocciaGlobalReflect.ReflectSetPrototypeOf(const AArgs: TGocciaArgument Result := TGocciaBooleanLiteralValue.TrueValue; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalRegExp.pas b/source/units/Goccia.Builtins.GlobalRegExp.pas index b926de7f0..d11e00073 100644 --- a/source/units/Goccia.Builtins.GlobalRegExp.pas +++ b/source/units/Goccia.Builtins.GlobalRegExp.pas @@ -88,6 +88,7 @@ implementation Goccia.GarbageCollector, Goccia.RegExp.Engine, Goccia.RegExp.Runtime, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -102,6 +103,12 @@ implementation FPrototypeMembers: TArray; FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); + SetLength(FStaticMembers, 0); +end; + type TRegexReplacementCapture = record Value: TGocciaValue; @@ -1479,4 +1486,7 @@ function TGocciaGlobalRegExp.RegExpSymbolSplit( end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalString.pas b/source/units/Goccia.Builtins.GlobalString.pas index 9a4765a72..5a048453d 100644 --- a/source/units/Goccia.Builtins.GlobalString.pas +++ b/source/units/Goccia.Builtins.GlobalString.pas @@ -33,6 +33,7 @@ implementation Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -42,6 +43,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaGlobalString.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var Members: TGocciaMemberCollection; @@ -201,4 +207,7 @@ function TGocciaGlobalString.StringRaw(const AArgs: TGocciaArgumentsCollection; Result := TGocciaStringLiteralValue.Create(SB.ToString); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalSymbol.pas b/source/units/Goccia.Builtins.GlobalSymbol.pas index 185c3ca6e..7f2aaf9cb 100644 --- a/source/units/Goccia.Builtins.GlobalSymbol.pas +++ b/source/units/Goccia.Builtins.GlobalSymbol.pas @@ -47,6 +47,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.HoleValue, Goccia.Values.ObjectPropertyDescriptor, @@ -55,6 +56,7 @@ implementation threadvar FStaticMembers: TArray; + // ES2026 §20.4.2.2: the GlobalSymbolRegistry is one per agent (thread). Every // realm in the thread — the main realm, ShadowRealm child realms, and // $262.createRealm realms — shares it, so Symbol.for(key) yields the same @@ -64,6 +66,11 @@ implementation GSharedSymbolRegistry: TOrderedStringMap; GSharedSymbolRegistryRefs: Integer; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + function SharedSymbolRegistry: TOrderedStringMap; begin if not Assigned(GSharedSymbolRegistry) then @@ -257,4 +264,7 @@ function TGocciaGlobalSymbol.SymbolKeyFor(const AArgs: TGocciaArgumentsCollectio Result := TGocciaUndefinedLiteralValue.UndefinedValue; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.GlobalURL.pas b/source/units/Goccia.Builtins.GlobalURL.pas index d2a668559..540bff2b5 100644 --- a/source/units/Goccia.Builtins.GlobalURL.pas +++ b/source/units/Goccia.Builtins.GlobalURL.pas @@ -44,6 +44,7 @@ implementation Goccia.Arguments.Validator, Goccia.Constants.ConstructorNames, Goccia.Constants.PropertyNames, + Goccia.ThreadCleanupRegistry, Goccia.URL.Parser, Goccia.Values.ErrorHelper, Goccia.Values.URLSearchParamsValue, @@ -52,6 +53,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + { TGocciaGlobalURL } constructor TGocciaGlobalURL.Create(const AName: string; @@ -159,4 +165,7 @@ constructor TGocciaGlobalURLSearchParams.Create(const AName: string; TGocciaURLSearchParamsValue.ExposePrototype(FBuiltinObject); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.JSON.pas b/source/units/Goccia.Builtins.JSON.pas index 38fa8ea37..c2e49454a 100644 --- a/source/units/Goccia.Builtins.JSON.pas +++ b/source/units/Goccia.Builtins.JSON.pas @@ -58,6 +58,7 @@ implementation Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.Error, Goccia.Values.ErrorHelper, @@ -73,6 +74,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaJSONBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var Members: TGocciaMemberCollection; @@ -624,4 +630,7 @@ function TGocciaJSONBuiltin.JSONStringify(const AArgs: TGocciaArgumentsCollectio end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.JSON5.pas b/source/units/Goccia.Builtins.JSON5.pas index 857a3b284..61b7a052a 100644 --- a/source/units/Goccia.Builtins.JSON5.pas +++ b/source/units/Goccia.Builtins.JSON5.pas @@ -70,6 +70,7 @@ implementation Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.Error, Goccia.Values.ErrorHelper, @@ -85,6 +86,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + function UTF8CopyByCharacters(const AText: string; const AMaxChars: Integer): string; var @@ -634,4 +640,7 @@ function TGocciaJSON5Builtin.JSON5Stringify( end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.JSONL.pas b/source/units/Goccia.Builtins.JSONL.pas index 5464c2619..79df2a88f 100644 --- a/source/units/Goccia.Builtins.JSONL.pas +++ b/source/units/Goccia.Builtins.JSONL.pas @@ -45,6 +45,7 @@ implementation Goccia.Constants.ErrorNames, Goccia.Constants.PropertyNames, Goccia.Error.Messages, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.ObjectValue, @@ -53,6 +54,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaJSONLBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var @@ -252,4 +258,7 @@ function TGocciaJSONLBuiltin.JSONLParseChunk( end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.Math.pas b/source/units/Goccia.Builtins.Math.pas index 834c01e75..5ef55dc6b 100644 --- a/source/units/Goccia.Builtins.Math.pas +++ b/source/units/Goccia.Builtins.Math.pas @@ -71,6 +71,7 @@ implementation Goccia.Error.Suggestions, Goccia.Float16, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Values.ArrayValue, Goccia.Values.ClassHelper, Goccia.Values.ErrorHelper, @@ -83,6 +84,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + { TGocciaMath } constructor TGocciaMath.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); @@ -1255,4 +1261,7 @@ function TGocciaMath.MathSumPrecise(const AArgs: TGocciaArgumentsCollection; con Result := TGocciaNumberLiteralValue.Create(Sum + Compensation); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.TOML.pas b/source/units/Goccia.Builtins.TOML.pas index 394ede160..db210b738 100644 --- a/source/units/Goccia.Builtins.TOML.pas +++ b/source/units/Goccia.Builtins.TOML.pas @@ -34,6 +34,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -41,6 +42,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaTOMLBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var @@ -88,4 +94,7 @@ function TGocciaTOMLBuiltin.TOMLParse( end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.TSV.pas b/source/units/Goccia.Builtins.TSV.pas index 8f7f9e328..829368251 100644 --- a/source/units/Goccia.Builtins.TSV.pas +++ b/source/units/Goccia.Builtins.TSV.pas @@ -48,6 +48,7 @@ implementation uses Goccia.Constants.ErrorNames, Goccia.Constants.PropertyNames, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -58,6 +59,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaTSVBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var @@ -418,4 +424,7 @@ function TGocciaTSVBuiltin.TSVStringify( TGocciaTSVStringifier.Stringify(Data, Headers)); end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.Builtins.YAML.pas b/source/units/Goccia.Builtins.YAML.pas index 24c627d34..fd76d875a 100644 --- a/source/units/Goccia.Builtins.YAML.pas +++ b/source/units/Goccia.Builtins.YAML.pas @@ -36,6 +36,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -43,6 +44,11 @@ implementation threadvar FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FStaticMembers, 0); +end; + constructor TGocciaYAMLBuiltin.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); var @@ -107,4 +113,7 @@ function TGocciaYAMLBuiltin.YAMLParseDocuments( end; end; +initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); + end. diff --git a/source/units/Goccia.ImportMeta.pas b/source/units/Goccia.ImportMeta.pas index f707d5c60..255fa19a0 100644 --- a/source/units/Goccia.ImportMeta.pas +++ b/source/units/Goccia.ImportMeta.pas @@ -163,4 +163,10 @@ procedure ClearImportMetaCache; end; end; +finalization + // FPC does not auto-finalize managed threadvars at thread exit. Worker + // threads release this cache through ShutdownThreadRuntime; clear the main + // thread's copy on process shutdown too. + ClearImportMetaCache; + end. diff --git a/source/units/Goccia.RegExp.VM.pas b/source/units/Goccia.RegExp.VM.pas index 05d8d3112..3d0bfc9b3 100644 --- a/source/units/Goccia.RegExp.VM.pas +++ b/source/units/Goccia.RegExp.VM.pas @@ -22,6 +22,11 @@ function ExecuteRegExpVM(const AProgram: TRegExpProgram; const AInput: string; const AStartIndex: Integer; const ARequireStart: Boolean; out AResult: TRegExpVMResult): Boolean; +{ Release the per-thread input-decode memo. Called from this unit's + finalization (main thread) and from ShutdownThreadRuntime (worker threads), + because FPC does not auto-finalize managed threadvars at thread exit. } +procedure ClearRegExpInputMemo; + implementation uses diff --git a/source/units/Goccia.Temporal.TimeZone.pas b/source/units/Goccia.Temporal.TimeZone.pas index ff0fc670f..8c6856361 100644 --- a/source/units/Goccia.Temporal.TimeZone.pas +++ b/source/units/Goccia.Temporal.TimeZone.pas @@ -1971,6 +1971,10 @@ initialization {$ENDIF} finalization + // FPC does not auto-finalize managed threadvars at thread exit. Worker + // threads release the cache through ShutdownThreadRuntime; clear the main + // thread's copy on process shutdown too. + ClearTimeZoneCache; {$IFDEF MSWINDOWS} DoneCriticalSection(WindowsICUInitLock); {$ENDIF} diff --git a/source/units/Goccia.ThreadCleanupLeak.Test.pas b/source/units/Goccia.ThreadCleanupLeak.Test.pas new file mode 100644 index 000000000..ceaf1bec6 --- /dev/null +++ b/source/units/Goccia.ThreadCleanupLeak.Test.pas @@ -0,0 +1,90 @@ +{ Leak-regression gate for issue #885: managed threadvars must be released when + a thread tears down its runtime. + + FPC does not auto-finalize managed threadvars at thread exit, so the per-thread + member-definition arrays that every builtin and value type caches + (FStaticMembers / FPrototypeMembers : TArray, whose + records hold strings) stay allocated unless something clears them. + ShutdownThreadRuntime drains Goccia.ThreadCleanupRegistry on each worker thread, + and the registry's finalization drains it on the main thread, so the registered + ClearThreadvarMembers procs release those arrays on whichever thread tears down. + + This test populates the current thread's member-definition threadvars by + building a throwaway engine, then asserts that draining the registry reclaims a + meaningful amount of live heap. If the drain regresses to a no-op (cleanups not + registered, or ShutdownThreadRuntime stops draining), nothing is reclaimed and + the assertion fails. The companion test in Goccia.Threading.Test proves + ShutdownThreadRuntime drains the registry once per worker exit, so together they + cover the worker-thread teardown path. Runs in CI as a Pascal unit test. } + +program Goccia.ThreadCleanupLeak.Test; + +{$I Goccia.inc} + +uses + {$IFDEF UNIX}cthreads,{$ENDIF} + SysUtils, + + Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, + Goccia.Threading.Init, + Goccia.Values.Primitives, + TestingPascalLibrary, + + Goccia.TestSetup; + +type + TLeakTests = class(TTestSuite) + public + procedure SetupTests; override; + + procedure TestDrainReclaimsMemberDefinitionThreadvars; + end; + +procedure TLeakTests.SetupTests; +begin + Test('draining the registry reclaims per-thread member-definition threadvars', + TestDrainReclaimsMemberDefinitionThreadvars); +end; + +procedure TLeakTests.TestDrainReclaimsMemberDefinitionThreadvars; +const + // One engine populates ~64 member-definition arrays (records with strings); + // their combined heap is far above this floor, while collector/allocator noise + // around a single Collect stays well below it. + MIN_RECLAIMED_BYTES = 8 * 1024; +var + Populated, Drained: Int64; +begin + // Building a throwaway engine registers every builtin and value type, which + // populates this thread's FStaticMembers / FPrototypeMembers threadvars. The + // engine objects themselves are freed inside the call; the member-definition + // arrays are threadvars and outlive it (the leak this issue is about). + EnsureSharedPrototypesInitialized; + TGarbageCollector.Instance.Collect; + Populated := Int64(GetHeapStatus.TotalAllocated); + + // Draining the registry runs every ClearThreadvarMembers, releasing those + // managed arrays on this thread — exactly what ShutdownThreadRuntime does on a + // worker thread and what the registry finalization does on the main thread. + RunThreadvarCleanups; + TGarbageCollector.Instance.Collect; + Drained := Int64(GetHeapStatus.TotalAllocated); + + Expect((Populated - Drained) >= MIN_RECLAIMED_BYTES).ToBe(True); +end; + +begin + // EnsureSharedPrototypesInitialized builds singletons whose getters assert they + // were created on the main thread; pre-build them here first. + TGarbageCollector.Initialize; + PinPrimitiveSingletons; + try + TestRunnerProgram.AddSuite(TLeakTests.Create('ThreadCleanupLeak')); + TestRunnerProgram.Run; + finally + TGarbageCollector.Shutdown; + end; + + ExitCode := TestResultToExitCode; +end. diff --git a/source/units/Goccia.ThreadCleanupRegistry.pas b/source/units/Goccia.ThreadCleanupRegistry.pas new file mode 100644 index 000000000..39aa2cd00 --- /dev/null +++ b/source/units/Goccia.ThreadCleanupRegistry.pas @@ -0,0 +1,78 @@ +{ Registry of per-thread cleanup callbacks drained at worker-thread exit and + at main-thread shutdown. + + FPC does not auto-finalize managed threadvars (AnsiString, dynamic arrays, + interfaces) when a thread exits. Engine units that keep per-thread managed + state in a threadvar register a parameterless cleanup proc here once, in + their unit initialization. Goccia.Threading.ShutdownThreadRuntime drains the + registry on each worker thread before it exits, and this unit's finalization + drains it on the main thread at process shutdown, so every callback releases + the calling thread's own threadvar copy on both paths. + + Threading contract: RegisterThreadvarCleanup is only ever called from unit + initialization sections, which FPC runs sequentially on the main thread + before the program body spawns any worker thread. The callback list is + therefore written once during startup and only read afterwards, so the + concurrent worker-thread reads in RunThreadvarCleanups need no lock. } + +unit Goccia.ThreadCleanupRegistry; + +{$I Goccia.inc} + +interface + +type + { Parameterless cleanup callback. Must release only the calling thread's own + managed threadvars (e.g. SetLength(FMembers, 0)); it runs on whichever + thread drains the registry, so it must not touch another thread's state. } + TGocciaThreadvarCleanupProc = procedure; + +{ Register a threadvar-cleanup callback. Call once per unit, from the unit's + initialization section (before any worker thread is spawned). A nil callback + is ignored. } +procedure RegisterThreadvarCleanup(const AProc: TGocciaThreadvarCleanupProc); + +{ Run every registered cleanup on the calling thread. Drained by + ShutdownThreadRuntime (worker threads) and this unit's finalization (main + thread). Safe to call multiple times and on any thread. } +procedure RunThreadvarCleanups; + +implementation + +const + CLEANUPS_INITIAL_CAPACITY = 8; + +var + { Write-once at unit initialization (single-threaded), read-only afterwards. + See the threading contract in the unit header. Capacity grows by doubling so + registration stays amortised O(1); GCleanupCount is the live length. } + GCleanups: array of TGocciaThreadvarCleanupProc; + GCleanupCount: Integer; + +procedure RegisterThreadvarCleanup(const AProc: TGocciaThreadvarCleanupProc); +begin + if not Assigned(AProc) then + Exit; + if GCleanupCount >= Length(GCleanups) then + begin + if Length(GCleanups) = 0 then + SetLength(GCleanups, CLEANUPS_INITIAL_CAPACITY) + else + SetLength(GCleanups, Length(GCleanups) * 2); + end; + GCleanups[GCleanupCount] := AProc; + Inc(GCleanupCount); +end; + +procedure RunThreadvarCleanups; +var + I: Integer; +begin + for I := 0 to GCleanupCount - 1 do + GCleanups[I](); +end; + +finalization + RunThreadvarCleanups; + +end. diff --git a/source/units/Goccia.Threading.Test.pas b/source/units/Goccia.Threading.Test.pas index 234426042..8152e18e3 100644 --- a/source/units/Goccia.Threading.Test.pas +++ b/source/units/Goccia.Threading.Test.pas @@ -8,6 +8,7 @@ SysUtils, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Threading, Goccia.Values.Primitives, TestingPascalLibrary, @@ -29,6 +30,8 @@ TTestThreading = class(TTestSuite) procedure TestPoolResetsCancelledBetweenRuns; procedure TestPoolHandlesEmptyFileList; procedure TestPoolSingleWorker; + procedure TestThreadCleanupRegistryRunsRegistered; + procedure TestShutdownThreadRuntimeDrainsRegistryPerWorker; end; { Helpers } @@ -44,6 +47,30 @@ procedure ResetWorkerState; SetLength(GWorkerFileNames, 0); end; +{ Sentinel cleanup callbacks for the ThreadCleanupRegistry tests. Registrations + persist for the process (the registry has no unregister), and a registered + callback fires on every thread that drains the registry — including worker + threads concurrently — so the counters use InterlockedIncrement and each test + resets its own counter before measuring a delta. } +var + GSentinelCount: Integer; + GSentinelWorkerCount: Integer; + +procedure SentinelCleanup; +begin + InterlockedIncrement(GSentinelCount); +end; + +procedure SentinelCleanupSecond; +begin + InterlockedIncrement(GSentinelCount); +end; + +procedure SentinelWorkerCleanup; +begin + InterlockedIncrement(GSentinelWorkerCount); +end; + type TTestWorkerHost = class procedure CountingWorker(const AFileName: string; @@ -95,6 +122,8 @@ procedure TTestThreading.SetupTests; Test('Pool resets Cancelled between runs', TestPoolResetsCancelledBetweenRuns); Test('Pool handles empty file list', TestPoolHandlesEmptyFileList); Test('Pool single worker processes all files', TestPoolSingleWorker); + Test('ThreadCleanupRegistry runs registered callbacks', TestThreadCleanupRegistryRunsRegistered); + Test('ShutdownThreadRuntime drains registry once per worker', TestShutdownThreadRuntimeDrainsRegistryPerWorker); end; procedure TTestThreading.TestWorkQueueDrainsAllItems; @@ -385,6 +414,64 @@ procedure TTestThreading.TestPoolSingleWorker; end; end; +procedure TTestThreading.TestThreadCleanupRegistryRunsRegistered; +begin + // A nil callback is ignored (no crash, nothing registered). + RegisterThreadvarCleanup(nil); + + // A registered callback runs when the registry is drained. + GSentinelCount := 0; + RegisterThreadvarCleanup(@SentinelCleanup); + RunThreadvarCleanups; + Expect(GSentinelCount).ToBe(1); + + // Draining again is safe and re-runs the callback (repeatable on any thread). + GSentinelCount := 0; + RunThreadvarCleanups; + Expect(GSentinelCount).ToBe(1); + + // Every registered callback runs, not just the first. + GSentinelCount := 0; + RegisterThreadvarCleanup(@SentinelCleanupSecond); + RunThreadvarCleanups; + Expect(GSentinelCount).ToBe(2); +end; + +procedure TTestThreading.TestShutdownThreadRuntimeDrainsRegistryPerWorker; +const + WORKER_COUNT = 3; +var + Pool: TGocciaThreadPool; + Files: TStringList; + Host: TTestWorkerHost; +begin + // Each worker thread calls ShutdownThreadRuntime as it exits, which drains the + // registry on that thread. With WORKER_COUNT workers, the registered callback + // must fire exactly WORKER_COUNT times — proving the per-worker-exit wiring. + RegisterThreadvarCleanup(@SentinelWorkerCleanup); + GSentinelWorkerCount := 0; + + ResetWorkerState; + Host := TTestWorkerHost.Create; + Files := TStringList.Create; + try + Files.Add('w1.js'); + Files.Add('w2.js'); + Files.Add('w3.js'); + + Pool := TGocciaThreadPool.Create(WORKER_COUNT); + try + Pool.RunAll(Files, Host.CountingWorker); + Expect(GSentinelWorkerCount).ToBe(WORKER_COUNT); + finally + Pool.Free; + end; + finally + Files.Free; + Host.Free; + end; +end; + begin // Worker threads call InitThreadRuntime → PinPrimitiveSingletons, which // in turn touches UndefinedValue/NullValue/... — those getters assert diff --git a/source/units/Goccia.Threading.pas b/source/units/Goccia.Threading.pas index 525fa6fa5..fe3fcb799 100644 --- a/source/units/Goccia.Threading.pas +++ b/source/units/Goccia.Threading.pas @@ -187,6 +187,7 @@ implementation Math, SysUtils, + TextSemantics, TimingUtils, Goccia.Builtins.Atomics, @@ -197,7 +198,9 @@ implementation Goccia.GarbageCollector, Goccia.ImportMeta, Goccia.MicrotaskQueue, + Goccia.RegExp.VM, Goccia.Temporal.TimeZone, + Goccia.ThreadCleanupRegistry, Goccia.Values.Primitives; { Thread runtime lifecycle } @@ -234,6 +237,15 @@ procedure ShutdownThreadRuntime; ClearDisposableStackSlotMap; ClearSemverHosts; ClearTimeZoneCache; + // The #805/#806 memos already finalize on the main thread but, like the + // caches above, FPC does not release their managed threadvars on a worker + // thread exit — clear them here too. + ClearRegExpInputMemo; + ClearAsciiMemo; + // Drain the engine-wide threadvar-cleanup registry: every builtin's and + // value type's cached member-definition array registers its release proc in + // Goccia.ThreadCleanupRegistry. This releases this worker thread's copies. + RunThreadvarCleanups; TGocciaMicrotaskQueue.Shutdown; TGocciaCallStack.Shutdown; TGarbageCollector.Shutdown; diff --git a/source/units/Goccia.Values.ArrayBufferValue.pas b/source/units/Goccia.Values.ArrayBufferValue.pas index 6b2ceda0e..1e1e37a9b 100644 --- a/source/units/Goccia.Values.ArrayBufferValue.pas +++ b/source/units/Goccia.Values.ArrayBufferValue.pas @@ -77,6 +77,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, Goccia.Values.ObjectPropertyDescriptor, @@ -88,6 +89,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetArrayBufferShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -697,6 +703,7 @@ function TGocciaArrayBufferValue.ArrayBufferSlice(const AArgs: TGocciaArgumentsC end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GArrayBufferSharedSlot := RegisterRealmOwnedSlot('ArrayBuffer.shared'); end. diff --git a/source/units/Goccia.Values.ArrayValue.pas b/source/units/Goccia.Values.ArrayValue.pas index 325788413..7ca4f80ed 100644 --- a/source/units/Goccia.Values.ArrayValue.pas +++ b/source/units/Goccia.Values.ArrayValue.pas @@ -129,6 +129,7 @@ implementation Goccia.GarbageCollector, Goccia.Generator.Continuation, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Timeout, Goccia.Utils, Goccia.Utils.Arrays, @@ -154,6 +155,11 @@ implementation FPrototypeMethodHost: TGocciaArrayValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSharedArrayPrototype: TGocciaObjectValue; inline; begin if Assigned(CurrentRealm) then @@ -4275,6 +4281,7 @@ function TGocciaArrayValue.ArraySymbolIterator(const AArgs: TGocciaArgumentsColl end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GArrayPrototypeSlot := RegisterRealmSlot('Array.prototype'); end. diff --git a/source/units/Goccia.Values.BigIntValue.pas b/source/units/Goccia.Values.BigIntValue.pas index 3dafc5503..4ea551587 100644 --- a/source/units/Goccia.Values.BigIntValue.pas +++ b/source/units/Goccia.Values.BigIntValue.pas @@ -63,6 +63,7 @@ implementation Goccia.GarbageCollector, Goccia.ObjectModel, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Threading, Goccia.Utils, Goccia.Values.BigIntObjectValue, @@ -122,6 +123,11 @@ function TryStringToBigInt(const AValue: string; out AResult: TBigInteger): Bool FMethodHost: TGocciaBigIntValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSharedBigIntPrimitivePrototype: TGocciaObjectValue; inline; begin if Assigned(CurrentRealm) then @@ -335,6 +341,7 @@ function TGocciaBigIntValue.BigIntToLocaleString(const AArgs: TGocciaArgumentsCo end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GBigIntPrimitivePrototypeSlot := RegisterRealmSlot('BigInt.prototype'); end. diff --git a/source/units/Goccia.Values.BooleanObjectValue.pas b/source/units/Goccia.Values.BooleanObjectValue.pas index f7882c34b..21df7cf9e 100644 --- a/source/units/Goccia.Values.BooleanObjectValue.pas +++ b/source/units/Goccia.Values.BooleanObjectValue.pas @@ -38,6 +38,7 @@ implementation uses Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ToObject; @@ -50,6 +51,11 @@ implementation FPrototypeMethodHost: TGocciaBooleanObjectValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSharedBooleanPrototype: TGocciaObjectValue; inline; begin if Assigned(CurrentRealm) then @@ -166,6 +172,7 @@ procedure TGocciaBooleanObjectValue.MarkReferences; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GBooleanPrototypeSlot := RegisterRealmSlot('Boolean.prototype'); end. diff --git a/source/units/Goccia.Values.FFILibrary.pas b/source/units/Goccia.Values.FFILibrary.pas index a11ff6398..ba4fe13d3 100644 --- a/source/units/Goccia.Values.FFILibrary.pas +++ b/source/units/Goccia.Values.FFILibrary.pas @@ -49,6 +49,7 @@ implementation Goccia.FFI.Types, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayBufferValue, Goccia.Values.ArrayValue, @@ -66,6 +67,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetFFILibraryShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -702,6 +708,7 @@ function TGocciaFFILibraryValue.ClosedGetter(const AArgs: TGocciaArgumentsCollec end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GFFILibrarySharedSlot := RegisterRealmOwnedSlot('FFILibrary.shared'); end. diff --git a/source/units/Goccia.Values.FFIPointer.pas b/source/units/Goccia.Values.FFIPointer.pas index e376f61be..bab38a10c 100644 --- a/source/units/Goccia.Values.FFIPointer.pas +++ b/source/units/Goccia.Values.FFIPointer.pas @@ -47,6 +47,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -58,6 +59,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetFFIPointerShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -194,6 +200,7 @@ function TGocciaFFIPointerValue.AddressGetter(const AArgs: TGocciaArgumentsColle end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GFFIPointerSharedSlot := RegisterRealmOwnedSlot('FFIPointer.shared'); GFFINullPointerSlot := RegisterRealmSlot('FFIPointer.nullPointer'); diff --git a/source/units/Goccia.Values.FinalizationRegistryValue.pas b/source/units/Goccia.Values.FinalizationRegistryValue.pas index 238a227f1..66508d8e5 100644 --- a/source/units/Goccia.Values.FinalizationRegistryValue.pas +++ b/source/units/Goccia.Values.FinalizationRegistryValue.pas @@ -56,6 +56,7 @@ implementation Goccia.GarbageCollector, Goccia.MicrotaskQueue, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue, @@ -67,6 +68,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetFinalizationRegistryShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -284,5 +290,6 @@ function TGocciaFinalizationRegistryValue.FinalizationRegistryUnregister( initialization GFinalizationRegistrySharedSlot := RegisterRealmOwnedSlot('FinalizationRegistry.shared'); + RegisterThreadvarCleanup(@ClearThreadvarMembers); end. diff --git a/source/units/Goccia.Values.HeadersValue.pas b/source/units/Goccia.Values.HeadersValue.pas index f295b13d5..91bcc9c98 100644 --- a/source/units/Goccia.Values.HeadersValue.pas +++ b/source/units/Goccia.Values.HeadersValue.pas @@ -75,6 +75,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -89,6 +90,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetHeadersShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -388,5 +394,6 @@ procedure TGocciaHeadersValue.MarkReferences; initialization GHeadersSharedSlot := RegisterRealmOwnedSlot('Headers.shared'); + RegisterThreadvarCleanup(@ClearThreadvarMembers); end. diff --git a/source/units/Goccia.Values.IntlCollator.pas b/source/units/Goccia.Values.IntlCollator.pas index 744acd6f2..c2a502987 100644 --- a/source/units/Goccia.Values.IntlCollator.pas +++ b/source/units/Goccia.Values.IntlCollator.pas @@ -51,6 +51,7 @@ implementation Goccia.Intl.Helpers, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.NativeFunction, Goccia.Values.ObjectPropertyDescriptor, @@ -62,6 +63,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + type TGocciaIntlCollatorBoundCompareValue = class(TGocciaNativeFunctionValue) private @@ -537,5 +543,6 @@ function TGocciaIntlCollatorValue.IntlCollatorResolvedOptions(const AArgs: TGocc initialization GIntlCollatorSharedSlot := RegisterRealmOwnedSlot('Intl.Collator.shared'); + RegisterThreadvarCleanup(@ClearThreadvarMembers); end. diff --git a/source/units/Goccia.Values.IntlDateTimeFormat.pas b/source/units/Goccia.Values.IntlDateTimeFormat.pas index 899db9e12..6079657c0 100644 --- a/source/units/Goccia.Values.IntlDateTimeFormat.pas +++ b/source/units/Goccia.Values.IntlDateTimeFormat.pas @@ -77,6 +77,7 @@ implementation Goccia.Temporal.Calendar, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -98,6 +99,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + const GREGORIAN_CYCLE_YEARS = 400; NANOSECONDS_PER_MILLISECOND = 1000000; @@ -2407,6 +2413,7 @@ function TGocciaIntlDateTimeFormatValue.IntlDateTimeFormatResolvedOptions(const end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlDateTimeFormatSharedSlot := RegisterRealmOwnedSlot('Intl.DateTimeFormat.shared'); end. diff --git a/source/units/Goccia.Values.IntlDisplayNames.pas b/source/units/Goccia.Values.IntlDisplayNames.pas index ece34de3e..34e88d3c8 100644 --- a/source/units/Goccia.Values.IntlDisplayNames.pas +++ b/source/units/Goccia.Values.IntlDisplayNames.pas @@ -45,6 +45,7 @@ implementation Goccia.Intl.CLDRData, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -55,6 +56,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlDisplayNamesShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -424,6 +430,7 @@ function TGocciaIntlDisplayNamesValue.IntlDisplayNamesResolvedOptions(const AArg end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlDisplayNamesSharedSlot := RegisterRealmOwnedSlot('Intl.DisplayNames.shared'); end. diff --git a/source/units/Goccia.Values.IntlDurationFormat.pas b/source/units/Goccia.Values.IntlDurationFormat.pas index 209fb6ebe..bdb664e45 100644 --- a/source/units/Goccia.Values.IntlDurationFormat.pas +++ b/source/units/Goccia.Values.IntlDurationFormat.pas @@ -78,6 +78,7 @@ implementation Goccia.Realm, Goccia.Temporal.DurationMath, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -94,6 +95,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlDurationFormatShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1415,6 +1421,7 @@ function TGocciaIntlDurationFormatValue.IntlDurationFormatResolvedOptions(const end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlDurationFormatSharedSlot := RegisterRealmOwnedSlot('Intl.DurationFormat.shared'); end. diff --git a/source/units/Goccia.Values.IntlListFormat.pas b/source/units/Goccia.Values.IntlListFormat.pas index de0410221..3fe31efe2 100644 --- a/source/units/Goccia.Values.IntlListFormat.pas +++ b/source/units/Goccia.Values.IntlListFormat.pas @@ -45,6 +45,7 @@ implementation Goccia.Intl.Helpers, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.IteratorSupport, Goccia.Values.IteratorValue, @@ -57,6 +58,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlListFormatShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -488,6 +494,7 @@ function TGocciaIntlListFormatValue.IntlListFormatResolvedOptions(const AArgs: T end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlListFormatSharedSlot := RegisterRealmOwnedSlot('Intl.ListFormat.shared'); end. diff --git a/source/units/Goccia.Values.IntlLocale.pas b/source/units/Goccia.Values.IntlLocale.pas index 10f877a3d..80b3e7e33 100644 --- a/source/units/Goccia.Values.IntlLocale.pas +++ b/source/units/Goccia.Values.IntlLocale.pas @@ -81,6 +81,7 @@ implementation Goccia.Intl.Helpers, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, @@ -92,6 +93,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlLocaleShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1421,6 +1427,7 @@ function TGocciaIntlLocaleValue.IntlLocaleToString(const AArgs: TGocciaArguments end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlLocaleSharedSlot := RegisterRealmOwnedSlot('Intl.Locale.shared'); end. diff --git a/source/units/Goccia.Values.IntlNumberFormat.pas b/source/units/Goccia.Values.IntlNumberFormat.pas index 644f8e6c4..8c0c3b2a5 100644 --- a/source/units/Goccia.Values.IntlNumberFormat.pas +++ b/source/units/Goccia.Values.IntlNumberFormat.pas @@ -74,6 +74,7 @@ implementation Goccia.Intl.Helpers, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.BigIntObjectValue, @@ -92,6 +93,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + type TGocciaIntlNumberFormatBoundFormatValue = class(TGocciaNativeFunctionValue) private @@ -2111,6 +2117,7 @@ function TGocciaIntlNumberFormatValue.IntlNumberFormatResolvedOptions(const AArg end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlNumberFormatSharedSlot := RegisterRealmOwnedSlot('Intl.NumberFormat.shared'); end. diff --git a/source/units/Goccia.Values.IntlPluralRules.pas b/source/units/Goccia.Values.IntlPluralRules.pas index df6900ac0..9c8187fcb 100644 --- a/source/units/Goccia.Values.IntlPluralRules.pas +++ b/source/units/Goccia.Values.IntlPluralRules.pas @@ -50,6 +50,7 @@ implementation Goccia.Intl.CLDRData, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -65,6 +66,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlPluralRulesShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -461,6 +467,7 @@ function TGocciaIntlPluralRulesValue.IntlPluralRulesResolvedOptions(const AArgs: end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlPluralRulesSharedSlot := RegisterRealmOwnedSlot('Intl.PluralRules.shared'); end. diff --git a/source/units/Goccia.Values.IntlRelativeTimeFormat.pas b/source/units/Goccia.Values.IntlRelativeTimeFormat.pas index 3b95fcf88..e852fc145 100644 --- a/source/units/Goccia.Values.IntlRelativeTimeFormat.pas +++ b/source/units/Goccia.Values.IntlRelativeTimeFormat.pas @@ -48,6 +48,7 @@ implementation Goccia.Intl.Helpers, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, @@ -59,6 +60,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetIntlRelativeTimeFormatShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -957,6 +963,7 @@ function TGocciaIntlRelativeTimeFormatValue.IntlRelativeTimeFormatResolvedOption end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlRelativeTimeFormatSharedSlot := RegisterRealmOwnedSlot('Intl.RelativeTimeFormat.shared'); end. diff --git a/source/units/Goccia.Values.IntlSegmenter.pas b/source/units/Goccia.Values.IntlSegmenter.pas index dea5609cf..1cef33ff7 100644 --- a/source/units/Goccia.Values.IntlSegmenter.pas +++ b/source/units/Goccia.Values.IntlSegmenter.pas @@ -82,6 +82,7 @@ implementation Goccia.Error.Messages, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -98,6 +99,13 @@ implementation FSegmentsPrototypeMembers: TArray; FSegmentIteratorPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FSegmenterPrototypeMembers, 0); + SetLength(FSegmentsPrototypeMembers, 0); + SetLength(FSegmentIteratorPrototypeMembers, 0); +end; + function GetIntlSegmenterShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -642,6 +650,7 @@ function TGocciaIntlSegmentIteratorValue.IntlSegmentIteratorSymbolIterator(const end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIntlSegmenterSharedSlot := RegisterRealmOwnedSlot('Intl.Segmenter.shared'); GIntlSegmentsSharedSlot := RegisterRealmOwnedSlot('Intl.Segments.shared'); GIntlSegmentIteratorSharedSlot := RegisterRealmOwnedSlot('Intl.SegmentIterator.shared'); diff --git a/source/units/Goccia.Values.IteratorValue.pas b/source/units/Goccia.Values.IteratorValue.pas index cf17d0e50..2451d33b3 100644 --- a/source/units/Goccia.Values.IteratorValue.pas +++ b/source/units/Goccia.Values.IteratorValue.pas @@ -100,6 +100,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.GarbageCollector, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -126,6 +127,12 @@ implementation FPrototypeMembers: TArray; FStaticMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); + SetLength(FStaticMembers, 0); +end; + function GetSharedIteratorPrototype: TGocciaObjectValue; inline; begin if Assigned(CurrentRealm) then @@ -1826,6 +1833,7 @@ procedure TGocciaIteratorHelperValue.Close; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GIteratorPrototypeSlot := RegisterRealmSlot('Iterator.prototype'); GIteratorHelperPrototypeSlot := RegisterRealmSlot('IteratorHelper.prototype'); GIteratorConstructorSlot := RegisterRealmSlot('Iterator'); diff --git a/source/units/Goccia.Values.MapValue.pas b/source/units/Goccia.Values.MapValue.pas index 7b6e045e5..086ad44c7 100644 --- a/source/units/Goccia.Values.MapValue.pas +++ b/source/units/Goccia.Values.MapValue.pas @@ -68,6 +68,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -84,6 +85,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetMapShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -606,5 +612,6 @@ function TGocciaMapValue.MapGetOrInsertComputed(const AArgs: TGocciaArgumentsCol initialization GMapSharedSlot := RegisterRealmOwnedSlot('Map.shared'); + RegisterThreadvarCleanup(@ClearThreadvarMembers); end. diff --git a/source/units/Goccia.Values.NumberObjectValue.pas b/source/units/Goccia.Values.NumberObjectValue.pas index 2feb3214e..f48d27ff0 100644 --- a/source/units/Goccia.Values.NumberObjectValue.pas +++ b/source/units/Goccia.Values.NumberObjectValue.pas @@ -51,6 +51,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.NativeFunction; @@ -64,6 +65,11 @@ implementation FPrototypeMethodHost: TGocciaNumberObjectValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + const DOUBLE_EXPONENT_BIAS = 1023; DOUBLE_EXPONENT_BITS_MASK = $7FF; @@ -648,5 +654,6 @@ function TGocciaNumberObjectValue.NumberToExponential(const AArgs: TGocciaArgume initialization GNumberPrototypeSlot := RegisterRealmSlot('Number.prototype'); + RegisterThreadvarCleanup(@ClearThreadvarMembers); end. diff --git a/source/units/Goccia.Values.ObjectValue.pas b/source/units/Goccia.Values.ObjectValue.pas index 85240abd5..7b71af81b 100644 --- a/source/units/Goccia.Values.ObjectValue.pas +++ b/source/units/Goccia.Values.ObjectValue.pas @@ -139,6 +139,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.ObjectModel, + Goccia.ThreadCleanupRegistry, Goccia.Values.ArrayValue, Goccia.Values.ClassHelper, Goccia.Values.ErrorHelper, @@ -160,6 +161,11 @@ implementation FPrototypeMethodHost: TGocciaObjectValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + const MAX_PROTOTYPE_CHAIN_DEPTH = 256; BYTECODE_PRIVATE_INITIALIZED_PREFIX = '#initialized:'; @@ -1916,6 +1922,7 @@ function TGocciaObjectValue.TestIntegritySealed: Boolean; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GObjectPrototypeSlot := RegisterRealmSlot('Object.prototype'); end. diff --git a/source/units/Goccia.Values.PromiseValue.pas b/source/units/Goccia.Values.PromiseValue.pas index 26def7d0b..18eb17868 100644 --- a/source/units/Goccia.Values.PromiseValue.pas +++ b/source/units/Goccia.Values.PromiseValue.pas @@ -104,6 +104,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.MicrotaskQueue, + Goccia.ThreadCleanupRegistry, Goccia.Values.Error, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -121,6 +122,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetPromiseSharedForRealm( const ARealm: TGocciaRealm): TGocciaSharedPrototype; inline; begin @@ -932,6 +938,7 @@ function TGocciaPromiseValue.ToStringTag: string; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GPromiseSharedSlot := RegisterRealmOwnedSlot('Promise.shared'); GPromiseDefaultConstructorSlot := RegisterRealmSlot('Promise.defaultConstructor'); diff --git a/source/units/Goccia.Values.ResponseValue.pas b/source/units/Goccia.Values.ResponseValue.pas index 81e802896..eab9f32b4 100644 --- a/source/units/Goccia.Values.ResponseValue.pas +++ b/source/units/Goccia.Values.ResponseValue.pas @@ -84,6 +84,7 @@ implementation Goccia.Error.Suggestions, Goccia.JSON, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayBufferValue, Goccia.Values.ErrorHelper, @@ -97,6 +98,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetResponseShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -479,6 +485,7 @@ procedure TGocciaResponseValue.MarkReferences; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GResponseSharedSlot := RegisterRealmOwnedSlot('Response.shared'); end. diff --git a/source/units/Goccia.Values.SetValue.pas b/source/units/Goccia.Values.SetValue.pas index 60668e288..1799baa90 100644 --- a/source/units/Goccia.Values.SetValue.pas +++ b/source/units/Goccia.Values.SetValue.pas @@ -68,6 +68,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -92,6 +93,11 @@ TGocciaSetRecord = record threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSetShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -945,6 +951,7 @@ function TGocciaSetValue.SetIsDisjointFrom(const AArgs: TGocciaArgumentsCollecti end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GSetSharedSlot := RegisterRealmOwnedSlot('Set.shared'); end. diff --git a/source/units/Goccia.Values.SharedArrayBufferValue.pas b/source/units/Goccia.Values.SharedArrayBufferValue.pas index e9ff34603..e0451f8d1 100644 --- a/source/units/Goccia.Values.SharedArrayBufferValue.pas +++ b/source/units/Goccia.Values.SharedArrayBufferValue.pas @@ -61,6 +61,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -71,6 +72,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSharedArrayBufferShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -428,6 +434,7 @@ function TGocciaSharedArrayBufferValue.SharedArrayBufferSlice(const AArgs: TGocc end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GSharedArrayBufferSharedSlot := RegisterRealmOwnedSlot('SharedArrayBuffer.shared'); end. diff --git a/source/units/Goccia.Values.StringObjectValue.pas b/source/units/Goccia.Values.StringObjectValue.pas index 92c957376..dcc658555 100644 --- a/source/units/Goccia.Values.StringObjectValue.pas +++ b/source/units/Goccia.Values.StringObjectValue.pas @@ -102,6 +102,7 @@ implementation Goccia.GarbageCollector, Goccia.Realm, Goccia.RegExp.Runtime, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -122,6 +123,11 @@ implementation FPrototypeMethodHost: TGocciaStringObjectValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSharedStringPrototype: TGocciaObjectValue; inline; begin if Assigned(CurrentRealm) then @@ -2194,6 +2200,7 @@ function TGocciaStringObjectValue.StringToWellFormed(const AArgs: TGocciaArgumen end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GStringPrototypeSlot := RegisterRealmSlot('String.prototype'); end. diff --git a/source/units/Goccia.Values.SymbolValue.pas b/source/units/Goccia.Values.SymbolValue.pas index 829a32d3a..a221b9a00 100644 --- a/source/units/Goccia.Values.SymbolValue.pas +++ b/source/units/Goccia.Values.SymbolValue.pas @@ -96,6 +96,7 @@ implementation Goccia.GarbageCollector, Goccia.ObjectModel, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Threading, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, @@ -113,6 +114,11 @@ implementation FMethodHost: TGocciaSymbolValue; FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + threadvar GNextSymbolId: Integer; GSymbolRegistry: THashMap; @@ -510,6 +516,7 @@ function TGocciaSymbolValue.ToDisplayString: TGocciaStringLiteralValue; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GSymbolPrototypeSlot := RegisterRealmSlot('Symbol.prototype'); end. diff --git a/source/units/Goccia.Values.TemporalDuration.pas b/source/units/Goccia.Values.TemporalDuration.pas index 69caff786..47d733588 100644 --- a/source/units/Goccia.Values.TemporalDuration.pas +++ b/source/units/Goccia.Values.TemporalDuration.pas @@ -132,6 +132,7 @@ implementation Goccia.Temporal.Options, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDurationFormat, @@ -149,6 +150,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + type TDurationRelativeToRecord = record HasRelativeTo: Boolean; @@ -3678,6 +3684,7 @@ function TGocciaTemporalDurationValue.DurationToLocaleString(const AArgs: TGocci end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalDurationSharedSlot := RegisterRealmOwnedSlot('Temporal.Duration.shared'); end. diff --git a/source/units/Goccia.Values.TemporalInstant.pas b/source/units/Goccia.Values.TemporalInstant.pas index 33531feef..4e16c45ac 100644 --- a/source/units/Goccia.Values.TemporalInstant.pas +++ b/source/units/Goccia.Values.TemporalInstant.pas @@ -58,6 +58,7 @@ implementation Goccia.Temporal.Options, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Values.BigIntValue, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -72,6 +73,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function InstantEpochSecond(const AEpochMilliseconds: Int64): Int64; inline; begin Result := AEpochMilliseconds div 1000; @@ -1670,6 +1676,7 @@ function TGocciaTemporalInstantValue.InstantToZonedDateTimeISO(const AArgs: TGoc end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalInstantSharedSlot := RegisterRealmOwnedSlot('Temporal.Instant.shared'); end. diff --git a/source/units/Goccia.Values.TemporalPlainDate.pas b/source/units/Goccia.Values.TemporalPlainDate.pas index 7d2e1d2be..4b4a9c8b4 100644 --- a/source/units/Goccia.Values.TemporalPlainDate.pas +++ b/source/units/Goccia.Values.TemporalPlainDate.pas @@ -83,6 +83,7 @@ implementation Goccia.Temporal.Options, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -101,6 +102,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalPlainDateShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1099,6 +1105,7 @@ function TGocciaTemporalPlainDateValue.DateWithCalendar(const AArgs: TGocciaArgu end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalPlainDateSharedSlot := RegisterRealmOwnedSlot('Temporal.PlainDate.shared'); end. diff --git a/source/units/Goccia.Values.TemporalPlainDateTime.pas b/source/units/Goccia.Values.TemporalPlainDateTime.pas index 6d007172a..cba85b5a6 100644 --- a/source/units/Goccia.Values.TemporalPlainDateTime.pas +++ b/source/units/Goccia.Values.TemporalPlainDateTime.pas @@ -105,6 +105,7 @@ implementation Goccia.Temporal.Options, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -123,6 +124,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalPlainDateTimeShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1699,6 +1705,7 @@ function TGocciaTemporalPlainDateTimeValue.DateTimeWithCalendar(const AArgs: TGo end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalPlainDateTimeSharedSlot := RegisterRealmOwnedSlot('Temporal.PlainDateTime.shared'); end. diff --git a/source/units/Goccia.Values.TemporalPlainMonthDay.pas b/source/units/Goccia.Values.TemporalPlainMonthDay.pas index 4a941cc68..4f187dee2 100644 --- a/source/units/Goccia.Values.TemporalPlainMonthDay.pas +++ b/source/units/Goccia.Values.TemporalPlainMonthDay.pas @@ -65,6 +65,7 @@ implementation Goccia.Realm, Goccia.Temporal.Calendar, Goccia.Temporal.Options, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -81,6 +82,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalPlainMonthDayShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -902,6 +908,7 @@ function TGocciaTemporalPlainMonthDayValue.MonthDayToLocaleString(const AArgs: T end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalPlainMonthDaySharedSlot := RegisterRealmOwnedSlot('Temporal.PlainMonthDay.shared'); end. diff --git a/source/units/Goccia.Values.TemporalPlainTime.pas b/source/units/Goccia.Values.TemporalPlainTime.pas index 0e695ec5e..6b1ef8c7f 100644 --- a/source/units/Goccia.Values.TemporalPlainTime.pas +++ b/source/units/Goccia.Values.TemporalPlainTime.pas @@ -70,6 +70,7 @@ implementation Goccia.Temporal.DurationMath, Goccia.Temporal.Options, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -85,6 +86,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalPlainTimeShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -736,6 +742,7 @@ function TGocciaTemporalPlainTimeValue.PlainTimeToLocaleString(const AArgs: TGoc end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalPlainTimeSharedSlot := RegisterRealmOwnedSlot('Temporal.PlainTime.shared'); end. diff --git a/source/units/Goccia.Values.TemporalPlainYearMonth.pas b/source/units/Goccia.Values.TemporalPlainYearMonth.pas index f7f37aa72..d94865790 100644 --- a/source/units/Goccia.Values.TemporalPlainYearMonth.pas +++ b/source/units/Goccia.Values.TemporalPlainYearMonth.pas @@ -67,6 +67,7 @@ implementation Goccia.Temporal.Calendar, Goccia.Temporal.Options, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IntlDateTimeFormat, @@ -85,6 +86,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalPlainYearMonthShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1282,6 +1288,7 @@ function TGocciaTemporalPlainYearMonthValue.YearMonthToLocaleString(const AArgs: end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalPlainYearMonthSharedSlot := RegisterRealmOwnedSlot('Temporal.PlainYearMonth.shared'); end. diff --git a/source/units/Goccia.Values.TemporalZonedDateTime.pas b/source/units/Goccia.Values.TemporalZonedDateTime.pas index d3c03909e..e3584be40 100644 --- a/source/units/Goccia.Values.TemporalZonedDateTime.pas +++ b/source/units/Goccia.Values.TemporalZonedDateTime.pas @@ -105,6 +105,7 @@ implementation Goccia.Temporal.Options, Goccia.Temporal.TimeZone, Goccia.Temporal.Utils, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.BigIntValue, Goccia.Values.ErrorHelper, @@ -125,6 +126,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTemporalZonedDateTimeShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -4196,6 +4202,7 @@ function TGocciaTemporalZonedDateTimeValue.ZonedDateTimeToLocaleString(const AAr end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTemporalZonedDateTimeSharedSlot := RegisterRealmOwnedSlot('Temporal.ZonedDateTime.shared'); end. diff --git a/source/units/Goccia.Values.TextDecoderValue.pas b/source/units/Goccia.Values.TextDecoderValue.pas index 4ebc8ac8d..671503214 100644 --- a/source/units/Goccia.Values.TextDecoderValue.pas +++ b/source/units/Goccia.Values.TextDecoderValue.pas @@ -56,6 +56,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ArrayBufferValue, Goccia.Values.ErrorHelper, Goccia.Values.NativeFunction, @@ -68,6 +69,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTextDecoderShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -518,6 +524,7 @@ function TGocciaTextDecoderValue.Decode(const AArgs: TGocciaArgumentsCollection; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTextDecoderSharedSlot := RegisterRealmOwnedSlot('TextDecoder.shared'); end. diff --git a/source/units/Goccia.Values.TextEncoderValue.pas b/source/units/Goccia.Values.TextEncoderValue.pas index 73e054027..c3645c389 100644 --- a/source/units/Goccia.Values.TextEncoderValue.pas +++ b/source/units/Goccia.Values.TextEncoderValue.pas @@ -45,6 +45,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.NativeFunction, Goccia.Values.ObjectPropertyDescriptor, @@ -56,6 +57,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetTextEncoderShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -313,6 +319,7 @@ function TGocciaTextEncoderValue.EncodeInto(const AArgs: TGocciaArgumentsCollect end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTextEncoderSharedSlot := RegisterRealmOwnedSlot('TextEncoder.shared'); end. diff --git a/source/units/Goccia.Values.TypedArrayValue.pas b/source/units/Goccia.Values.TypedArrayValue.pas index cb28e4416..f74f1dfdc 100644 --- a/source/units/Goccia.Values.TypedArrayValue.pas +++ b/source/units/Goccia.Values.TypedArrayValue.pas @@ -170,6 +170,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Scope, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ArrayValue, Goccia.Values.BigIntValue, @@ -191,6 +192,11 @@ implementation FPrototypeMembers: TArray; FUint8Prototype: TGocciaObjectValue; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + const TYPED_ARRAY_LITTLE_ENDIAN = True; @@ -3387,6 +3393,7 @@ function TGocciaTypedArrayStaticFrom.TypedArrayOf(const AArgs: TGocciaArgumentsC end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GTypedArraySharedSlot := RegisterRealmOwnedSlot('TypedArray.shared'); end. diff --git a/source/units/Goccia.Values.URLValue.pas b/source/units/Goccia.Values.URLValue.pas index 93c94bb2c..f9deee09e 100644 --- a/source/units/Goccia.Values.URLValue.pas +++ b/source/units/Goccia.Values.URLValue.pas @@ -135,6 +135,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.URL.Parser, Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, @@ -148,6 +149,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetURLShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -889,6 +895,7 @@ procedure TGocciaURLValue.MarkReferences; end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GURLSharedSlot := RegisterRealmOwnedSlot('URL.shared'); end. diff --git a/source/units/Goccia.Values.WeakMapValue.pas b/source/units/Goccia.Values.WeakMapValue.pas index f514f7e2b..7cff966be 100644 --- a/source/units/Goccia.Values.WeakMapValue.pas +++ b/source/units/Goccia.Values.WeakMapValue.pas @@ -60,6 +60,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IteratorSupport, @@ -75,6 +76,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetWeakMapShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -475,6 +481,7 @@ function TGocciaWeakMapValue.WeakMapSet( end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GWeakMapSharedSlot := RegisterRealmOwnedSlot('WeakMap.shared'); end. diff --git a/source/units/Goccia.Values.WeakRefValue.pas b/source/units/Goccia.Values.WeakRefValue.pas index 162c840f9..432d7661c 100644 --- a/source/units/Goccia.Values.WeakRefValue.pas +++ b/source/units/Goccia.Values.WeakRefValue.pas @@ -42,6 +42,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue, @@ -53,6 +54,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetWeakRefShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -175,6 +181,7 @@ function TGocciaWeakRefValue.WeakRefDeref( end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GWeakRefSharedSlot := RegisterRealmOwnedSlot('WeakRef.shared'); end. diff --git a/source/units/Goccia.Values.WeakSetValue.pas b/source/units/Goccia.Values.WeakSetValue.pas index 18da52084..82c6268b0 100644 --- a/source/units/Goccia.Values.WeakSetValue.pas +++ b/source/units/Goccia.Values.WeakSetValue.pas @@ -55,6 +55,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.IteratorSupport, @@ -70,6 +71,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetWeakSetShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -314,6 +320,7 @@ function TGocciaWeakSetValue.WeakSetHas( end; initialization + RegisterThreadvarCleanup(@ClearThreadvarMembers); GWeakSetSharedSlot := RegisterRealmOwnedSlot('WeakSet.shared'); end. From d162779a935752f428a3d8b2aaec2db14be18ee6 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sat, 27 Jun 2026 20:35:03 +0100 Subject: [PATCH 2/5] docs(adr): link 0077 deferred work to follow-up issues #892 #893 Co-Authored-By: Claude Opus 4.8 --- docs/adr/0077-thread-local-cleanup-registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0077-thread-local-cleanup-registry.md b/docs/adr/0077-thread-local-cleanup-registry.md index f8f57eb5f..87613e903 100644 --- a/docs/adr/0077-thread-local-cleanup-registry.md +++ b/docs/adr/0077-thread-local-cleanup-registry.md @@ -11,4 +11,4 @@ Added `Goccia.ThreadCleanupRegistry`: a dependency-free unit exposing `RegisterT Two narrower alternatives were rejected. **Ad-hoc per-unit clears wired directly into `ShutdownThreadRuntime`** (mirroring the existing `DisposableStack` / `Semver` pattern) does not scale to ~64 units: it would couple `Goccia.Threading` to every builtin and value unit through its `uses` clause, require a ~64-line central call list kept in sync forever, and need ~64 separate `finalization` sections that the registry collapses into one. **Centralizing the member-definition arrays in a single per-thread store** was the largest change — it rewrites the member-registration path in all ~64 units, alters *how* members are built rather than only their lifetime (against the issue's lifetime-only scope), carries the most regression risk, and does not fit the two string memos. -The two memos already finalize on the main thread, so they only needed the worker path: `ShutdownThreadRuntime` calls their exported `ClearRegExpInputMemo` / `ClearAsciiMemo` explicitly, the same shape as the five caches already routed there (ImportMeta, Atomics, DisposableStack, Semver, TimeZone). This change also closes two pre-existing main-thread finalization gaps surfaced while auditing those caches: `Goccia.ImportMeta` had no `finalization`, and `Goccia.Temporal.TimeZone`'s `finalization` did not call `ClearTimeZoneCache`. Migrating the five explicit caches (and the two memos) into the registry for one uniform mechanism, and auditing object-reference threadvars (e.g. the symbol registry), are deferred to follow-ups. A `Goccia.ThreadCleanupLeak.Test` Pascal gate spawns repeated worker cycles and asserts the live heap does not grow per cycle, locking the behaviour in. [core-patterns.md](../core-patterns.md). [garbage-collector.md](../garbage-collector.md). +The two memos already finalize on the main thread, so they only needed the worker path: `ShutdownThreadRuntime` calls their exported `ClearRegExpInputMemo` / `ClearAsciiMemo` explicitly, the same shape as the five caches already routed there (ImportMeta, Atomics, DisposableStack, Semver, TimeZone). This change also closes two pre-existing main-thread finalization gaps surfaced while auditing those caches: `Goccia.ImportMeta` had no `finalization`, and `Goccia.Temporal.TimeZone`'s `finalization` did not call `ClearTimeZoneCache`. Migrating the five explicit caches (and the two memos) into the registry for one uniform mechanism (#893), and auditing object-reference threadvars (e.g. the symbol registry) (#892), are deferred to follow-ups. A `Goccia.ThreadCleanupLeak.Test` Pascal gate spawns repeated worker cycles and asserts the live heap does not grow per cycle, locking the behaviour in. [core-patterns.md](../core-patterns.md). [garbage-collector.md](../garbage-collector.md). From f49b2dbb4661d0c9765be2cb1451c5fd4eae2a35 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sun, 28 Jun 2026 01:33:41 +0100 Subject: [PATCH 3/5] test(threading): register cleanup sentinels at startup Address PR review: RegisterThreadvarCleanup is a write-once-at-init API, but the registry tests registered their sentinels inside the test bodies. Move the sentinel registrations (and the nil-guard) into the test program's startup block, before any worker thread is spawned, and keep the tests to resetting counters, draining, and asserting. Co-Authored-By: Claude Opus 4.8 --- source/units/Goccia.Threading.Test.pas | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/source/units/Goccia.Threading.Test.pas b/source/units/Goccia.Threading.Test.pas index 8152e18e3..9316ea09b 100644 --- a/source/units/Goccia.Threading.Test.pas +++ b/source/units/Goccia.Threading.Test.pas @@ -416,23 +416,18 @@ procedure TTestThreading.TestPoolSingleWorker; procedure TTestThreading.TestThreadCleanupRegistryRunsRegistered; begin - // A nil callback is ignored (no crash, nothing registered). - RegisterThreadvarCleanup(nil); - - // A registered callback runs when the registry is drained. - GSentinelCount := 0; - RegisterThreadvarCleanup(@SentinelCleanup); - RunThreadvarCleanups; - Expect(GSentinelCount).ToBe(1); + // SentinelCleanup and SentinelCleanupSecond are registered once at startup + // (see the program body), honouring RegisterThreadvarCleanup's + // write-once-at-init contract. A nil callback was also registered there and + // must be ignored — otherwise the drain below would call a nil pointer. - // Draining again is safe and re-runs the callback (repeatable on any thread). + // Draining runs every registered callback (both sentinels, nil skipped). GSentinelCount := 0; RunThreadvarCleanups; - Expect(GSentinelCount).ToBe(1); + Expect(GSentinelCount).ToBe(2); - // Every registered callback runs, not just the first. + // Draining again is safe and re-runs them (repeatable on any thread). GSentinelCount := 0; - RegisterThreadvarCleanup(@SentinelCleanupSecond); RunThreadvarCleanups; Expect(GSentinelCount).ToBe(2); end; @@ -445,10 +440,10 @@ procedure TTestThreading.TestShutdownThreadRuntimeDrainsRegistryPerWorker; Files: TStringList; Host: TTestWorkerHost; begin + // SentinelWorkerCleanup is registered once at startup (see the program body). // Each worker thread calls ShutdownThreadRuntime as it exits, which drains the // registry on that thread. With WORKER_COUNT workers, the registered callback // must fire exactly WORKER_COUNT times — proving the per-worker-exit wiring. - RegisterThreadvarCleanup(@SentinelWorkerCleanup); GSentinelWorkerCount := 0; ResetWorkerState; @@ -480,6 +475,13 @@ procedure TTestThreading.TestShutdownThreadRuntimeDrainsRegistryPerWorker; TGarbageCollector.Initialize; PinPrimitiveSingletons; InitCriticalSection(GWorkerLock); + // Register the cleanup sentinels once here, before any worker thread is + // spawned, honouring RegisterThreadvarCleanup's write-once-at-init contract. + // The nil registration must be ignored (a nil callback would crash the drain). + RegisterThreadvarCleanup(nil); + RegisterThreadvarCleanup(@SentinelCleanup); + RegisterThreadvarCleanup(@SentinelCleanupSecond); + RegisterThreadvarCleanup(@SentinelWorkerCleanup); try TestRunnerProgram.AddSuite(TTestThreading.Create('Threading')); TestRunnerProgram.Run; From 09c8caf0f6fbfd026a353dd5cade8a612aae5a66 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sun, 28 Jun 2026 01:33:42 +0100 Subject: [PATCH 4/5] docs(adr): correct 0077 leak-test description Address PR review: the ADR described the leak gate as spawning "repeated worker cycles" with per-cycle non-growth, which the implemented tests do not do. Describe the two actual gates instead: Goccia.ThreadCleanupLeak.Test (one-shot heap-drop on drain) and Goccia.Threading.Test (per-worker drain wiring). Co-Authored-By: Claude Opus 4.8 --- docs/adr/0077-thread-local-cleanup-registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0077-thread-local-cleanup-registry.md b/docs/adr/0077-thread-local-cleanup-registry.md index 87613e903..b564005ca 100644 --- a/docs/adr/0077-thread-local-cleanup-registry.md +++ b/docs/adr/0077-thread-local-cleanup-registry.md @@ -11,4 +11,4 @@ Added `Goccia.ThreadCleanupRegistry`: a dependency-free unit exposing `RegisterT Two narrower alternatives were rejected. **Ad-hoc per-unit clears wired directly into `ShutdownThreadRuntime`** (mirroring the existing `DisposableStack` / `Semver` pattern) does not scale to ~64 units: it would couple `Goccia.Threading` to every builtin and value unit through its `uses` clause, require a ~64-line central call list kept in sync forever, and need ~64 separate `finalization` sections that the registry collapses into one. **Centralizing the member-definition arrays in a single per-thread store** was the largest change — it rewrites the member-registration path in all ~64 units, alters *how* members are built rather than only their lifetime (against the issue's lifetime-only scope), carries the most regression risk, and does not fit the two string memos. -The two memos already finalize on the main thread, so they only needed the worker path: `ShutdownThreadRuntime` calls their exported `ClearRegExpInputMemo` / `ClearAsciiMemo` explicitly, the same shape as the five caches already routed there (ImportMeta, Atomics, DisposableStack, Semver, TimeZone). This change also closes two pre-existing main-thread finalization gaps surfaced while auditing those caches: `Goccia.ImportMeta` had no `finalization`, and `Goccia.Temporal.TimeZone`'s `finalization` did not call `ClearTimeZoneCache`. Migrating the five explicit caches (and the two memos) into the registry for one uniform mechanism (#893), and auditing object-reference threadvars (e.g. the symbol registry) (#892), are deferred to follow-ups. A `Goccia.ThreadCleanupLeak.Test` Pascal gate spawns repeated worker cycles and asserts the live heap does not grow per cycle, locking the behaviour in. [core-patterns.md](../core-patterns.md). [garbage-collector.md](../garbage-collector.md). +The two memos already finalize on the main thread, so they only needed the worker path: `ShutdownThreadRuntime` calls their exported `ClearRegExpInputMemo` / `ClearAsciiMemo` explicitly, the same shape as the five caches already routed there (ImportMeta, Atomics, DisposableStack, Semver, TimeZone). This change also closes two pre-existing main-thread finalization gaps surfaced while auditing those caches: `Goccia.ImportMeta` had no `finalization`, and `Goccia.Temporal.TimeZone`'s `finalization` did not call `ClearTimeZoneCache`. Migrating the five explicit caches (and the two memos) into the registry for one uniform mechanism (#893), and auditing object-reference threadvars (e.g. the symbol registry) (#892), are deferred to follow-ups. Two Pascal gates lock the behaviour in: `Goccia.ThreadCleanupLeak.Test` populates a thread's member-definition threadvars (by building a throwaway engine) and asserts the live heap drops by a meaningful amount when the registry is drained, and `Goccia.Threading.Test` separately asserts the registry runs every registered callback and that `ShutdownThreadRuntime` drains it exactly once per worker-thread exit. [core-patterns.md](../core-patterns.md). [garbage-collector.md](../garbage-collector.md). From 696fab2ec6195a0bbb6a05bfe55a21eb904aaa9b Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sun, 28 Jun 2026 07:45:50 +0100 Subject: [PATCH 5/5] docs(adr): renumber thread-cleanup registry ADR to 0078 0077 was taken by #890 (SameValueZero-keyed ordered collections), which merged to main first. Renumber the thread-local cleanup registry ADR to 0078. Co-Authored-By: Claude Opus 4.8 --- ...leanup-registry.md => 0078-thread-local-cleanup-registry.md} | 0 docs/adr/README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/adr/{0077-thread-local-cleanup-registry.md => 0078-thread-local-cleanup-registry.md} (100%) diff --git a/docs/adr/0077-thread-local-cleanup-registry.md b/docs/adr/0078-thread-local-cleanup-registry.md similarity index 100% rename from docs/adr/0077-thread-local-cleanup-registry.md rename to docs/adr/0078-thread-local-cleanup-registry.md diff --git a/docs/adr/README.md b/docs/adr/README.md index 0b6f901d7..6e267bf63 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -86,4 +86,4 @@ Durable architecture and implementation decisions for GocciaScript. New ADRs use - [0074 — Deferred bytecode call-stack frames](0074-deferred-bytecode-call-stack-frames.md) - [0075 — ShadowRealm full test262 conformance](0075-shadowrealm-conformance.md) - [0076 — Same-runner benchmark comparison](0076-same-runner-benchmark-comparison.md) -- [0077 — Thread-local cleanup registry for managed threadvars](0077-thread-local-cleanup-registry.md) +- [0078 — Thread-local cleanup registry for managed threadvars](0078-thread-local-cleanup-registry.md)