@woosh/meep-engine 2.165.7 → 2.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/package.json +1 -1
  3. package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
  4. package/src/core/binary/BinaryBuffer.js +1 -1
  5. package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
  6. package/src/core/binary/EncodingBinaryBuffer.js +3 -1
  7. package/src/core/cache/Cache.d.ts.map +1 -1
  8. package/src/core/cache/Cache.js +0 -4
  9. package/src/core/cache/CacheElement.d.ts.map +1 -1
  10. package/src/core/cache/CacheElement.js +4 -0
  11. package/src/core/cache/FrequencySketch.d.ts +0 -21
  12. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  13. package/src/core/cache/FrequencySketch.js +66 -65
  14. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
  15. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
  16. package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
  17. package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
  18. package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
  19. package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
  20. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
  21. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
  22. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
  23. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
  24. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
  25. package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
  26. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
  27. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
  28. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
  29. package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
  30. package/src/core/collection/RingBuffer.d.ts.map +1 -1
  31. package/src/core/collection/RingBuffer.js +15 -3
  32. package/src/core/collection/map/HashMap.d.ts +1 -1
  33. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  34. package/src/core/collection/map/HashMap.js +5 -17
  35. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
  36. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
  37. package/src/core/geom/ConicRay.d.ts.map +1 -1
  38. package/src/core/geom/ConicRay.js +9 -2
  39. package/src/core/geom/Quaternion.d.ts.map +1 -1
  40. package/src/core/geom/Quaternion.js +7 -3
  41. package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
  42. package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
  43. package/src/core/math/build_gaussian_kernel_1d.js +4 -2
  44. package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
  45. package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
  46. package/src/core/math/build_gaussian_kernel_2d.js +4 -2
  47. package/src/core/math/gaussian.d.ts +2 -2
  48. package/src/core/math/gaussian.js +2 -2
  49. package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
  50. package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
  51. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
  52. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
  53. package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
  54. package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
  55. package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
  56. package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
  57. package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
  58. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
  59. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
  60. package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
  61. package/src/core/math/statistics/computeStatisticalMean.js +1 -1
  62. package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
  63. package/src/core/math/statistics/gaussian_amplitude.js +3 -1
  64. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  65. package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
  66. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
  67. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
  68. package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
  69. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
  70. package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
  71. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  72. package/src/engine/ecs/EntityComponentDataset.js +5 -21
  73. package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
  74. package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
  75. package/src/engine/ecs/fow/FogOfWar.js +202 -54
  76. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
  77. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
  78. package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
  79. package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
  80. package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
  81. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
  82. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
  83. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
  84. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
  85. package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
  86. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
  87. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
  88. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
  89. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
  90. package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
  91. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  92. package/src/engine/graphics/GraphicsEngine.js +14 -3
  93. package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
  94. package/src/engine/graphics/composit/CompositLayer.js +258 -255
  95. package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
  96. package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
  97. package/src/engine/graphics/composit/LayerCompositer.js +19 -10
  98. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
  99. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
  100. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
  101. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
  102. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
  103. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
  104. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
  105. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
  106. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
  107. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
  108. package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
  109. package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
  110. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
  111. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
  112. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
  113. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
  114. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
  115. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
  116. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
  117. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
  118. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
  119. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
  120. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
  121. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
  122. package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
  123. package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
  124. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
  125. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  126. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
  127. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
  128. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
  129. package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
  130. package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
  131. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
  132. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
  133. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
  134. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  135. package/src/engine/graphics/trail/tube/TubeX.js +557 -555
  136. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
  137. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
  138. package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
  139. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  140. package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
  141. package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
  142. package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
  143. package/src/engine/navigation/ecs/components/Path.js +10 -4
  144. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
  145. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  146. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
@@ -0,0 +1,243 @@
1
+ # W-TinyLFU benchmarks
2
+
3
+ Benchmarks comparing `CacheWTinylfu` against the LRU `Cache` sibling. They split into **two axes**
4
+ that answer different questions:
5
+
6
+ | Axis | Question | Answered by |
7
+ |------|----------|-------------|
8
+ | **Effectiveness** (hit rate) | What fraction of accesses hit? Determined by the **policy**. | `hitRate.spec.js` |
9
+ | **Performance** (throughput / latency / space) | What does each operation **cost**? Determined by the **implementation**. | everything else |
10
+
11
+ You need both to judge the trade: the policy benchmark tells you what W-TinyLFU *saves you in misses*;
12
+ the performance benchmarks tell you what it *costs per op and in RAM*. The exchange rate between them is
13
+ the cost of a miss in your system (a disk/network miss dwarfs a few hundred ns; a cheap recompute does not).
14
+
15
+ > ⚠️ **About the numbers below.** They're from one dev machine (Node v20.14.0, Windows 11) and microbenchmarks
16
+ > are noisy — absolute nanoseconds wander run-to-run by tens of percent. **Trust the ratios and the shape, not
17
+ > the exact ns.** The ratios are measured *interleaved* (LRU and W-TinyLFU timed back-to-back each round, then
18
+ > the per-round ratios medianed), so they stay stable even as the absolutes drift. Re-run on your own hardware
19
+ > for absolutes.
20
+
21
+ ---
22
+
23
+ ## Running
24
+
25
+ Each benchmark is a **self-executing spec**: a single `<name>.spec.js` that is *both* a runnable Node
26
+ benchmark and a Jest spec. The measurement **only ever runs under plain `node`** — never in-process under
27
+ Jest, because Jest's babel transform badly distorts the timings (see *Why not measure under Jest* below).
28
+ The files are stripped from the published npm package by the `*.spec.js` rule.
29
+
30
+ **To get numbers — run it directly with `node`, from the repo root:**
31
+
32
+ ```bash
33
+ node src/core/cache/wtinylfu/benchmark/<name>.spec.js
34
+ ```
35
+
36
+ where `<name>` is one of: `hitRate`, `fillDrain`, `readAll`, `continuousPut`, `mixed`, `latency`, `scaling`,
37
+ `memory`.
38
+
39
+ **Under Jest** each file is a `describe.skip`, so `npx jest` collects but skips them (zero CI cost). Un-skip
40
+ one (flip `describe.skip` → `describe`) and Jest will **spawn a clean `node` child on the file** and check the
41
+ output is sane — you get the same pristine numbers, never the transformed ones.
42
+
43
+ Two flags (for the `node` invocation):
44
+
45
+ - **`--expose-gc`** (a Node flag, goes *before* the script path): lets a benchmark force GC between trials for
46
+ cleaner numbers. Optional for most; **required** for `memory` (it refuses to run without it). *(When run via
47
+ Jest, the child is always spawned with `--expose-gc`.)*
48
+ ```bash
49
+ node --expose-gc src/core/cache/wtinylfu/benchmark/fillDrain.spec.js
50
+ ```
51
+ - **`HASH=int`** (an env var): the **isolated-hash toggle** — use integer keys + an identity hash instead of
52
+ string keys + `computeStringHash`, to strip string-hashing cost and see pure-policy overhead. Works on
53
+ **every** benchmark.
54
+ ```bash
55
+ HASH=int node --expose-gc src/core/cache/wtinylfu/benchmark/readAll.spec.js
56
+ ```
57
+
58
+ All workloads are **seeded**, so runs are reproducible. Both caches are always built at **matched capacity**
59
+ (N weight-1 entries) so the comparison is fair.
60
+
61
+ ### Layout & file structure
62
+
63
+ Each benchmark lives in its own self-contained file in this `benchmark/` folder as `<name>.spec.js`.
64
+ There is **no shared harness** — each file inlines the scaffolding it needs, so it stands alone (copy it,
65
+ run it, read it without chasing imports). Every file follows the same skeleton:
66
+
67
+ ```
68
+ 1. HEADER what it measures · how to run · how to read it
69
+ 2. CONFIG the knobs: CAPACITY / WARMUP / TRIALS / SEED
70
+ 3. SCAFFOLDING inlined: clock, rng, matched-capacity factories, measureInterleaved, any workload
71
+ (top-level — pure definitions, safe when Jest imports the file)
72
+ if (under Jest) → spawn a clean `node` on this file + assert the output is sane
73
+ else → the actual benchmark:
74
+ 4. WORKLOAD the setup + the timed body (the "arrange")
75
+ 5. MEASURE measureInterleaved([LRU, W-TinyLFU], {...}) (the "act")
76
+ 6. REPORT formatting + console.log (the "assert" analog — reports, no pass/fail)
77
+ ```
78
+
79
+ The `if (typeof describe === "function")` split is what keeps the measurement out of Jest's transform: under
80
+ Jest only the spawn-branch runs; under `node` only the `else` (measurement) branch runs.
81
+
82
+ A benchmark **reports** numbers; it does not **assert** pass/fail. That's why the measurement always runs
83
+ under plain `node` — the Jest side only launches it and checks the output isn't empty.
84
+
85
+ ### Why not measure under Jest
86
+
87
+ Jest runs every file through **babel-jest** before V8 sees it (transpiled to CommonJS, wrapped in Jest's
88
+ module registry). That transformed code JITs differently from the native ES module `node` runs — and the
89
+ overhead is **not uniform**: a cheap op (LRU) takes a bigger proportional hit than a heavier one (W-TinyLFU).
90
+ Measured in-process under Jest, absolutes inflated ~2–12× **and several ratios inverted** — `fill` read
91
+ 1.76x → 0.91x, `mixed` 1.48x → 0.90x, `latency p50` 3.0x → 0.86x — i.e. Jest would tell you W-TinyLFU is
92
+ *faster* than LRU on inserts, which is false. It was also ~4× slower wall-clock. So the benchmark body never
93
+ runs under Jest; the spec spawns a clean `node` child and only checks the output is well-formed.
94
+
95
+ ---
96
+
97
+ ## Effectiveness — `hitRate.spec.js`
98
+
99
+ Replays seeded access traces as a read-through cache and reports **hit ratio** (capacity N = 500). This is
100
+ where W-TinyLFU earns its keep.
101
+
102
+ ```
103
+ LRU W-TinyLFU Δ
104
+ uniform random (no skew) 10.0% 10.0% 0.0 ← control: no harm
105
+ zipfian skew 0.7 / 0.9 / 1.1 22.7/45.4/70.7 ~equal ±1 ← realistic skew: roughly even
106
+ scan (hot set + one-shot scans) 31.6% 47.0% +15.4 ← scan resistance: the big win
107
+ cyclic loop (size > capacity) LRU pathological case win ← incumbent-favoring ties
108
+ ```
109
+
110
+ **Takeaway:** roughly even with LRU on plain skew, *no worse* on uniform, and a large win on **scan-heavy**
111
+ and **cyclic** workloads (where LRU is catastrophic). That scan resistance is the reason to pay the costs below.
112
+
113
+ ---
114
+
115
+ ## Performance
116
+
117
+ Realistic (string-key) mode, `--expose-gc`, interleaved, median of 100 trials. **Capacities:** the read / fill /
118
+ space and `continuousPut` benchmarks use **N = 65,536**; `mixed` uses a quarter of that (**N = 16,384**,
119
+ keyspace 10×N) so its keyspace can dwarf the cache for realistic misses. All are powers of two — safe under
120
+ churn because the shared HashMap grows headroom at its load factor.
121
+
122
+ ### `fillDrain` — insert + teardown
123
+
124
+ ```
125
+ fill (put) | LRU ~197 ns/op | W-TinyLFU ~378 ns/op | ~2.1x
126
+ drain (clear) | LRU ~21 ms/call | W-TinyLFU ~11 ms/call | ~0.52x
127
+ drain (remove) | LRU ~259 ns/op | W-TinyLFU ~317 ns/op | ~1.2x
128
+ ```
129
+ `fill` is the policy's insert cost (sketch + region routing). The `drain` rows reflect each class's *teardown
130
+ implementation*, not the policy — LRU's `clear()` removes element-by-element while W-TinyLFU bulk-resets, so the
131
+ drain difference is an implementation artifact.
132
+
133
+ ### `readAll` — read path (cold vs warm)
134
+
135
+ ```
136
+ read cold | LRU ~370 ns/op | W-TinyLFU ~710 ns/op | ~1.9x
137
+ read warm | LRU ~290 ns/op | W-TinyLFU ~490 ns/op | ~1.7x
138
+ ```
139
+ Every get is a hit. `cold` = first read-pass (W-TinyLFU's promotion activity); `warm` = settled steady state.
140
+ The premium is modest and one-time, not a recurring spike.
141
+
142
+ ### `continuousPut` — sustained load (churn vs update)
143
+
144
+ ```
145
+ put churn | LRU ~660 ns/op | W-TinyLFU ~656 ns/op | ~1.0x (new keys → evict + admission duel)
146
+ put update | LRU ~220 ns/op | W-TinyLFU ~447 ns/op | ~2.0x (re-put existing keys → in-place)
147
+ ```
148
+ **The key reframe.** W-TinyLFU's extra policy work is roughly **additive (~150–200 ns/op)**, not a multiplier —
149
+ on the cheap `update` baseline it shows as ~2.1×. On `churn` it doesn't show at all: **element pooling** makes
150
+ W-TinyLFU's inserts allocation-free, while LRU still allocates an element per insert, so that saving *offsets*
151
+ the policy overhead and W-TinyLFU draws level with LRU (~1.0×).
152
+
153
+ ### `mixed` — realistic blended throughput
154
+
155
+ ```
156
+ mixed get/put | LRU ~157 ns/op | W-TinyLFU ~224 ns/op | ~1.4x
157
+ ```
158
+ Zipfian read-through (get; on miss, put) over a keyspace 10× the cache — the most representative single number.
159
+
160
+ ### `latency` — the tail (percentiles)
161
+
162
+ ```
163
+ p50 p99 p999 max
164
+ LRU 200 ~700 ~2400 multi-ms (GC)
165
+ W-TinyLFU 300 ~1300 ~7400 multi-ms (GC)
166
+ ```
167
+ Per-op timing during a fill. W-TinyLFU's whole distribution shifts up ~2× — its heavier insert, paid broadly
168
+ rather than in a rare spike. The **max is a multi-ms GC pause on both**, not a policy event — read the policy
169
+ tail in p99/p999, not max.
170
+
171
+ ### `memory` — bytes per entry (space) · requires `--expose-gc`
172
+
173
+ ```
174
+ fresh fill after churn (steady state)
175
+ LRU | ~144 (heap ~136 + ext ~8) | ~224 (heap ~208 + ext ~16) bytes/entry
176
+ W-TinyLFU | ~164 (heap ~152 + ext ~12) | ~245 (heap ~225 + ext ~20) bytes/entry
177
+ → fresh, W-TinyLFU vs LRU: +~20 B/entry (+~16 heap = per-element `region` + `keyHash`; +~4 sketch)
178
+ → churn cost (both caches): +~80 B/entry (the shared HashMap doubling its slab at the load factor)
179
+ ```
180
+ ~14% more RAM per entry for the **policy** (fresh) — the **W-vs-LRU difference of ~4 B ≈ the sketch**. The
181
+ **churn cost** is the same +~80 for both because it's the *shared HashMap*, not the policy: once the cache is
182
+ full and evicting, the entries slab (and its bins) hits the 0.75 load factor and **doubles** to keep amortised
183
+ O(1) inserts, so a churning cache carries ~2× the entry headroom of a fresh fill.
184
+
185
+ ### `scaling` — O(1) check + locality across N
186
+
187
+ ```
188
+ N LRU W-TinyLFU ratio
189
+ 2,048 62.3 175.6 2.88x
190
+ 16,384 102.8 230.0 2.25x
191
+ 131,072 379.2 556.8 1.47x
192
+ 1,048,576 667.7 911.8 1.37x (warm random read, ns/op)
193
+ ```
194
+ Per-op cost rises only ~11× from 2k→1M (not ~500×) → **O(1) confirmed**; the rise is **CPU-cache locality**.
195
+ The ratio **compresses as N grows** — both caches increasingly bottlenecked on RAM latency at large N.
196
+
197
+ ### `HASH=int` — pure-policy overhead (cross-cutting)
198
+
199
+ Run any benchmark with `HASH=int` to strip string hashing. Example (read warm):
200
+
201
+ ```
202
+ realistic isolated (HASH=int)
203
+ LRU ~290 ns/op ~151 ns/op
204
+ W-TinyLFU ~490 ns/op ~338 ns/op
205
+ ratio ~1.7x ~2.3x
206
+ ```
207
+ Reveals that **string hashing is a large *shared* cost** (~140 ns of LRU's warm read was just `Map` string
208
+ hashing, paid by both caches). Removing it shrinks the baseline, so the *same* fixed policy overhead becomes a
209
+ bigger multiple.
210
+
211
+ ---
212
+
213
+ ## Headline findings
214
+
215
+ 1. **Cost is a roughly fixed additive overhead, not a multiplier** — ~100–300 ns/op of extra work (sketch
216
+ increment + region bookkeeping), so the "Nx" you see depends mostly on how cheap the underlying op already was.
217
+ 2. **O(1) and predictable** — confirmed across 3 orders of magnitude; the only growth is shared CPU-cache locality.
218
+ 3. **No tail surprises** — the overhead is spread across the whole distribution (median through p999), not a rare
219
+ spike; the worst-case `max` is a GC pause both caches share.
220
+ 4. **~14% more memory per entry** — the per-element `region` + `keyHash` fields plus the O(N) frequency sketch
221
+ (plus a shared ~+80 B/entry once any cache is full and churning — the HashMap doubling its slab, not policy-specific).
222
+ 5. **Bought against scan resistance** — the effectiveness axis shows large hit-rate wins on scan/cyclic workloads,
223
+ while staying roughly even with LRU on plain skew and doing no harm on uniform.
224
+
225
+ **The trade in one line:** ~150–200 ns/op and ~14% RAM, predictable and O(1), in exchange for scan-resistant,
226
+ frequency-aware retention — worth it exactly when your misses are expensive and your traffic isn't purely uniform.
227
+
228
+ ## Methodology notes (why the numbers are trustworthy)
229
+
230
+ - **Pre-generated keys** outside the timed region (else you measure string allocation, not the cache).
231
+ - **Warmup** discarded so the JIT has compiled the hot path (we measure optimized machine code, not the cold
232
+ interpreter).
233
+ - **Interleaved trials** — every round times LRU and W-TinyLFU back-to-back, and the ratio is the **median of
234
+ the per-round ratios** (paired). So machine drift within a round hits both equally and cancels in the ratio.
235
+ - **Median of trials** (not mean) to shrug off the occasional GC pause.
236
+ - **Selective `forceGc`** — forced for low-allocation bodies (reads, teardown) where a stray pause is pure noise;
237
+ *not* for allocation-heavy bodies (fill, churn) where GC is part of the real cost.
238
+ - **Result sinks** so pure-read loops aren't deleted by dead-code elimination.
239
+ - **Matched capacity & shared seeded workloads** so LRU and W-TinyLFU face identical conditions.
240
+ - **Measured under `node`, never in-process under a test runner** — a transform (babel/esbuild) distorts both
241
+ the absolute ns *and* the ratio (a cheap op suffers proportionally more overhead). Measured under Jest
242
+ in-process, several ratios *inverted* (e.g. `fill` read 1.76x → 0.91x). That's why the self-executing spec
243
+ spawns a clean `node` child instead of measuring inside Jest — see *Why not measure under Jest*.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pure decision predicates for the W-TinyLFU policy.
3
+ *
4
+ * No state: weights and frequencies in, verdicts out. This is the policy's "functional core" —
5
+ * what makes the cache W-TinyLFU rather than a generic cache — extracted so each decision can be
6
+ * unit-tested directly with plain numbers, without observing element placement.
7
+ */
8
+ /**
9
+ * Total weight Main (Trial + Protected) is allowed to hold.
10
+ * @param {number} maxWeight total cache weight budget
11
+ * @param {number} windowMaxWeight Window region's weight cap
12
+ * @returns {number}
13
+ */
14
+ export function mainCapacity(maxWeight: number, windowMaxWeight: number): number;
15
+ /**
16
+ * Does Main exceed its budget once `extraWeight` is added? Counts current Trial occupancy.
17
+ * @param {number} trialWeight
18
+ * @param {number} protectedWeight
19
+ * @param {number} extraWeight
20
+ * @param {number} mainCap
21
+ * @returns {boolean}
22
+ */
23
+ export function exceedsMainWeight(trialWeight: number, protectedWeight: number, extraWeight: number, mainCap: number): boolean;
24
+ /**
25
+ * Could `weight` never fit in Main, even with Trial fully evicted? Only Protected's live weight is
26
+ * the immovable floor (Trial is fully evictable). Distinct from {@link exceedsMainWeight}, which
27
+ * counts current Trial occupancy.
28
+ * @param {number} protectedWeight
29
+ * @param {number} weight
30
+ * @param {number} mainCap
31
+ * @returns {boolean}
32
+ */
33
+ export function cannotFitMain(protectedWeight: number, weight: number, mainCap: number): boolean;
34
+ /**
35
+ * Is `weight` too big to ever fit in Window, even when empty? (ceiling test, ignores occupancy)
36
+ * @param {number} weight
37
+ * @param {number} windowMaxWeight
38
+ * @returns {boolean}
39
+ */
40
+ export function tooBigForWindow(weight: number, windowMaxWeight: number): boolean;
41
+ /**
42
+ * Decide where a new element of `weight` goes on admission:
43
+ * - REJECT: can never fit Main, even with Trial fully evicted;
44
+ * - TRIAL: fits Main but too big for Window, so goes straight to the admission duel;
45
+ * - WINDOW: normal case.
46
+ * @param {number} weight
47
+ * @param {number} protectedWeight live Protected occupancy (the immovable floor)
48
+ * @param {number} windowMaxWeight Window's weight cap
49
+ * @param {number} mainCap Main's weight budget
50
+ * @returns {number} one of {@link AdmissionRoute}
51
+ */
52
+ export function classifyAdmission(weight: number, protectedWeight: number, windowMaxWeight: number, mainCap: number): number;
53
+ /**
54
+ * Does the admission candidate beat the victim on frequency? Ties favour the incumbent (strict >).
55
+ * @param {number} candidateFrequency
56
+ * @param {number} victimFrequency
57
+ * @returns {boolean}
58
+ */
59
+ export function winsAdmissionDuel(candidateFrequency: number, victimFrequency: number): boolean;
60
+ /**
61
+ * Should a Trial element be promoted to Protected? It must be hot enough AND fit Protected — an
62
+ * element with weight > protectedMaxWeight is a valid Trial resident that stays pinned (never
63
+ * promotable). So this is the full Trial→Protected transition gate.
64
+ * @param {number} frequency
65
+ * @param {number} promotionThreshold
66
+ * @param {number} weight
67
+ * @param {number} protectedMaxWeight
68
+ * @returns {boolean}
69
+ */
70
+ export function shouldPromoteToProtected(frequency: number, promotionThreshold: number, weight: number, protectedMaxWeight: number): boolean;
71
+ /**
72
+ * Where a newly-admitted element should be routed.
73
+ */
74
+ export type AdmissionRoute = number;
75
+ export namespace AdmissionRoute {
76
+ let REJECT: number;
77
+ let WINDOW: number;
78
+ let TRIAL: number;
79
+ }
80
+ //# sourceMappingURL=wtinylfuDecisions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wtinylfuDecisions.d.ts","sourceRoot":"","sources":["../../../../../src/core/cache/wtinylfu/wtinylfuDecisions.js"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,wCAJW,MAAM,mBACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;;;GAOG;AACH,+CANW,MAAM,mBACN,MAAM,eACN,MAAM,WACN,MAAM,GACJ,OAAO,CAKnB;AAED;;;;;;;;GAQG;AACH,+CALW,MAAM,UACN,MAAM,WACN,MAAM,GACJ,OAAO,CAKnB;AAED;;;;;GAKG;AACH,wCAJW,MAAM,mBACN,MAAM,GACJ,OAAO,CAInB;AAQD;;;;;;;;;;GAUG;AACH,0CANW,MAAM,mBACN,MAAM,mBACN,MAAM,WACN,MAAM,GACJ,MAAM,CAMlB;AAED;;;;;GAKG;AACH,sDAJW,MAAM,mBACN,MAAM,GACJ,OAAO,CAInB;AAED;;;;;;;;;GASG;AACH,oDANW,MAAM,sBACN,MAAM,UACN,MAAM,sBACN,MAAM,GACJ,OAAO,CAKnB;;;;6BA5CS,MAAM"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Pure decision predicates for the W-TinyLFU policy.
3
+ *
4
+ * No state: weights and frequencies in, verdicts out. This is the policy's "functional core" —
5
+ * what makes the cache W-TinyLFU rather than a generic cache — extracted so each decision can be
6
+ * unit-tested directly with plain numbers, without observing element placement.
7
+ */
8
+
9
+ /**
10
+ * Total weight Main (Trial + Protected) is allowed to hold.
11
+ * @param {number} maxWeight total cache weight budget
12
+ * @param {number} windowMaxWeight Window region's weight cap
13
+ * @returns {number}
14
+ */
15
+ export function mainCapacity(maxWeight, windowMaxWeight) {
16
+ return maxWeight - windowMaxWeight;
17
+ }
18
+
19
+ /**
20
+ * Does Main exceed its budget once `extraWeight` is added? Counts current Trial occupancy.
21
+ * @param {number} trialWeight
22
+ * @param {number} protectedWeight
23
+ * @param {number} extraWeight
24
+ * @param {number} mainCap
25
+ * @returns {boolean}
26
+ */
27
+ export function exceedsMainWeight(trialWeight, protectedWeight, extraWeight, mainCap) {
28
+ const projectedMainWeight = trialWeight + protectedWeight + extraWeight
29
+ return projectedMainWeight > mainCap;
30
+ }
31
+
32
+ /**
33
+ * Could `weight` never fit in Main, even with Trial fully evicted? Only Protected's live weight is
34
+ * the immovable floor (Trial is fully evictable). Distinct from {@link exceedsMainWeight}, which
35
+ * counts current Trial occupancy.
36
+ * @param {number} protectedWeight
37
+ * @param {number} weight
38
+ * @param {number} mainCap
39
+ * @returns {boolean}
40
+ */
41
+ export function cannotFitMain(protectedWeight, weight, mainCap) {
42
+ const requiredMainWeight = protectedWeight + weight;
43
+ return requiredMainWeight > mainCap;
44
+ }
45
+
46
+ /**
47
+ * Is `weight` too big to ever fit in Window, even when empty? (ceiling test, ignores occupancy)
48
+ * @param {number} weight
49
+ * @param {number} windowMaxWeight
50
+ * @returns {boolean}
51
+ */
52
+ export function tooBigForWindow(weight, windowMaxWeight) {
53
+ return weight > windowMaxWeight;
54
+ }
55
+
56
+ /**
57
+ * Where a newly-admitted element should be routed.
58
+ * @enum {number}
59
+ */
60
+ export const AdmissionRoute = { REJECT: 0, WINDOW: 1, TRIAL: 2 };
61
+
62
+ /**
63
+ * Decide where a new element of `weight` goes on admission:
64
+ * - REJECT: can never fit Main, even with Trial fully evicted;
65
+ * - TRIAL: fits Main but too big for Window, so goes straight to the admission duel;
66
+ * - WINDOW: normal case.
67
+ * @param {number} weight
68
+ * @param {number} protectedWeight live Protected occupancy (the immovable floor)
69
+ * @param {number} windowMaxWeight Window's weight cap
70
+ * @param {number} mainCap Main's weight budget
71
+ * @returns {number} one of {@link AdmissionRoute}
72
+ */
73
+ export function classifyAdmission(weight, protectedWeight, windowMaxWeight, mainCap) {
74
+ if (cannotFitMain(protectedWeight, weight, mainCap)) return AdmissionRoute.REJECT;
75
+ if (tooBigForWindow(weight, windowMaxWeight)) return AdmissionRoute.TRIAL;
76
+ return AdmissionRoute.WINDOW;
77
+ }
78
+
79
+ /**
80
+ * Does the admission candidate beat the victim on frequency? Ties favour the incumbent (strict >).
81
+ * @param {number} candidateFrequency
82
+ * @param {number} victimFrequency
83
+ * @returns {boolean}
84
+ */
85
+ export function winsAdmissionDuel(candidateFrequency, victimFrequency) {
86
+ return candidateFrequency > victimFrequency;
87
+ }
88
+
89
+ /**
90
+ * Should a Trial element be promoted to Protected? It must be hot enough AND fit Protected — an
91
+ * element with weight > protectedMaxWeight is a valid Trial resident that stays pinned (never
92
+ * promotable). So this is the full Trial→Protected transition gate.
93
+ * @param {number} frequency
94
+ * @param {number} promotionThreshold
95
+ * @param {number} weight
96
+ * @param {number} protectedMaxWeight
97
+ * @returns {boolean}
98
+ */
99
+ export function shouldPromoteToProtected(frequency, promotionThreshold, weight, protectedMaxWeight) {
100
+ return (frequency > promotionThreshold)
101
+ && (weight <= protectedMaxWeight);
102
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"RingBuffer.d.ts","sourceRoot":"","sources":["../../../../src/core/collection/RingBuffer.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH;IACI;;OAEG;IACH,kBAFW,MAAM,EAoChB;IA7BG;;;OAGG;IACH,MAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,MAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,MAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,OAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,MAFU,CAAC,EAAE,CAEc;IAG/B;;;OAGG;IACH,iBAFW,MAAM,QAwBhB;IAED;;;OAGG;IACH,WAFY,CAAC,CAIZ;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACJ,CAAC,CAYb;IAED,cAIC;IAED;;;OAGG;IACH,cAFW,CAAC,QAuBX;IAED;;;OAGG;IACH,SAFa,CAAC,GAAC,SAAS,CAcvB;IAED;;;OAGG;IACH,4BAFW,MAAM,QA8BhB;IAED;;;;;OAKG;IACH,2BAJoB,CAAC,KAAE,OAAO,kBAEjB,CAAC,GAAC,SAAS,CAsBvB;IAED;;;;OAIG;IACH,wBAHoB,CAAC,+BAmBpB;IAED;;;;OAIG;IACH,gBAHW,CAAC,GACC,OAAO,CAKnB;CACJ"}
1
+ {"version":3,"file":"RingBuffer.d.ts","sourceRoot":"","sources":["../../../../src/core/collection/RingBuffer.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH;IACI;;OAEG;IACH,kBAFW,MAAM,EAoChB;IA7BG;;;OAGG;IACH,MAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,MAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,MAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,OAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,MAFU,CAAC,EAAE,CAEc;IAG/B;;;OAGG;IACH,iBAFW,MAAM,QAwBhB;IAED;;;OAGG;IACH,WAFY,CAAC,CAKZ;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACJ,CAAC,CAYb;IAED,cAIC;IAED;;;OAGG;IACH,cAFW,CAAC,QAuBX;IAED;;;OAGG;IACH,SAFa,CAAC,GAAC,SAAS,CAcvB;IAED;;;OAGG;IACH,4BAFW,MAAM,QA8BhB;IAED;;;;;OAKG;IACH,2BAJoB,CAAC,KAAE,OAAO,kBAEjB,CAAC,GAAC,SAAS,CAsBvB;IAED;;;;OAIG;IACH,wBAHoB,CAAC,+BAmBpB;IAED;;;;OAIG;IACH,gBAHW,CAAC,GACC,OAAO,CAgBnB;CACJ"}
@@ -80,7 +80,8 @@ export class RingBuffer {
80
80
  * @return {V}
81
81
  */
82
82
  getHead() {
83
- return this.data[this.head - 1];
83
+ // delegate so the head-1 index wraps correctly when the write cursor sits at slot 0
84
+ return this.getFromHead(0);
84
85
  }
85
86
 
86
87
  /**
@@ -242,7 +243,18 @@ export class RingBuffer {
242
243
  * @returns {boolean}
243
244
  */
244
245
  contains(value) {
245
- // TODO do a bounds check
246
- return this.data.indexOf(value) !== -1;
246
+ // scan only live slots — a shift()-ed element is still physically present in the backing array
247
+ const count = this.count;
248
+ const size = this.size;
249
+ const tail = this.tail;
250
+ const data = this.data;
251
+
252
+ for (let i = 0; i < count; i++) {
253
+ if (data[(tail + i) % size] === value) {
254
+ return true;
255
+ }
256
+ }
257
+
258
+ return false;
247
259
  }
248
260
  }
@@ -79,7 +79,7 @@ export class HashMap<K, V> {
79
79
  */
80
80
  private __bin_count_mask;
81
81
  /**
82
- * How full the table can get before number of buckets is increased
82
+ * Fraction of the entries slab allowed live before it grows instead of compacting
83
83
  * @type {number}
84
84
  * @private
85
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"HashMap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/map/HashMap.js"],"names":[],"mappings":"AA+GA;;;;;;;;;GASG;AACH;IAsFI;;;;;;OAMG;IACH,4FALuB,MAAM,EAsC5B;IA7HD;;;;;OAKG;IACH,eAAoB;IAEpB;;;;OAIG;IACH,kBAAyB;IAEzB;;;;OAIG;IACH,wBAAoB;IAEpB;;;;OAIG;IACH,wBAAoB;IAEpB;;;;OAIG;IACH,eAAW;IAEX;;;;OAIG;IACH,oBAAgB;IAEhB;;;;OAIG;IACH,kCAA8B;IAE9B;;;;OAIG;IACH,iCAA6B;IAE7B;;;;OAIG;IACH,qCAAiC;IAEjC;;;;OAIG;IACH,yBAAqB;IAErB;;;;OAIG;IACH,sBAAoC;IAEpC;;;;OAIG;IACH,kBAAc;IAwBV;;;;;OAKG;IACH,iCAAsC;IACtC;;;;;OAKG;IACH,qCAA8C;IAOlD;;;OAGG;IACH,mBAEC;IAED;;;OAGG;IACH,kBAFa,MAAM,CAIlB;IA2CD;;;;;OAKG;IACH,0BAUC;IAuGD;;;;OAIG;IACH,SAHW,CAAC,SACD,CAAC,QA0EX;IAED;;;;OAIG;IACH,SAHW,CAAC,GACC,CAAC,GAAC,SAAS,CA+BvB;IAED;;;;;;;;OAQG;IACH,kBALW,CAAC,kBACQ,CAAC,KAAE,CAAC,0BAEZ,CAAC,CAgBZ;IAED;;;;;OAKG;IACH,cAJW,CAAC,SACD,CAAC,GACA,CAAC,CAaZ;IAwBD;;;;OAIG;IACH,YAHW,CAAC,GACC,OAAO,CA+CnB;IAED;;;;;OAKG;IACH,4CAFa,OAAO,CA+BnB;IAOD;;OAEG;IACH,gBA0DC;IAmBD,2CA0BC;IAED;;;;OAIG;IACH,SAHW,CAAC,GACC,OAAO,CAInB;IAED;;OAEG;IACH,cA6BC;IA+BD;;;OAGG;IACH,WAFa,SAAS,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAM3B;IAED;;;OAGG;IACH,UAFa,SAAS,CAAC,CAAC,CAOvB;IAED;;;OAGG;IACH,QAFa,SAAS,CAAC,CAAC,CAMvB;IA1DD,yDA2BC;;CAgCJ"}
1
+ {"version":3,"file":"HashMap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/map/HashMap.js"],"names":[],"mappings":"AA+GA;;;;;;;;;GASG;AACH;IAsFI;;;;;;OAMG;IACH,4FALuB,MAAM,EAsC5B;IA7HD;;;;;OAKG;IACH,eAAoB;IAEpB;;;;OAIG;IACH,kBAAyB;IAEzB;;;;OAIG;IACH,wBAAoB;IAEpB;;;;OAIG;IACH,wBAAoB;IAEpB;;;;OAIG;IACH,eAAW;IAEX;;;;OAIG;IACH,oBAAgB;IAEhB;;;;OAIG;IACH,kCAA8B;IAE9B;;;;OAIG;IACH,iCAA6B;IAE7B;;;;OAIG;IACH,qCAAiC;IAEjC;;;;OAIG;IACH,yBAAqB;IAErB;;;;OAIG;IACH,sBAAoC;IAEpC;;;;OAIG;IACH,kBAAc;IAwBV;;;;;OAKG;IACH,iCAAsC;IACtC;;;;;OAKG;IACH,qCAA8C;IAOlD;;;OAGG;IACH,mBAEC;IAED;;;OAGG;IACH,kBAFa,MAAM,CAIlB;IA2CD;;;;;OAKG;IACH,0BAUC;IAsGD;;;;OAIG;IACH,SAHW,CAAC,SACD,CAAC,QA+DX;IAED;;;;OAIG;IACH,SAHW,CAAC,GACC,CAAC,GAAC,SAAS,CA+BvB;IAED;;;;;;;;OAQG;IACH,kBALW,CAAC,kBACQ,CAAC,KAAE,CAAC,0BAEZ,CAAC,CAgBZ;IAED;;;;;OAKG;IACH,cAJW,CAAC,SACD,CAAC,GACA,CAAC,CAaZ;IAwBD;;;;OAIG;IACH,YAHW,CAAC,GACC,OAAO,CA+CnB;IAED;;;;;OAKG;IACH,4CAFa,OAAO,CA+BnB;IAOD;;OAEG;IACH,gBA0DC;IAmBD,2CA0BC;IAED;;;;OAIG;IACH,SAHW,CAAC,GACC,OAAO,CAInB;IAED;;OAEG;IACH,cA6BC;IA+BD;;;OAGG;IACH,WAFa,SAAS,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAM3B;IAED;;;OAGG;IACH,UAFa,SAAS,CAAC,CAAC,CAOvB;IAED;;;OAGG;IACH,QAFa,SAAS,CAAC,CAAC,CAMvB;IA1DD,yDA2BC;;CAgCJ"}
@@ -192,7 +192,7 @@ export class HashMap {
192
192
  __bin_count_mask = 0;
193
193
 
194
194
  /**
195
- * How full the table can get before number of buckets is increased
195
+ * Fraction of the entries slab allowed live before it grows instead of compacting
196
196
  * @type {number}
197
197
  * @private
198
198
  */
@@ -413,12 +413,11 @@ export class HashMap {
413
413
  return;
414
414
  }
415
415
 
416
- if (this.__size === this.__entries_allocated_count) {
417
- // used up all allocated entries
418
- // bin count must always be larger than end of the entries table
416
+ if (this.__size >= this.__entries_allocated_count * this.__load_factor) {
417
+ // slab mostly live grow for headroom (compacting would reclaim too little and thrash)
419
418
  this.#grow();
420
419
  } else {
421
- // exhausted entries array, perform compaction
420
+ // dead entries to reclaim — compact in place
422
421
  this.rebuild();
423
422
  }
424
423
  }
@@ -488,18 +487,7 @@ export class HashMap {
488
487
 
489
488
  }
490
489
 
491
- const old_size = this.__size;
492
- const new_size = old_size + 1;
493
- this.__size = new_size;
494
-
495
- // compute actual current load
496
- const bucket_count = this.__bin_count;
497
- const load = new_size / bucket_count;
498
-
499
- if (load > this.__load_factor) {
500
- // current load is too high, increase table size
501
- this.#grow();
502
- }
490
+ this.__size++;
503
491
  }
504
492
 
505
493
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"line2_compute_segment_point_distance_sqr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,6DAPW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,UA4BhB"}
1
+ {"version":3,"file":"line2_compute_segment_point_distance_sqr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,6DAPW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,UAiChB"}
@@ -26,6 +26,11 @@ export function line2_compute_segment_point_distance_sqr(
26
26
  //
27
27
  const d2 = line_delta_x * line_delta_x + line_delta_y * line_delta_y;
28
28
 
29
+ if (d2 === 0) {
30
+ // line is a point
31
+ return v2_distance_sqr(px, py, x0, y0);
32
+ }
33
+
29
34
  const d3 = line_delta_x * sp0_x + line_delta_y * sp0_y;
30
35
 
31
36
  const t = clamp01(d3 / d2);
@@ -1 +1 @@
1
- {"version":3,"file":"ConicRay.d.ts","sourceRoot":"","sources":["../../../../src/core/geom/ConicRay.js"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH;IAgLI;;;;;;;OAOG;IACH,sBANW,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,QAAQ,CAkBpB;IAtMD;;;;OAIG;IACH,oBAFU,OAAO,CAEgB;IAEjC;;;;OAIG;IACH,OAFU,MAAM,CAEN;IAEV;;;;;;;MAKC;IAED,0BAGC;IAED;;;OAGG;IACH,2CAGC;IAED;;;OAGG;IACH,6CAMC;IAED;;;;OAIG;IACH,cAHW,QAAQ,GACN,OAAO,CAMnB;IAED;;;;;OAKG;IACH,qBAJW,QAAQ,cACR,MAAM,GACJ,OAAO,CAKnB;IAED;;;OAGG;IACH,QAFY,MAAM,CAOjB;IAED;;;OAGG;IACH,YAFW,QAAQ,QAKlB;IAED;;;;OAIG;IACH,4BAHW,OAAO,GACL,OAAO,CAMnB;IAED;;;;;OAKG;IACH,gDAFW,OAAO,QAoEjB;IA6BL;;;OAGG;IACH,qBAFU,OAAO,CAEY;CAN5B;;kBAUS,MAAM;;oBA7NI,cAAc"}
1
+ {"version":3,"file":"ConicRay.d.ts","sourceRoot":"","sources":["../../../../src/core/geom/ConicRay.js"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH;IAuLI;;;;;;;OAOG;IACH,sBANW,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,QAAQ,CAkBpB;IA7MD;;;;OAIG;IACH,oBAFU,OAAO,CAEgB;IAEjC;;;;OAIG;IACH,OAFU,MAAM,CAEN;IAEV;;;;;;;MAKC;IAED,0BAGC;IAED;;;OAGG;IACH,2CAGC;IAED;;;OAGG;IACH,6CAMC;IAED;;;;OAIG;IACH,cAHW,QAAQ,GACN,OAAO,CAMnB;IAED;;;;;OAKG;IACH,qBAJW,QAAQ,cACR,MAAM,GACJ,OAAO,CAKnB;IAED;;;OAGG;IACH,QAFY,MAAM,CAOjB;IAED;;;OAGG;IACH,YAFW,QAAQ,QAKlB;IAED;;;;OAIG;IACH,4BAHW,OAAO,GACL,OAAO,CAMnB;IAED;;;;;OAKG;IACH,gDAFW,OAAO,QA2EjB;IA6BL;;;OAGG;IACH,qBAFU,OAAO,CAEY;CAN5B;;kBAUS,MAAM;;oBApOI,cAAc"}
@@ -153,6 +153,13 @@ export class ConicRay {
153
153
  return;
154
154
  }
155
155
 
156
+ // The antipode (0,0,-1) is the singularity of the k = 1/(1+dZ) rotation below (dZ === -1 -> k = Infinity).
157
+ // Map the cap onto the south pole with a 180 degree rotation about the X axis: (x, y, z) -> (x, -y, -z).
158
+ if (direction._equals(0, 0, -1)) {
159
+ result.set(x, -y, -z);
160
+ return;
161
+ }
162
+
156
163
  assert.ok(direction.isNormalized(), 'direction vector must be normalized');
157
164
 
158
165
  // Find the rotation axis `u` and rotation angle `rot` [1]
@@ -168,8 +175,8 @@ export class ConicRay {
168
175
 
169
176
  //write matrix
170
177
  const c = dZ;
171
- // NOTE: singular at dZ === -1 (the antipode). The (0,0,1) case is handled by the early return above and
172
- // cone sampling here keeps dZ > -1.
178
+ // Singular at dZ === -1 (the antipode); both the (0,0,1) and (0,0,-1) poles are handled by the early
179
+ // returns above, so dZ is strictly in (-1, 1) here.
173
180
  const k = 1 / (1 + dZ);
174
181
  const tx = -k * dY;
175
182
  const ty = k * dX;
@@ -1 +1 @@
1
- {"version":3,"file":"Quaternion.d.ts","sourceRoot":"","sources":["../../../../src/core/geom/Quaternion.js"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;GAcG;AACH,iEALwB,MAAM;IA2R1B;;;;;OAKG;IACH,2BAJW,OAAO,SACP,MAAM,GACJ,UAAU,CAQtB;IA4rCD;;;;OAIG;IACH,kCAFa,UAAU,CAQtB;IAED;;;;;;;OAOG;IACH,0BALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,UAAU,CAQtB;IAED;;;;;;OAMG;IACH,6BALW,UAAU,QACV,UAAU,MACV,UAAU,aACV,MAAM,QA0BhB;IA1/CD;;;;OAIG;IACH,uDAEC;IAtCD;;;;;;;OAOG;IACH,gBANW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,EAwBhB;IAbG,UAAW;IACX,UAAW;IACX,UAAW;IACX,UAAW;IAEX;;;;;;OAMG;IACH,wBAA6B;IAoBjC;;;OAGG;IACH,mBAEC;IAdD;;;OAGG;IACH,gBAEC;IAkBD;;;OAGG;IACH,mBAEC;IAdD;;;OAGG;IACH,gBAEC;IAkBD;;;OAGG;IACH,mBAEC;IAdD;;;OAGG;IACH,gBAEC;IAkBD;;;OAGG;IACH,mBAEC;IAdD;;;OAGG;IACH,gBAEC;IAWD;;;;;;;;;;;;;;OAcG;IACH,kBARW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,IAAI,CAqDhB;IAED;;;;;OAKG;IACH,sBAJW,OAAO,OACP,OAAO,GACL,IAAI,CAShB;IAED;;;;OAIG;IACH,WAHW,UAAU,GACT,MAAM,CAQjB;IAED;;;;OAIG;IACH,mBAHW,UAAU,GACR,IAAI,CAKhB;IAED;;;;;;;OAOG;IACH,UAHa,IAAI,CAwBhB;IAED;;;;OAIG;IACH,aAHa,IAAI,CAKhB;IAED;;;;OAIG;IACH,eAHW,UAAU,GACT,MAAM,CAoBjB;IAiBD;;;;;OAKG;IACH,oBAJW,OAAO,SACP,MAAM,GACJ,IAAI,CAQhB;IAED;;;;;;;OAOG;IACH,uBANW,MAAM,UACN,MAAM,UACN,MAAM,SACN,MAAM,GACJ,IAAI,CAkChB;IAED;;;;;;;;OAQG;IACH,2BANW,OAAO,SACP,UAAU,SACV,UAAU,GACR,IAAI,CAyChB;IAED;;;;OAIG;IACH,wBAHW,OAAO,GACL,MAAM,CAYlB;IAED;;;;OAIG;IACH,sBAHW,OAAO,GACL,MAAM,CAmBlB;IAED;;;OAGG;IACH,aAFa,IAAI,CAoBhB;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACL,IAAI,CASf;IAED;;;OAGG;IACH,gBAHW,UAAU,GACR,IAAI,CAIhB;IAED;;;;;OAKG;IACH,2BAJW,UAAU,UACV,UAAU,GACR,IAAI,CAchB;IAED;;;;;;;;;;;OAWG;IACH,yBAVW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,IAAI,CAehB;IAED;;;OAGG;IACH,UAFY,MAAM,CASjB;IAED;;;;;OAKG;IACH,qBAJW,UAAU,aACV,MAAM,GACJ,IAAI,CAMhB;IAED;;;;;;OAMG;IACH,eALW,OAAO,UACP,OAAO,OACP,OAAO,QAWjB;IAGD;;;;;;;;;;;;;;;OAeG;IACH,kBAbW,MAAM,KACN,MAAM,KACN,MAAM,mBAEJ,IAAI,CA+ChB;IAED;;;;OAIG;IACH,yBAFW,OAAO,QA4BjB;IAED;;;OAGG;IACH,yBAFW,OAAO,QA2BjB;IAED;;;OAGG;IACH,yBAFW,OAAO,QA0BjB;IAED;;;;;;;;;;;;;OAaG;IACH,gBAPW,MAAM,MACN,MAAM,MACN,MAAM,GACJ,IAAI,CAUhB;IAGD;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;;;;OASG;IACH,sBALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CAqBhB;IAED;;;;;;OAMG;IACH,sBAJW,OAAO,MACP,OAAO,GACL,IAAI,CAkEhB;IAED;;;OAGG;IACH,4BAHW,MAAM,EAAE,GAAC,YAAY,GACnB,IAAI,CAYhB;IAED;;;;;;;;;;;;;;OAcG;IACH,6BAXW,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,IAAI,CA4EhB;IAED;;;;;OAKG;IACH,YAJW,UAAU,KACV,MAAM,GACJ,IAAI,CAMhB;IAED;;;;;;;OAOG;IACH,uBALW,UAAU,UACV,UAAU,KACV,MAAM,GACJ,IAAI,CAgBhB;IAED;;;;;;OAMG;IACH,uBALW,UAAU,MACV,UAAU,KACV,MAAM,GACJ,IAAI,CA4DhB;IAGD;;;;;OAKG;IACH,aAJW,UAAU,KACV,MAAM,GACJ,IAAI,CAIhB;IAED;;;;;OAKG;IACH,sCAFa,IAAI,CAUhB;IAED;;;;OAIG;IACH,YAHW,UAAU,GACR,IAAI,CAIhB;IAED;;;OAGG;IACH,SAFa,UAAU,CAQtB;IAED;;;;;;;;OAQG;IACH,OANW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,GACJ,IAAI,CA+BhB;IAED;;;;;MAOC;IAED;;;;OAIG;IACH,oBAFY,IAAI,CAIf;IAED;;;OAGG;IACH,2CAKC;IAED;;;OAGG;IACH,6CAOC;IAED;;;OAGG;IACH,kDAKC;IAED;;;OAGG;IACH,oDAOC;IAED;;;;;OAKG;IACH,iBAJW,MAAM,EAAE,WACR,MAAM,GACJ,IAAI,CAYhB;IAED;;;;;OAKG;IACH,gBAJW,MAAM,EAAE,WACR,MAAM,GACJ,MAAM,EAAE,CAYpB;IAED;;;;;OAKG;IACH,cAJW,UAAU,GACR,OAAO,CASnB;IAED;;;OAGG;IACH,QAFa,MAAM,CAQlB;IAED;;;;;OAKG;IACH,qBAJW,UAAU,cACV,MAAM,GACL,OAAO,CAOlB;IAED;;;;;;;;OAQG;IACH,kBAPW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACL,OAAO,CASlB;IAED;;;OAGG;IACH,wBAFsB,MAAM,QAI3B;IAED;;;OAGG;IACH,sBAFsB,MAAM,QAuB3B;IAqEL;;OAEG;IACH,uBApMe,MAAM,EAAE,WACR,MAAM,KACJ,IAAI,CAkMa;IAElC;;OAEG;IACH,uBAvLe,MAAM,EAAE,WACR,MAAM,KACJ,MAAM,EAAE,CAqLQ;IAEjC;;OAEG;IACH,kBA5Le,MAAM,EAAE,WACR,MAAM,KACJ,MAAM,EAAE,CA0LG;IAC5B,qBA3zBe,MAAM,KACN,MAAM,KACN,MAAM,KACJ,IAAI,CAwzBe;IAQpC;;;;OAIG;IACH,uBAFU,OAAO,CAEgB;CA7BhC;;kBAoBS,UAAU;kBAaV,MAAM;;;oBAplDI,cAAc"}
1
+ {"version":3,"file":"Quaternion.d.ts","sourceRoot":"","sources":["../../../../src/core/geom/Quaternion.js"],"names":[],"mappings":"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"}
@@ -687,7 +687,9 @@ export class Quaternion extends Float64Array {
687
687
 
688
688
  const r21 = 2 * (x * z + y * w);
689
689
 
690
- const theta = Math.asin(r21);
690
+ // r21 is mathematically in [-1, 1]; floating-point rounding can push it just past the bound at
691
+ // gimbal lock, where Math.asin would return NaN. Clamp removes only the overshoot.
692
+ const theta = Math.asin(clamp(r21, -1, 1));
691
693
 
692
694
  const r11 = 2 * (z * w - x * y);
693
695
  const r12 = w2 + x2 - y2 - z2;
@@ -721,7 +723,8 @@ export class Quaternion extends Float64Array {
721
723
  r32 = w2 - x2 + y2 - z2;
722
724
 
723
725
  const psi = Math.atan2(r31, r32);
724
- const theta = Math.asin(r21);
726
+ // clamp against floating-point overshoot at gimbal lock (see toEulerAnglesXYZ)
727
+ const theta = Math.asin(clamp(r21, -1, 1));
725
728
  const phi = Math.atan2(r11, r12);
726
729
 
727
730
  // result.set(psi, theta, phi);
@@ -752,7 +755,8 @@ export class Quaternion extends Float64Array {
752
755
  r32 = ww - xx - yy + zz;
753
756
 
754
757
  const psi = Math.atan2(r31, r32);
755
- const theta = Math.asin(r21);
758
+ // clamp against floating-point overshoot at gimbal lock (see toEulerAnglesXYZ)
759
+ const theta = Math.asin(clamp(r21, -1, 1));
756
760
  const phi = Math.atan2(r11, r12);
757
761
 
758
762
  result.set(psi, theta, phi);
@@ -1,8 +1,8 @@
1
1
  /**
2
- *
2
+ * Build a normalized 1-dimensional gaussian kernel, taps sum to 1.
3
3
  * @param {Float32Array} kernel
4
4
  * @param {number} sample_count
5
- * @param {number} sigma
5
+ * @param {number} sigma standard deviation, must be greater than 0. A sigma of 0 makes every tap `NaN`, as the centre tap evaluates `exp(-0/0)`
6
6
  */
7
7
  export function build_gaussian_kernel_1d(kernel: Float32Array, sample_count: number, sigma: number): void;
8
8
  //# sourceMappingURL=build_gaussian_kernel_1d.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build_gaussian_kernel_1d.d.ts","sourceRoot":"","sources":["../../../../src/core/math/build_gaussian_kernel_1d.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,iDAJW,YAAY,gBACZ,MAAM,SACN,MAAM,QAmBhB"}
1
+ {"version":3,"file":"build_gaussian_kernel_1d.d.ts","sourceRoot":"","sources":["../../../../src/core/math/build_gaussian_kernel_1d.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,iDAJW,YAAY,gBACZ,MAAM,SACN,MAAM,QAoBhB"}