@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,807 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frequency-based admission cache, using W-TinyLFU (Window Tiny Least Frequency)
|
|
3
|
+
*
|
|
4
|
+
* Cache regions:
|
|
5
|
+
* 1. Window (LRU) Burst traffic
|
|
6
|
+
* 2. Main (SLRU) Long-term residency
|
|
7
|
+
* Probation (LRU) Trial area for candidate from Window or demoted from Protected
|
|
8
|
+
* Protected (LRU) VIP area for frequently accessed items
|
|
9
|
+
*
|
|
10
|
+
* Life cycle:
|
|
11
|
+
* 1. Admission: New items enter the Window.
|
|
12
|
+
* 2. Promoting to Probation: If Window is full, the Candidate (oldest window element) attempts to move
|
|
13
|
+
* into the Probation.
|
|
14
|
+
* 3. Frequency Duel: If the Probation is full, the Candidate (oldest window element) competes against the
|
|
15
|
+
* Victim (oldest probation element) using a Count-Min Sketch frequency filter.
|
|
16
|
+
* 4. Promotion to Protected: A Probation item accessed again (or exceeds a threshold) is promoted to the
|
|
17
|
+
* Protected list.
|
|
18
|
+
* 5. Demotion: If the Protected list reaches its capacity, the oldest item is demoted
|
|
19
|
+
* to the Probation list to make room.
|
|
20
|
+
*
|
|
21
|
+
* Capacity management:
|
|
22
|
+
* Can be governed by count or weight
|
|
23
|
+
* Window ~1%
|
|
24
|
+
* Probation ~20%
|
|
25
|
+
* Protected ~80%
|
|
26
|
+
* The Main cache segments are dynamic. Protected is capped at ~80%. Probation will vary
|
|
27
|
+
* as it consumes the remainder, based on promotion/demotion influence.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {CacheRegionWTinylfu} from "./CacheRegionWTinylfu.js";
|
|
31
|
+
import {assert} from "../../assert.js";
|
|
32
|
+
import {CacheElementWTinylfu} from "./CacheElementWTinylfu.js";
|
|
33
|
+
import {CacheRegionType} from "./CacheRegionType.js";
|
|
34
|
+
import {returnZero} from "../../function/returnZero.js";
|
|
35
|
+
import {returnOne} from "../../function/returnOne.js";
|
|
36
|
+
import {FrequencySketch} from "../FrequencySketch.js";
|
|
37
|
+
import {invokeObjectHash} from "../../model/object/invokeObjectHash.js";
|
|
38
|
+
import {invokeObjectEquals} from "../../model/object/invokeObjectEquals.js";
|
|
39
|
+
import {HashMap} from "../../collection/map/HashMap.js";
|
|
40
|
+
import {mainCapacity, exceedsMainWeight, cannotFitMain, winsAdmissionDuel, shouldPromoteToProtected, classifyAdmission, AdmissionRoute} from "./wtinylfuDecisions.js";
|
|
41
|
+
import {Signal} from "../../events/signal/Signal.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cap on the reuse pool of evicted element objects. Under churn the pool hovers near 1 (one evict per
|
|
45
|
+
* insert), so this only bounds it under remove-heavy bursts; past the cap, elements are left to GC.
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
const ELEMENT_POOL_MAX = 256;
|
|
49
|
+
|
|
50
|
+
export class CacheWTinylfu {
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @type {number}
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
#maxWeight = Number.POSITIVE_INFINITY;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @type {number}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
#promotionThreshold = 1;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @type {function(Key):number}
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
#keyHashFunction = invokeObjectHash;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cache regions — kept as three direct fields (not a {window,trial,protected} record) so each hot-path
|
|
72
|
+
* access is a single property read rather than two.
|
|
73
|
+
* @type {CacheRegionWTinylfu}
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
#window;
|
|
77
|
+
/** @type {CacheRegionWTinylfu} @private */
|
|
78
|
+
#trial;
|
|
79
|
+
/** @type {CacheRegionWTinylfu} @private */
|
|
80
|
+
#protected;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
*
|
|
84
|
+
* @type {FrequencySketch}
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
#sketch = new FrequencySketch();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @type {function(Key): number}
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
#keyWeigher;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @type {function(Value): number}
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
#valueWeigher;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @type {HashMap<Key, CacheElementWTinylfu<Key,Value>>}
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
#data;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reuse pool of evicted element objects. `put` takes one from here instead of allocating a fresh
|
|
109
|
+
* element; under churn this makes steady-state inserts allocation-free. Capped at {@link ELEMENT_POOL_MAX}.
|
|
110
|
+
* @type {CacheElementWTinylfu[]}
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
#elementPool = [];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Dispatched when an element is added or its value replaced. send2(key, value)
|
|
117
|
+
* @type {Signal}
|
|
118
|
+
*/
|
|
119
|
+
onSet = new Signal();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dispatched when an element leaves the cache, by eviction or explicit remove. send2(key, value)
|
|
123
|
+
* @type {Signal}
|
|
124
|
+
*/
|
|
125
|
+
onRemoved = new Signal();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Dispatched when an element leaves due to capacity, immediately after onRemoved. send2(key, value)
|
|
129
|
+
* @type {Signal}
|
|
130
|
+
*/
|
|
131
|
+
onEvicted = new Signal();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Dispatched when admission control turns an element away (reject-guard or lost duel) — it was
|
|
135
|
+
* never stored. Unique to W-TinyLFU. send2(key, value)
|
|
136
|
+
* @type {Signal}
|
|
137
|
+
*/
|
|
138
|
+
onRejected = new Signal();
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
*
|
|
142
|
+
* @param {number} [maxWeight=Number.POSITIVE_INFINITY] total weight budget of the cache; an element's weight is keyWeigher(key) + valueWeigher(value)
|
|
143
|
+
* @param {number} [promotionThreshold=1] frequency an item must exceed to be promoted out of Trial (Probation)
|
|
144
|
+
* @param {number} [capacityWindow=1] weight cap of the Window region (burst buffer)
|
|
145
|
+
* @param {number} [capacityProtected=1] weight cap of the Protected region (VIP segment of Main)
|
|
146
|
+
* @param {function(key:Key):number} [keyWeigher= key=>0] function to compute weight of a key
|
|
147
|
+
* @param {function(value:Value):number} [valueWeigher= value=>1] function to compute weight of a value
|
|
148
|
+
* @param {function(Key):number} [keyHashFunction]
|
|
149
|
+
* @param {function(Key, Key):boolean} [keyEqualityFunction]
|
|
150
|
+
*/
|
|
151
|
+
constructor({
|
|
152
|
+
maxWeight = Number.POSITIVE_INFINITY,
|
|
153
|
+
promotionThreshold = 1,
|
|
154
|
+
capacityWindow = 1,
|
|
155
|
+
capacityProtected = 1,
|
|
156
|
+
keyWeigher = returnZero,
|
|
157
|
+
valueWeigher = returnOne,
|
|
158
|
+
keyHashFunction = invokeObjectHash,
|
|
159
|
+
keyEqualityFunction = invokeObjectEquals
|
|
160
|
+
} = {}) {
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {number} parameter
|
|
164
|
+
* @param {string} message
|
|
165
|
+
*/
|
|
166
|
+
const numberAssert = (parameter, message) => {
|
|
167
|
+
assert.isNumber(parameter, message);
|
|
168
|
+
assert.notNaN(parameter, message);
|
|
169
|
+
assert.greaterThanOrEqual(parameter, 0, message +' < 0');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
numberAssert(maxWeight, 'maxWeight');
|
|
173
|
+
numberAssert(promotionThreshold, 'promotionThreshold');
|
|
174
|
+
numberAssert(capacityWindow, 'capacityWindow');
|
|
175
|
+
numberAssert(capacityProtected, 'capacityProtected');
|
|
176
|
+
|
|
177
|
+
const trialRegionSize = maxWeight - (capacityWindow + capacityProtected)
|
|
178
|
+
assert.greaterThan(trialRegionSize, 0, "probation region <= 0");
|
|
179
|
+
assert.greaterThanOrEqual(trialRegionSize, capacityWindow, 'capacityWindow > trialRegionSize');
|
|
180
|
+
|
|
181
|
+
assert.isFunction(keyWeigher, 'keyWeigher');
|
|
182
|
+
assert.isFunction(valueWeigher, 'valueWeigher');
|
|
183
|
+
assert.isFunction(keyHashFunction, 'keyHashFunction');
|
|
184
|
+
assert.isFunction(keyEqualityFunction, 'keyEqualityFunction');
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
this.#maxWeight = maxWeight;
|
|
188
|
+
this.#promotionThreshold = promotionThreshold;
|
|
189
|
+
this.#keyHashFunction = keyHashFunction;
|
|
190
|
+
|
|
191
|
+
this.#window = new CacheRegionWTinylfu({maxWeight: capacityWindow});
|
|
192
|
+
this.#trial = new CacheRegionWTinylfu({});
|
|
193
|
+
this.#protected = new CacheRegionWTinylfu({maxWeight: capacityProtected});
|
|
194
|
+
|
|
195
|
+
this.#keyWeigher = keyWeigher;
|
|
196
|
+
this.#valueWeigher = valueWeigher;
|
|
197
|
+
|
|
198
|
+
this.#data = new HashMap({
|
|
199
|
+
keyHashFunction,
|
|
200
|
+
keyEqualityFunction
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// The sketch is sized dynamically to the live element count in __admit; seed a minimal table.
|
|
204
|
+
this.#sketch.ensureCapacity(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Update the recency & frequency of the cache element
|
|
209
|
+
* @param {CacheElementWTinylfu} element
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
__touch(element) {
|
|
213
|
+
this.__incrementSketch(element);
|
|
214
|
+
this.__onAccess(element);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Recency + promotion side of an access (no frequency increment).
|
|
219
|
+
* @param {CacheElementWTinylfu} element
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
__onAccess(element) {
|
|
223
|
+
switch (element.region) {
|
|
224
|
+
case CacheRegionType.Window:
|
|
225
|
+
this.#window.touch(element);
|
|
226
|
+
break;
|
|
227
|
+
case CacheRegionType.Trial:
|
|
228
|
+
this.__onTrialAccess(element)
|
|
229
|
+
break;
|
|
230
|
+
case CacheRegionType.Protected:
|
|
231
|
+
this.#protected.touch(element);
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
throw new Error ("Invalid region found");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Access of a Trial element: promote to Protected if hot enough and it fits, else bump recency.
|
|
239
|
+
* @param {CacheElementWTinylfu} element
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
__onTrialAccess(element) {
|
|
243
|
+
const trialRegion = this.#trial;
|
|
244
|
+
const protectedRegion = this.#protected;
|
|
245
|
+
|
|
246
|
+
if (this.__shouldPromoteToProtected(element)) {
|
|
247
|
+
trialRegion.remove(element);
|
|
248
|
+
|
|
249
|
+
element.region = CacheRegionType.Protected;
|
|
250
|
+
protectedRegion.add(element);
|
|
251
|
+
|
|
252
|
+
while (protectedRegion.wouldExceedCapacity())
|
|
253
|
+
{
|
|
254
|
+
const demoted = protectedRegion.findEvictionVictim();
|
|
255
|
+
protectedRegion.remove(demoted);
|
|
256
|
+
|
|
257
|
+
demoted.region = CacheRegionType.Trial;
|
|
258
|
+
trialRegion.add(demoted);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
trialRegion.touch(element);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Record an access to `element` in the frequency sketch (using its cached key hash).
|
|
268
|
+
* @param {CacheElementWTinylfu} element
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
__incrementSketch(element) {
|
|
272
|
+
this.#sketch.increment(element.keyHash);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Estimated access frequency of `element` from the sketch (using its cached key hash).
|
|
277
|
+
* @param {CacheElementWTinylfu} element
|
|
278
|
+
* @returns {number}
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
__frequency(element) {
|
|
282
|
+
return this.#sketch.frequency(element.keyHash);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Is `element` hot enough and small enough to be promoted from Trial to Protected?
|
|
287
|
+
* (frequency above threshold AND fits Protected — contract in {@link shouldPromoteToProtected})
|
|
288
|
+
* @param {CacheElementWTinylfu} element
|
|
289
|
+
* @returns {boolean}
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
__shouldPromoteToProtected(element) {
|
|
293
|
+
return shouldPromoteToProtected(this.__frequency(element), this.#promotionThreshold, element.weight, this.#protected.maxWeight);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The admission duel: does the candidate beat the victim on frequency?
|
|
298
|
+
* (contract in {@link winsAdmissionDuel})
|
|
299
|
+
* @param {CacheElementWTinylfu} candidate
|
|
300
|
+
* @param {CacheElementWTinylfu} victim
|
|
301
|
+
* @returns {boolean}
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
__admissionDuel(candidate, victim) {
|
|
305
|
+
return winsAdmissionDuel(this.__frequency(candidate), this.__frequency(victim));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Explicitly remove (invalidate) a key. Fires onRemoved, but not onEvicted (not capacity-driven).
|
|
310
|
+
* @param {Key} key
|
|
311
|
+
* @returns {boolean} true if the key was present and removed
|
|
312
|
+
*/
|
|
313
|
+
remove(key) {
|
|
314
|
+
const element = this.#data.get(key);
|
|
315
|
+
if (element === undefined) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.__evict(element, element.weight, false); // manual removal: onRemoved only, not onEvicted
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Remove all entries. Fires onRemoved for each (explicit removal, so not onEvicted).
|
|
325
|
+
* The frequency sketch is left intact (history decays on its own).
|
|
326
|
+
*/
|
|
327
|
+
clear() {
|
|
328
|
+
for (const element of this.#data.values()) {
|
|
329
|
+
this.onRemoved.send2(element.key, element.value);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.#data.clear();
|
|
333
|
+
this.#window.clear();
|
|
334
|
+
this.#trial.clear();
|
|
335
|
+
this.#protected.clear();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* An element's weight: keyWeigher(key) + valueWeigher(value). Asserts each is a non-negative number.
|
|
340
|
+
* @param {Key} key
|
|
341
|
+
* @param {Value} value
|
|
342
|
+
* @returns {number}
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
__computeElementWeight(key, value) {
|
|
346
|
+
const keyWeight = this.#keyWeigher(key);
|
|
347
|
+
|
|
348
|
+
assert.greaterThanOrEqual(keyWeight, 0, 'keyWeight');
|
|
349
|
+
|
|
350
|
+
const valueWeight = this.#valueWeigher(value);
|
|
351
|
+
|
|
352
|
+
assert.greaterThanOrEqual(valueWeight, 0, 'valueWeight');
|
|
353
|
+
|
|
354
|
+
return keyWeight + valueWeight;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Total weight Main (Trial + Protected) is allowed to hold.
|
|
359
|
+
* @returns {number}
|
|
360
|
+
* @private
|
|
361
|
+
*/
|
|
362
|
+
__mainCapacity() {
|
|
363
|
+
return mainCapacity(this.#maxWeight, this.#window.maxWeight);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Would Main exceed its budget once `extraWeight` is added, counting current occupancy?
|
|
368
|
+
* (contract in {@link exceedsMainWeight})
|
|
369
|
+
* @param {number} [extraWeight=0]
|
|
370
|
+
* @returns {boolean}
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
__exceedsMainWeight(extraWeight = 0) {
|
|
374
|
+
return exceedsMainWeight(this.#trial.weight, this.#protected.weight, extraWeight, this.__mainCapacity());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Could `weight` never fit Main, even with Trial fully evicted? (Protected's live weight is the
|
|
379
|
+
* immovable floor — contract in {@link cannotFitMain}.)
|
|
380
|
+
* @param {number} weight
|
|
381
|
+
* @returns {boolean}
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
__cannotFitMain(weight) {
|
|
385
|
+
return cannotFitMain(this.#protected.weight, weight, this.__mainCapacity());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Decide where an element of `weight` is routed on admission: Reject, Window, or Trial.
|
|
390
|
+
* (contract in {@link classifyAdmission})
|
|
391
|
+
* @param {number} weight
|
|
392
|
+
* @returns {number} one of AdmissionRoute
|
|
393
|
+
* @private
|
|
394
|
+
*/
|
|
395
|
+
__classifyAdmission(weight) {
|
|
396
|
+
return classifyAdmission(weight, this.#protected.weight, this.#window.maxWeight, this.__mainCapacity());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Demote a Protected element to Trial. Pure relocation — the caller restores Main's budget.
|
|
401
|
+
* @param {CacheElementWTinylfu} element
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
__demoteToTrial(element) {
|
|
405
|
+
this.#protected.remove(element);
|
|
406
|
+
element.region = CacheRegionType.Trial;
|
|
407
|
+
this.#trial.add(element);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Refit a grown Protected element
|
|
412
|
+
* @param {CacheElementWTinylfu} element
|
|
413
|
+
* @param {number} newWeight
|
|
414
|
+
* @param {number} delta
|
|
415
|
+
* @returns {boolean} whether `element` survived
|
|
416
|
+
* @private
|
|
417
|
+
*/
|
|
418
|
+
__refitGrownProtected(element, newWeight, delta) {
|
|
419
|
+
// Too big to ever fit Main, even alone → cannot keep it.
|
|
420
|
+
if (newWeight > this.__mainCapacity()) {
|
|
421
|
+
this.__evict(element);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
element.weight = newWeight;
|
|
426
|
+
this.#protected.adjustWeight(delta);
|
|
427
|
+
this.#protected.touch(element); // keep the updated element (protect from demotion below)
|
|
428
|
+
|
|
429
|
+
while (this.#protected.wouldExceedCapacity()) {
|
|
430
|
+
this.__demoteToTrial(this.#protected.findEvictionVictim());
|
|
431
|
+
}
|
|
432
|
+
while (this.__exceedsMainWeight()) {
|
|
433
|
+
this.__evict(this.__trialEvictionVictim());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Refit a grown Trial element
|
|
441
|
+
* @param {CacheElementWTinylfu} element
|
|
442
|
+
* @param {number} newWeight
|
|
443
|
+
* @param {number} delta
|
|
444
|
+
* @returns {boolean} whether `element` survived
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
__refitGrownTrial(element, newWeight, delta) {
|
|
448
|
+
if (this.__cannotFitMain(newWeight)) {
|
|
449
|
+
this.__evict(element);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
element.weight = newWeight;
|
|
454
|
+
this.#trial.adjustWeight(delta);
|
|
455
|
+
this.#trial.touch(element); // keep the updated element (protect from eviction below)
|
|
456
|
+
|
|
457
|
+
while (this.__exceedsMainWeight()) {
|
|
458
|
+
this.__evict(this.__trialEvictionVictim());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Refit a grown Window element
|
|
466
|
+
* @param {CacheElementWTinylfu} element
|
|
467
|
+
* @param {number} newWeight
|
|
468
|
+
* @param {number} delta
|
|
469
|
+
* @returns {boolean} whether `element` survived
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
__refitGrownWindow(element, newWeight, delta) {
|
|
473
|
+
const oldWeight = element.weight;
|
|
474
|
+
|
|
475
|
+
// Same placement decision as admission: the grown element either no longer fits Main (evict),
|
|
476
|
+
// outgrew Window (→ Trial), or still fits Window.
|
|
477
|
+
const route = this.__classifyAdmission(newWeight);
|
|
478
|
+
|
|
479
|
+
switch (route) {
|
|
480
|
+
case AdmissionRoute.REJECT:
|
|
481
|
+
this.__evict(element); // at oldWeight — weight not yet changed
|
|
482
|
+
return false;
|
|
483
|
+
case AdmissionRoute.TRIAL:
|
|
484
|
+
element.weight = newWeight;
|
|
485
|
+
return this.__nominateWindowToTrial(element, oldWeight);
|
|
486
|
+
case AdmissionRoute.WINDOW:
|
|
487
|
+
// keep it (MRU) and nominate older Window residents to Trial to absorb the growth
|
|
488
|
+
element.weight = newWeight;
|
|
489
|
+
this.#window.adjustWeight(delta);
|
|
490
|
+
this.#window.touch(element);
|
|
491
|
+
while (this.#window.wouldExceedCapacity()) {
|
|
492
|
+
this.__nominateWindowToTrial(this.#window.findEvictionVictim());
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Route an update to an existing key: shrink/same-weight applies in place, a growth dispatches to the
|
|
500
|
+
* region's refit handler. On survival, commits the new value and counts the access.
|
|
501
|
+
* @param {CacheElementWTinylfu} element
|
|
502
|
+
* @param {Value} newValue
|
|
503
|
+
* @private
|
|
504
|
+
*/
|
|
505
|
+
__routeUpdate(element, newValue) {
|
|
506
|
+
const newWeight = this.__computeElementWeight(element.key, newValue);
|
|
507
|
+
const delta = newWeight - element.weight;
|
|
508
|
+
|
|
509
|
+
// An update is an access — count its frequency up front, so it weighs into any
|
|
510
|
+
// admission duel the refit triggers.
|
|
511
|
+
this.__incrementSketch(element);
|
|
512
|
+
|
|
513
|
+
let survived;
|
|
514
|
+
if (delta <= 0) {
|
|
515
|
+
element.weight = newWeight;
|
|
516
|
+
this.__regionOf(element).adjustWeight(delta);
|
|
517
|
+
survived = true;
|
|
518
|
+
} else {
|
|
519
|
+
switch (element.region) {
|
|
520
|
+
case CacheRegionType.Window:
|
|
521
|
+
survived = this.__refitGrownWindow(element, newWeight, delta);
|
|
522
|
+
break;
|
|
523
|
+
case CacheRegionType.Trial:
|
|
524
|
+
survived = this.__refitGrownTrial(element, newWeight, delta);
|
|
525
|
+
break;
|
|
526
|
+
case CacheRegionType.Protected:
|
|
527
|
+
survived = this.__refitGrownProtected(element, newWeight, delta);
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
throw new Error("Invalid region found");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (survived) {
|
|
535
|
+
element.value = newValue; // commit payload after the element's place is secured
|
|
536
|
+
this.__onAccess(element); // recency + promotion (frequency already counted above)
|
|
537
|
+
this.onSet.send2(element.key, newValue); // value replaced
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Relocate a resident into Trial (already removed from its prior region by the caller).
|
|
543
|
+
* @param {CacheElementWTinylfu} movedElement
|
|
544
|
+
* @private
|
|
545
|
+
*/
|
|
546
|
+
__moveToTrial(movedElement) {
|
|
547
|
+
movedElement.region = CacheRegionType.Trial;
|
|
548
|
+
this.#trial.add(movedElement);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* First-time admission: place a new element in `region`, register it in the lookup map,
|
|
553
|
+
* and record its frequency.
|
|
554
|
+
* @param {CacheElementWTinylfu} element
|
|
555
|
+
* @param {CacheRegionType} region
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
__admit(element, region) {
|
|
559
|
+
element.region = region;
|
|
560
|
+
this.__regionOf(element).add(element);
|
|
561
|
+
this.#data.set(element.key, element);
|
|
562
|
+
this.#sketch.ensureCapacity(this.#data.size); // grow the sketch to track the live working set
|
|
563
|
+
this.__incrementSketch(element);
|
|
564
|
+
|
|
565
|
+
this.onSet.send2(element.key, element.value);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Remove an element from its region's list and the lookup map. Always fires onRemoved; also fires
|
|
570
|
+
* onEvicted when capacity-driven (`evicted`, the default), but not for manual removal.
|
|
571
|
+
* @param {CacheElementWTinylfu} element
|
|
572
|
+
* @param {number} [weight=element.weight]
|
|
573
|
+
* @param {boolean} [evicted=true] true for capacity eviction, false for explicit removal
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
__evict(element, weight = element.weight, evicted = true) {
|
|
577
|
+
this.__regionOf(element).remove(element, weight);
|
|
578
|
+
this.#data.delete(element.key);
|
|
579
|
+
|
|
580
|
+
this.onRemoved.send2(element.key, element.value);
|
|
581
|
+
if (evicted) {
|
|
582
|
+
this.onEvicted.send2(element.key, element.value);
|
|
583
|
+
}
|
|
584
|
+
this.#releaseElement(element); // fully detached now — return it to the reuse pool
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Return a fully-detached element to the reuse pool, clearing its references so it pins no user data.
|
|
589
|
+
* Caller MUST have already removed it from its region list and the lookup map, and fired any signals
|
|
590
|
+
* (which read key/value) — this nulls those out.
|
|
591
|
+
* @param {CacheElementWTinylfu} element
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
#releaseElement(element) {
|
|
595
|
+
element.key = null;
|
|
596
|
+
element.value = null;
|
|
597
|
+
element.next = null;
|
|
598
|
+
element.previous = null;
|
|
599
|
+
|
|
600
|
+
if (this.#elementPool.length < ELEMENT_POOL_MAX) {
|
|
601
|
+
this.#elementPool.push(element);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @param {CacheElementWTinylfu} element
|
|
607
|
+
* @returns {CacheRegionWTinylfu}
|
|
608
|
+
* @private
|
|
609
|
+
*/
|
|
610
|
+
__regionOf(element) {
|
|
611
|
+
switch (element.region) {
|
|
612
|
+
case CacheRegionType.Window: return this.#window;
|
|
613
|
+
case CacheRegionType.Trial: return this.#trial;
|
|
614
|
+
case CacheRegionType.Protected: return this.#protected;
|
|
615
|
+
default: throw new Error("Invalid region found");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Earn `element` a place in Trial: if Main is over budget, duel it against the Trial LRU victim and,
|
|
621
|
+
* on a win, evict Trial victims until it fits. Verdict only — the caller owns the element's data entry.
|
|
622
|
+
* @param {CacheElementWTinylfu} element
|
|
623
|
+
* @returns {boolean} true if `element` is admitted and space was made for it;
|
|
624
|
+
* false if it lost the admission duel.
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
__prepareTrialSpaceFor(element) {
|
|
628
|
+
if (!this.__exceedsMainWeight(element.weight)) {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!this.__admissionDuel(element, this.__trialEvictionVictim())) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// by design: the candidate duels only the Trial LRU victim; a win may then cascade several
|
|
637
|
+
// evictions when weighted (the candidate proved it beats the coldest resident — we commit).
|
|
638
|
+
while (this.__exceedsMainWeight(element.weight)) {
|
|
639
|
+
this.__evict(this.__trialEvictionVictim());
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @returns {CacheElementWTinylfu} Trial's LRU eviction victim
|
|
647
|
+
* @throws
|
|
648
|
+
* if Trial is empty while Main is over capacity (invariant: callers guarantee Trial holds enough evictable weight)
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
__trialEvictionVictim() {
|
|
652
|
+
const victim = this.#trial.findEvictionVictim();
|
|
653
|
+
if (!victim) {
|
|
654
|
+
throw new Error("Cache violation: no evictable victim in Trial while over capacity");
|
|
655
|
+
}
|
|
656
|
+
return victim;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Move a Window resident toward Trial through the admission duel; on a loss it is evicted
|
|
661
|
+
* (onRemoved + onEvicted).
|
|
662
|
+
* @param {CacheElementWTinylfu} candidate
|
|
663
|
+
* @param {number} [weight=candidate.weight] weight to reclaim from Window (override when it has changed)
|
|
664
|
+
* @returns {boolean} whether the candidate survived into Trial
|
|
665
|
+
* @private
|
|
666
|
+
*/
|
|
667
|
+
__nominateWindowToTrial(candidate, weight = candidate.weight) {
|
|
668
|
+
this.#window.remove(candidate, weight);
|
|
669
|
+
|
|
670
|
+
if (this.__prepareTrialSpaceFor(candidate)) {
|
|
671
|
+
this.__moveToTrial(candidate);
|
|
672
|
+
return true;
|
|
673
|
+
} else {
|
|
674
|
+
// lost the duel — a resident is evicted
|
|
675
|
+
this.#data.delete(candidate.key);
|
|
676
|
+
this.onRemoved.send2(candidate.key, candidate.value);
|
|
677
|
+
this.onEvicted.send2(candidate.key, candidate.value);
|
|
678
|
+
this.#releaseElement(candidate);
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Evict Window's LRU residents (nominating each to Trial) until `incomingWeight` fits.
|
|
685
|
+
* @param {number} incomingWeight
|
|
686
|
+
* @private
|
|
687
|
+
*/
|
|
688
|
+
__makeRoomInWindow(incomingWeight) {
|
|
689
|
+
while (this.#window.wouldExceedCapacity(incomingWeight)) {
|
|
690
|
+
const victim = this.#window.findEvictionVictim();
|
|
691
|
+
if (!victim) {
|
|
692
|
+
// invariant: over capacity implies at least one resident
|
|
693
|
+
throw new Error("Cache violation: no evictable victim in Window while over capacity");
|
|
694
|
+
}
|
|
695
|
+
this.__nominateWindowToTrial(victim);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Admit a new element that is too big for Window straight into the Trial duel; on a loss it is
|
|
701
|
+
* rejected (onRejected) — it was never stored.
|
|
702
|
+
* @param {CacheElementWTinylfu} newElement
|
|
703
|
+
* @private
|
|
704
|
+
*/
|
|
705
|
+
__nominateDirectToTrial(newElement) {
|
|
706
|
+
if (this.__prepareTrialSpaceFor(newElement)) {
|
|
707
|
+
this.__admit(newElement, CacheRegionType.Trial);
|
|
708
|
+
} else {
|
|
709
|
+
// lost the duel — newcomer never admitted
|
|
710
|
+
this.onRejected.send2(newElement.key, newElement.value);
|
|
711
|
+
this.#releaseElement(newElement);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Route a brand-new element per classifyAdmission: reject outright, insert into Window, or send it to
|
|
717
|
+
* the Trial duel.
|
|
718
|
+
* @param {CacheElementWTinylfu} newElement
|
|
719
|
+
* @private
|
|
720
|
+
*/
|
|
721
|
+
__routeAdmission(newElement) {
|
|
722
|
+
const weight = newElement.weight;
|
|
723
|
+
|
|
724
|
+
// Decision: where does this element go? (see classifyAdmission — TRIAL is a lower bar than
|
|
725
|
+
// Protected; an element too big for Protected is still a valid, pinned Trial resident.)
|
|
726
|
+
const route = this.__classifyAdmission(weight);
|
|
727
|
+
|
|
728
|
+
switch (route) {
|
|
729
|
+
case AdmissionRoute.REJECT:
|
|
730
|
+
this.onRejected.send2(newElement.key, newElement.value); // could never fit Main
|
|
731
|
+
return;
|
|
732
|
+
case AdmissionRoute.TRIAL:
|
|
733
|
+
this.__nominateDirectToTrial(newElement);
|
|
734
|
+
break;
|
|
735
|
+
case AdmissionRoute.WINDOW:
|
|
736
|
+
this.__makeRoomInWindow(weight);
|
|
737
|
+
this.__admit(newElement, CacheRegionType.Window);
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Insert a new entry, or update the value of an existing key. The main write entry point.
|
|
744
|
+
* @param {Key} key
|
|
745
|
+
* @param {Value} value
|
|
746
|
+
*/
|
|
747
|
+
put(key, value){
|
|
748
|
+
let element = this.#data.get(key);
|
|
749
|
+
|
|
750
|
+
if (element !== undefined) {
|
|
751
|
+
this.__routeUpdate(element, value);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Reuse an evicted element if one is pooled, else allocate. Both arrive with next/previous null.
|
|
756
|
+
const pool = this.#elementPool;
|
|
757
|
+
element = pool.length > 0 ? pool.pop() : new CacheElementWTinylfu();
|
|
758
|
+
element.key = key;
|
|
759
|
+
element.keyHash = this.#keyHashFunction(key) | 0; // hash once; `| 0` keeps it an inline SMI (no boxing)
|
|
760
|
+
element.value = value;
|
|
761
|
+
element.region = CacheRegionType.Window;
|
|
762
|
+
element.weight = this.__computeElementWeight(key, value);
|
|
763
|
+
|
|
764
|
+
this.__routeAdmission(element);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Look up a key. A hit counts as an access (recency + frequency, and may trigger promotion).
|
|
769
|
+
* @param {Key} key
|
|
770
|
+
* @returns {Value|null} the cached value, or null if not found
|
|
771
|
+
*/
|
|
772
|
+
get(key) {
|
|
773
|
+
const element = this.#data.get(key);
|
|
774
|
+
|
|
775
|
+
if (element === undefined) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
this.__touch(element); // a read is an access: frequency + recency + promotion
|
|
780
|
+
return element.value;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── diagnostics (inspection only; not part of the cache contract) ──
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* @internal Diagnostic for tests/monitoring: which region currently holds `key`, or null if absent.
|
|
787
|
+
* Returns the immutable {@link CacheRegionType} — it does NOT expose the element itself.
|
|
788
|
+
* @param {Key} key
|
|
789
|
+
* @returns {CacheRegionType|null}
|
|
790
|
+
*/
|
|
791
|
+
inspectRegion(key) {
|
|
792
|
+
const element = this.#data.get(key);
|
|
793
|
+
return element === undefined ? null : element.region;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @internal Diagnostic for tests/monitoring: the live weight currently held by each region.
|
|
798
|
+
* @returns {{window: number, trial: number, protected: number}}
|
|
799
|
+
*/
|
|
800
|
+
inspectWeights() {
|
|
801
|
+
return {
|
|
802
|
+
window: this.#window.weight,
|
|
803
|
+
trial: this.#trial.weight,
|
|
804
|
+
protected: this.#protected.weight,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|