diff --git a/docs/adr/0078-thread-local-cleanup-registry.md b/docs/adr/0078-thread-local-cleanup-registry.md new file mode 100644 index 00000000..b564005c --- /dev/null +++ b/docs/adr/0078-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 (#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). diff --git a/docs/adr/README.md b/docs/adr/README.md index f424b7be..87b81e02 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -87,3 +87,4 @@ Durable architecture and implementation decisions for GocciaScript. New ADRs use - [0075 — ShadowRealm full test262 conformance](0075-shadowrealm-conformance.md) - [0076 — Same-runner benchmark comparison](0076-same-runner-benchmark-comparison.md) - [0077 — SameValueZero-keyed ordered store for Map and Set](0077-samevaluezero-ordered-collections.md) +- [0078 — Thread-local cleanup registry for managed threadvars](0078-thread-local-cleanup-registry.md) diff --git a/source/shared/TextSemantics.pas b/source/shared/TextSemantics.pas index 3ee8c62f..c677a570 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 5de2931d..5e2568a7 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 e403d933..9dcf86d7 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 3aa67274..96be6028 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 88571331..1a208653 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 0302e60c..cb21c5ac 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 0727ca2e..fd86499b 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 e063e7cd..e08f2395 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 cb99c054..602c9b6b 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 8b9cd2f1..6d8a8363 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 f8ea267d..193f2c70 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 b926de7f..d11e0007 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 9a4765a7..5a048453 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 185c3ca6..7f2aaf9c 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 d2a66855..540bff2b 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 38fa8ea3..c2e49454 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 857a3b28..61b7a052 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 5464c261..79df2a88 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 834c01e7..5ef55dc6 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 394ede16..db210b73 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 8f7f9e32..82936825 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 24c627d3..fd76d875 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 f707d5c6..255fa19a 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 05d8d311..3d0bfc9b 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 ff0fc670..8c685636 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 00000000..ceaf1bec --- /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 00000000..39aa2cd0 --- /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 23442604..9316ea09 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,59 @@ procedure TTestThreading.TestPoolSingleWorker; end; end; +procedure TTestThreading.TestThreadCleanupRegistryRunsRegistered; +begin + // 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 runs every registered callback (both sentinels, nil skipped). + GSentinelCount := 0; + RunThreadvarCleanups; + Expect(GSentinelCount).ToBe(2); + + // Draining again is safe and re-runs them (repeatable on any thread). + GSentinelCount := 0; + RunThreadvarCleanups; + Expect(GSentinelCount).ToBe(2); +end; + +procedure TTestThreading.TestShutdownThreadRuntimeDrainsRegistryPerWorker; +const + WORKER_COUNT = 3; +var + Pool: TGocciaThreadPool; + 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. + 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 @@ -393,6 +475,13 @@ procedure TTestThreading.TestPoolSingleWorker; 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; diff --git a/source/units/Goccia.Threading.pas b/source/units/Goccia.Threading.pas index 525fa6fa..fe3fcb79 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 6b2ceda0..1e1e37a9 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 32578841..7ca4f80e 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 3dafc550..4ea55158 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 f7882c34..21df7cf9 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 a11ff639..ba4fe13d 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 e376f61b..bab38a10 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 238a227f..66508d8e 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 f295b13d..91bcc9c9 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 744acd6f..c2a50298 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 899db9e1..6079657c 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 ece34de3..34e88d3c 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 209fb6eb..bdb664e4 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 de041022..3fe31efe 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 10f877a3..80b3e7e3 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 644f8e6c..8c0c3b2a 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 df6900ac..9c8187fc 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 3b95fcf8..e852fc14 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 dea5609c..1cef33ff 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 cf17d0e5..2451d33b 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 b76bbb39..74840860 100644 --- a/source/units/Goccia.Values.MapValue.pas +++ b/source/units/Goccia.Values.MapValue.pas @@ -71,6 +71,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -87,6 +88,11 @@ implementation threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetMapShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -598,5 +604,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 2feb3214..f48d27ff 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 85240abd..7b71af81 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 26def7d0..18eb1786 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 81e80289..eab9f32b 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 21f47886..9e75df47 100644 --- a/source/units/Goccia.Values.SetValue.pas +++ b/source/units/Goccia.Values.SetValue.pas @@ -80,6 +80,7 @@ implementation Goccia.Error.Suggestions, Goccia.GarbageCollector, Goccia.Realm, + Goccia.ThreadCleanupRegistry, Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, @@ -104,6 +105,11 @@ TGocciaSetRecord = record threadvar FPrototypeMembers: TArray; +procedure ClearThreadvarMembers; +begin + SetLength(FPrototypeMembers, 0); +end; + function GetSetShared: TGocciaSharedPrototype; inline; begin if Assigned(CurrentRealm) then @@ -1061,6 +1067,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 e9ff3460..e0451f8d 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 92c95737..dcc65855 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 829a32d3..a221b9a0 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 69caff78..47d73358 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 33531fee..4e16c45a 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 7d2e1d2b..4b4a9c8b 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 6d007172..cba85b5a 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 4a941cc6..4f187dee 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 0e695ec5..6b1ef8c7 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 f7f37aa7..d9486579 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 d3c03909..e3584be4 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 4ebc8ac8..67150321 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 73e05402..c3645c38 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 cb28e441..f74f1dfd 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 93c94bb2..f9deee09 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 f514f7e2..7cff966b 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 162c840f..432d7661 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 18da5208..82c6268b 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.