@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.
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
- package/src/core/binary/BinaryBuffer.js +1 -1
- package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/EncodingBinaryBuffer.js +3 -1
- package/src/core/cache/Cache.d.ts.map +1 -1
- package/src/core/cache/Cache.js +0 -4
- package/src/core/cache/CacheElement.d.ts.map +1 -1
- package/src/core/cache/CacheElement.js +4 -0
- package/src/core/cache/FrequencySketch.d.ts +0 -21
- package/src/core/cache/FrequencySketch.d.ts.map +1 -1
- package/src/core/cache/FrequencySketch.js +66 -65
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
- package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
- package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
- package/src/core/collection/RingBuffer.d.ts.map +1 -1
- package/src/core/collection/RingBuffer.js +15 -3
- package/src/core/collection/map/HashMap.d.ts +1 -1
- package/src/core/collection/map/HashMap.d.ts.map +1 -1
- package/src/core/collection/map/HashMap.js +5 -17
- package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
- package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
- package/src/core/geom/ConicRay.d.ts.map +1 -1
- package/src/core/geom/ConicRay.js +9 -2
- package/src/core/geom/Quaternion.d.ts.map +1 -1
- package/src/core/geom/Quaternion.js +7 -3
- package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
- package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
- package/src/core/math/build_gaussian_kernel_1d.js +4 -2
- package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
- package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
- package/src/core/math/build_gaussian_kernel_2d.js +4 -2
- package/src/core/math/gaussian.d.ts +2 -2
- package/src/core/math/gaussian.js +2 -2
- package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
- package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
- package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
- package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
- package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
- package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
- package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
- package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
- package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
- package/src/core/math/statistics/computeStatisticalMean.js +1 -1
- package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
- package/src/core/math/statistics/gaussian_amplitude.js +3 -1
- package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
- package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
- package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
- package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
- package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
- package/src/engine/ecs/EntityComponentDataset.js +5 -21
- package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
- package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWar.js +202 -54
- package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
- package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
- package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
- package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
- package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
- package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
- package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
- package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
- package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +14 -3
- package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
- package/src/engine/graphics/composit/CompositLayer.js +258 -255
- package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
- package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
- package/src/engine/graphics/composit/LayerCompositer.js +19 -10
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
- package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
- package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
- package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
- package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
- package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
- package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
- package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
- package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
- package/src/engine/graphics/trail/tube/TubeX.js +557 -555
- package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
- package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
- package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
- package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
- package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
- package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
- package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
- package/src/engine/navigation/ecs/components/Path.js +10 -4
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- 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,
|
|
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
|
-
|
|
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
|
-
//
|
|
246
|
-
|
|
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
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
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
|
|
417
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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;
|
|
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
|
-
//
|
|
172
|
-
//
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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"}
|