1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
//! Count-min frequency sketch keyed directly on BLAKE3 hashes (ADR 040).
//!
//! Keys are already uniform 32-byte hashes, so each row's column index is a
//! disjoint 4-byte slice of the key reduced mod the sketch's width — no hash
//! functions. Counters are saturating `u8` — classic `TinyLFU` uses 4-bit
//! counters, and the extra headroom is free at one byte each. Halving ages them.
//!
//! [`ShardedCountMinSketch`] is the form the estimator uses. It splits the
//! counter array into [`SHARDS`] independently locked [`CountMinSketch`]es of
//! `cols / SHARDS` columns each, routed by a 4-byte slice of the key that no
//! row index reads. An `increment` or an `estimate` locks exactly one shard.
//!
//! Two properties of one wide sketch of `cols` columns are load-bearing for the
//! policies that read the estimates, and sharding keeps both:
//!
//! - **Aging cadence is global, not per shard.** Both readers compare estimates
//! against a shared scale: [`super::tinylfu::ProbationAdmission`] tests one
//! estimate against a node-wide `promotion_threshold`, and
//! [`super::tinylfu::TinyLfuEviction`] ranks candidates from *different*
//! shards least-frequent-first. Counters are only comparable if every counter
//! is caught up to the same clock when it is read. So the halving clock is one
//! node-wide observation counter, and a shard halves lazily — on its next
//! touch — by however many windows have elapsed since it last caught up. The
//! *work* stays sharded (`1 / SHARDS` of the array under one shard's lock,
//! landing on one observer), while the *cadence* stays global. A shard clocked
//! on its own traffic would age at a rate set by key skew, which is exactly
//! what a frequency sketch must not do.
//! - **Per-row collision rate.** Two distinct keys share a row counter only if
//! they share a shard (probability `1 / SHARDS`) and then a column within it
//! (`SHARDS / cols`) — the same `1 / cols` a single wide sketch gives, because
//! the shard slice and the row slices are disjoint bytes of a uniform hash.
//!
//! The over-report rate carries over as exactly as the per-row rate does. The
//! sketch reads a key hotter than it is when every one of its `ROWS` counters
//! also holds some other key's count. The polluting keys need not be the same
//! one across rows, so "two keys collide in all `ROWS` rows" is a far rarer
//! event that does not bound the error — do not size the sketch from it. For
//! `N` live keys the rate is `(1 - e^(-N / cols))^ROWS`, and [`SHARDS`] cancels
//! out: a shard divides the columns and the keys in the same proportion. So
//! sharding costs nothing in accuracy and the width alone sets it. The rate
//! turns on `N / cols`, which holds the shipped `cols = 65536` under one
//! percent out to roughly 25000 live keys. See
//! [`super::tinylfu::TinyLfuEstimator::new`] for the sizing and its floor.
use crateHash;
use ;
use ;
const ROWS: usize = 4;
/// Number of independently locked shards in a [`ShardedCountMinSketch`].
///
/// A concurrency knob: the total counter budget, the per-row collision rate, and
/// the aging cadence are all held constant across it (see the module docs), so
/// raising or lowering it moves contention and halving granularity without
/// moving any admission or eviction decision. The one quantity it does move is
/// the whole-sketch over-report rate, by `SHARDS^(ROWS-1)` — negligible at the
/// shipped sizing, and bounded in the module docs.
pub const SHARDS: usize = 16;
/// Bytes of key each index consumes: one `u32` per row, and one for the shard.
const SLICE_BYTES: usize = 4;
/// Byte offset of the shard-routing slice. It is derived from the bytes the row
/// indices consume, so shard choice and column choice stay independent functions
/// of the key however many rows the sketch has. The module docs' collision-rate
/// argument rests on that independence, and a stride or row count that put a row
/// index on the shard slice would correlate the two while leaving every test in
/// this file green — hence the derivation rather than a literal offset.
const SHARD_SLICE_START: usize = ROWS * SLICE_BYTES;
// The derivation keeps the slices disjoint; this bounds them to the key. Past
// `ROWS = 7` the shard slice runs off the end of a hash, `word_at` reads zero
// for every key, and the sharding collapses to one shard.
const _: = assert!;
/// Observations per node-wide aging window, per column of total width. A sketch
/// `cols` wide halves every `cols * AGING_WINDOW_PER_COL` observations.
const AGING_WINDOW_PER_COL: u64 = 10;
/// Read the little-endian word of [`SLICE_BYTES`] at `start` in `key`, or `0` if
/// the key is shorter than the slice (unreachable for a 32-byte BLAKE3 hash, but expressed
/// as a total function rather than an index that could panic).
/// A plain count-min counter array. It carries no aging clock of its own:
/// [`ShardedCountMinSketch`] owns the node-wide clock and calls [`Self::halve`],
/// which is what keeps every shard on one cadence.
/// One shard's counters plus the aging epoch they have already been caught up
/// to. `epoch` trails the sketch's node-wide epoch until the shard is next
/// touched; [`ShardedCountMinSketch::catch_up`] closes the gap.
/// A [`CountMinSketch`] split into [`SHARDS`] independently locked pieces on one
/// node-wide aging clock.
///
/// Every counter a given key touches lives in one shard, so an `increment` or an
/// `estimate` takes exactly one shard lock and reads or writes `ROWS` counters.
/// Halving is per shard and lazy: the shard is aged on its next touch by the
/// number of node-wide windows that have elapsed since it last caught up. So the
/// work is `1 / SHARDS` of the array under one lock, while every counter is
/// caught up to the node-wide clock at the moment it is read. A ranking pass
/// reads each candidate separately, so it carries one halving of skew for every
/// window boundary it crosses. A halving applied evenly across a pass cannot
/// invert the ranking — it only creates ties — but candidates read on opposite
/// sides of a boundary can misorder, and two whose estimates are within a
/// factor of two can swap. The window is `cols * 10` observations, so a sweep
/// crossing even one boundary is already the uncommon case, and the next sweep
/// re-reads both on one side of it.
///
/// The interior mutability is deliberate — the frequency estimator is shared by
/// every serve completion and every fill-path admission read, so both take
/// `&self` and neither can serialize on a single process-wide lock.