You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An imported base class's public static field is not the same initialized cell when it is read and post-incremented from a function in the defining module. Component._id++ reads as undefined, assigns NaN to both derived constructors, and leaves the externally visible Component._id at 0.
This is a correctness bug with a catastrophic performance consequence in perform-ecs: component bitmasks collide, destroyed entities remain in a component view, and the nominally linear destroy benchmark becomes a growing-array workload.
Reproduced on current main (c2da03439e6e848cfed73163c131acb601ae96dc, Perry 0.5.1519) and on PR #8645 (0ab2fb3909d8ad503b526acb23b2695f2d825bcb). PR #8645 fixes the separate Array-subclass constructor failure but does not fix this case.
In ddmills/js-ecs-benchmarks' perform-ecs / Destroy case:
State after destroy
Node
Perry
component IDs
[0, 1]
[NaN, NaN]
retained view entries after 100 destroys
0
200
retained view entries after 100,000 destroys
0
200,000
At full scale on an M1 Mac mini, Perry took 169.70 s and peaked at 256,832 KiB RSS. Block time increased from 1.95 s for the first 10,000 destroys to 31.07 s for the last 50,000, consistent with the ever-growing retained array. This timing must not be treated as a valid performance comparison until the semantic bug is fixed.
Expected behavior
A public static field has one canonical initialized storage cell across its defining module and compiled consumers.
Component._id++ returns the old numeric value and stores the incremented numeric value.
The repro prints the same values as Node.
perform-ecs assigns distinct component IDs and retains zero view entries after destruction.
Acceptance criteria
Add a two-module native regression based on the repro above and run it in both prebuilt-runtime and auto-optimize modes.
Cover post-increment from the defining module, post-increment from an importing module, and reads from both modules.
Include a forced-GC variant so moving/registration does not split or lose the static cell.
Add an ecosystem regression using the perform-ecs component-registration shape: IDs [0, 1], distinct masks, and zero retained entities after add/destroy.
The symptom points to static-field metadata/storage identity across module lowering: the defining-module function appears not to resolve the same initialized static cell that main.js observes. Relevant areas include static_field_meta, static initialization lowering, shared mutable capture, and class registration/parent-static storage.
Summary
An imported base class's public static field is not the same initialized cell when it is read and post-incremented from a function in the defining module.
Component._id++reads asundefined, assignsNaNto both derived constructors, and leaves the externally visibleComponent._idat0.This is a correctness bug with a catastrophic performance consequence in
perform-ecs: component bitmasks collide, destroyed entities remain in a component view, and the nominally linear destroy benchmark becomes a growing-array workload.Reproduced on current
main(c2da03439e6e848cfed73163c131acb601ae96dc, Perry 0.5.1519) and on PR #8645 (0ab2fb3909d8ad503b526acb23b2695f2d825bcb). PR #8645 fixes the separate Array-subclass constructor failure but does not fix this case.Self-contained repro
base.js:main.js:Build and run from a Perry checkout:
Observed:
The
perform-ecsintegration below also reproduces through the default auto-optimize path.Public-package impact
perform-ecs/src/Component.tsuses the same pattern:In
ddmills/js-ecs-benchmarks'perform-ecs / Destroycase:[0, 1][NaN, NaN]At full scale on an M1 Mac mini, Perry took 169.70 s and peaked at 256,832 KiB RSS. Block time increased from 1.95 s for the first 10,000 destroys to 31.07 s for the last 50,000, consistent with the ever-growing retained array. This timing must not be treated as a valid performance comparison until the semantic bug is fixed.
Expected behavior
Component._id++returns the old numeric value and stores the incremented numeric value.perform-ecsassigns distinct component IDs and retains zero view entries after destruction.Acceptance criteria
perform-ecscomponent-registration shape: IDs[0, 1], distinct masks, and zero retained entities after add/destroy.Likely area
The symptom points to static-field metadata/storage identity across module lowering: the defining-module function appears not to resolve the same initialized static cell that
main.jsobserves. Relevant areas includestatic_field_meta, static initialization lowering, shared mutable capture, and class registration/parent-static storage.