Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/resource_sketch.rs
Line
Count
Source
1
// Copyright 2026 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
//! What each kind of action used, learned by the scheduler from the usage
16
//! every attempt reports, and the reservation it shapes from that.
17
//!
18
//! The sketch is bounded by construction: a fixed-size entry per key, three
19
//! LRU families with capacities from a byte budget, and a window after which
20
//! a key not seen again is forgotten. A sample counts for between half a
21
//! window and a whole one (two half-window histograms per entry), so what a
22
//! key used last week stops shaping it, and a kill floor expires with the
23
//! window. The families are split over
24
//! [`SHARDS`] by key hash so a snapshot or an age-out never holds a lock the
25
//! Execute path waits on for long. Nothing on the Execute path reads a file
26
//! or parses JSON; a lookup is a hash probe and a 25-bucket scan.
27
28
use core::hash::{BuildHasher, BuildHasherDefault};
29
use core::num::NonZeroUsize;
30
use core::time::Duration;
31
use std::hash::DefaultHasher;
32
use std::sync::Arc;
33
34
use lru::LruCache;
35
use nativelink_config::schedulers::SizeClassSpec;
36
use nativelink_error::{Code, Error, make_err};
37
use parking_lot::Mutex;
38
39
/// Buckets of the log2 histograms, the rollup's `hist_bucket`: bucket 0 for
40
/// values under 1000, bucket `i` for `1000 * 2^(i-1) <= v < 1000 * 2^i`.
41
pub const HIST_BUCKETS: usize = 25;
42
43
/// Bumped by hand when the worker's sampler changes what a measurement
44
/// means (the process-group fix of v1.8.2 is the example). A snapshot of
45
/// another generation is dropped at load and the sketch starts empty.
46
pub const SAMPLER_GENERATION: u32 = 1;
47
48
/// Fixed charge for an entry and its map/Arc bookkeeping. Key contents are
49
/// charged separately. The allowance also covers its largest encoded form.
50
pub const ENTRY_BYTES: u64 = (size_of::<Entry>() + size_of::<SketchKey>() + 160 + 256) as u64;
51
/// Conservative allowance for a probation node, its Arc header and hash slot.
52
pub const PROBATION_ENTRY_BYTES: u64 = 256;
53
/// Reserved hash-table storage per configured slot, even when the LRU is empty.
54
const HASH_SLOT_BYTES: u64 = 64;
55
/// The maps, mutexes, sketch and two sentinel nodes allocated by each LRU,
56
/// even when empty. Hash slots allow for table rounding and spare capacity.
57
const FIXED_BYTES: u64 = (SHARDS
58
    * (size_of::<Mutex<Inner>>()
59
        + 6 * (size_of::<Entry>() + size_of::<SketchKey>() + 32)
60
        + 2 * 128)
61
    + size_of::<ResourceSketch>()
62
    + 1024) as u64;
63
64
/// Shards the families are split over by key hash. A lookup or a usage
65
/// report locks one; a snapshot and an age-out lock one at a time, so at
66
/// the full budget neither holds a lock the Execute path waits on for more
67
/// than a shard's worth of entries.
68
pub const SHARDS: usize = 32;
69
70
/// Default of `memory_budget_bytes`: about 300k keys.
71
pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 128 << 20;
72
73
/// Default of `window_days`.
74
pub const DEFAULT_WINDOW_DAYS: u64 = 7;
75
76
/// Default of `headroom_percent`, the chart's `headroomPercent`.
77
pub const DEFAULT_HEADROOM_PERCENT: u64 = 20;
78
79
/// What a measurement is filed under, chosen from what the client sent.
80
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
81
pub enum SketchKey {
82
    /// The client named a target; `mnemonic` is `None` when it named no
83
    /// mnemonic, which is its own key.
84
    Target {
85
        target: Arc<str>,
86
        mnemonic: Option<Arc<str>>,
87
    },
88
    /// The client named a mnemonic and no target.
89
    Mnemonic { mnemonic: Arc<str> },
90
    /// The client named neither. The `Command` digest as `hash-size`: an
91
    /// identity every client has, and the same program with the same
92
    /// arguments regardless of its inputs, so one key gathers every run.
93
    Command { digest: Arc<str> },
94
}
95
96
impl SketchKey {
97
    /// The key for an attempt: the target when the client sent one, else
98
    /// the mnemonic, else the command digest. Empty strings count as absent.
99
40.2k
    pub fn for_attempt(target: Option<&str>, mnemonic: Option<&str>, command_digest: &str) -> Self {
100
40.2k
        let target = target.filter(|s| !
s35.2k
.
is_empty35.2k
());
101
40.2k
        let mnemonic = mnemonic.filter(|s| !
s35.2k
.
is_empty35.2k
());
102
40.2k
        match (target, mnemonic) {
103
35.2k
            (Some(target), mnemonic) => Self::Target {
104
35.2k
                target: Arc::from(target),
105
35.2k
                mnemonic: mnemonic.map(Arc::from),
106
35.2k
            },
107
3
            (None, Some(mnemonic)) => Self::Mnemonic {
108
3
                mnemonic: Arc::from(mnemonic),
109
3
            },
110
5.01k
            (None, None) => Self::Command {
111
5.01k
                digest: Arc::from(command_digest),
112
5.01k
            },
113
        }
114
40.2k
    }
115
116
2.12M
    pub const fn family(&self) -> Family {
117
2.12M
        match self {
118
1.08M
            Self::Target { .. } => Family::Target,
119
1.03M
            Self::Mnemonic { .. } => Family::Mnemonic,
120
5.04k
            Self::Command { .. } => Family::Command,
121
        }
122
2.12M
    }
123
}
124
125
/// The three maps; each has its own capacity so a flood of nameless
126
/// actions cannot evict a single target key.
127
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128
pub enum Family {
129
    Target,
130
    Mnemonic,
131
    Command,
132
}
133
134
impl Family {
135
    pub const ALL: [Self; 3] = [Self::Target, Self::Mnemonic, Self::Command];
136
137
0
    pub const fn as_str(self) -> &'static str {
138
0
        match self {
139
0
            Self::Target => "target",
140
0
            Self::Mnemonic => "mnemonic",
141
0
            Self::Command => "command",
142
        }
143
0
    }
144
145
4.18M
    const fn index(self) -> usize {
146
4.18M
        match self {
147
2.11M
            Self::Target => 0,
148
2.06M
            Self::Mnemonic => 1,
149
5.71k
            Self::Command => 2,
150
        }
151
4.18M
    }
152
}
153
154
/// A log2 histogram with the rollup's buckets, so a snapshot and a rollup
155
/// row say the same thing about the same samples. Counts saturate at
156
/// `u64::MAX` is only a defensive limit; ordinary workloads must not saturate
157
/// individual buckets and change their relative weights.
158
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
159
pub struct Hist(pub [u64; HIST_BUCKETS]);
160
161
impl Hist {
162
    /// The bucket a value falls in.
163
2.07M
    pub fn bucket(value: u64) -> usize {
164
2.07M
        if value < 1000 {
165
14
            return 0;
166
2.07M
        }
167
2.07M
        let exponent = usize::try_from((value / 1000).ilog2()).unwrap_or(HIST_BUCKETS) + 1;
168
2.07M
        exponent.min(HIST_BUCKETS - 1)
169
2.07M
    }
170
171
2.07M
    pub fn add(&mut self, value: u64) {
172
2.07M
        let bucket = &mut self.0[Self::bucket(value)];
173
2.07M
        *bucket = bucket.saturating_add(1);
174
2.07M
    }
175
176
39
    pub fn total(&self) -> u64 {
177
975
        
self.039
.
iter39
().
fold39
(0u64, |sum, &n| sum.saturating_add(n))
178
39
    }
179
180
    /// Upper bound of the bucket the 95th percentile sample falls in; zero
181
    /// when empty. The upper bound over-reserves by less than 2x against
182
    /// the true p95; a lower edge or a midpoint could under-reserve, which
183
    /// a hint must never do.
184
4
    pub fn p95_upper(&self) -> u64 {
185
4
        self.p95_upper_with(&Self::default())
186
4
    }
187
188
    /// The same over the samples of `self` and `other` together.
189
141
    pub fn p95_upper_with(&self, other: &Self) -> u64 {
190
141
        let total: u128 = self
191
141
            .0
192
141
            .iter()
193
141
            .chain(other.0.iter())
194
7.05k
            .
map141
(|&n| u128::from(n))
195
141
            .sum();
196
141
        if total == 0 {
197
79
            return 0;
198
62
        }
199
62
        let target = (total * 95).div_ceil(100);
200
62
        let mut seen = 0u128;
201
527
        for (i, (&a, &b)) in 
self.062
.
iter62
().
zip62
(
other.062
.
iter62
()).
enumerate62
() {
202
527
            seen += u128::from(a) + u128::from(b);
203
527
            if seen >= target {
204
62
                return 1000u64 << i;
205
465
            }
206
        }
207
0
        0
208
141
    }
209
}
210
211
/// The samples of one half-window: histograms and the exact maxima.
212
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
213
pub struct Half {
214
    /// `peak_memory_kb` of every sampled attempt.
215
    pub memory: Hist,
216
    /// Millicores: `cpu_time_ms * 1000 / wall_time_ms`.
217
    pub cpu: Hist,
218
    /// `peak_disk_kb`; zero until workers measure it.
219
    pub disk: Hist,
220
    pub max_memory_kb: u64,
221
    pub max_cpu_millicores: u64,
222
    pub max_disk_kb: u64,
223
}
224
225
/// What the sketch knows about one key.
226
#[derive(Clone, Debug, Default, PartialEq, Eq)]
227
pub struct Entry {
228
    pub samples: u32,
229
    /// The samples of the current half-window and of the one before it. A
230
    /// lookup reads both, so a sample counts for between half a window and
231
    /// a whole one and never longer: what a key used last week stops
232
    /// shaping it, however often the key runs.
233
    pub current: Half,
234
    pub previous: Half,
235
    /// The largest reservation an attempt was killed at for memory within
236
    /// the window. The next hint sits above it.
237
    pub killed_at_kb: u64,
238
    /// When that kill was seen; the floor expires a window after it.
239
    pub killed_at_s: u64,
240
    /// Seconds since the Unix epoch of the last sample, for the window.
241
    pub last_seen_s: u64,
242
    /// The half-window `current` counts: `now_s / (window_s / 2)`; see
243
    /// [`Entry::settle`].
244
    pub period: u64,
245
    /// Set by the hints-file seed; replaced whole by the first real sample.
246
    pub seeded: bool,
247
}
248
249
/// One attempt's measurement as the sketch takes it.
250
#[derive(Clone, Copy, Debug, Default)]
251
pub struct Observation {
252
    pub peak_memory_kb: u64,
253
    /// `None` when the worker reported no wall time; `Some(0)` is a real
254
    /// measurement of no CPU and is kept (the reservation it ran under is
255
    /// not a measurement).
256
    pub cpu_millicores: Option<u64>,
257
    pub peak_disk_kb: u64,
258
    /// The memory the attempt was killed at, when it was killed for memory
259
    /// and the reservation is known. A kill at an unknown reservation
260
    /// records no floor: a floor of zero is no floor.
261
    pub killed_at_kb: Option<u64>,
262
    /// Any kill, for the probation rule.
263
    pub killed: bool,
264
    pub now_s: u64,
265
}
266
267
/// A reservation shaped from an entry. Zero is a dimension not stated.
268
#[derive(Clone, Debug, Default, PartialEq, Eq)]
269
pub struct Shaped {
270
    /// In the unit of `cpu_property_name`, millicores on the chart.
271
    pub cpu_count: u64,
272
    pub memory_kb: u64,
273
    pub disk_kb: u64,
274
    pub class: Option<String>,
275
}
276
277
impl Shaped {
278
15
    pub const fn is_empty(&self) -> bool {
279
15
        self.cpu_count == 0 && 
self.memory_kb == 06
&&
self.disk_kb == 02
280
15
    }
281
}
282
283
/// The numbers `ShapeOne` takes today, from the scheduler's own spec.
284
#[derive(Clone, Debug)]
285
pub struct Policy {
286
    pub headroom_percent: u64,
287
    pub classes: Vec<SizeClassSpec>,
288
    /// Which dimensions the fleet reserves: a number on one it does not
289
    /// would be a minimum no worker advertises.
290
    pub reserve_cpu: bool,
291
    pub reserve_memory: bool,
292
    pub reserve_disk: bool,
293
}
294
295
/// Reserve three quarters for snapshot encoding/loading and transient storage.
296
/// Of the resident quarter, 10% funds probation and the remaining
297
/// entries split 80/10/10. Variable-length keys consume the same byte budget.
298
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299
pub struct Budget {
300
    pub target: usize,
301
    pub mnemonic: usize,
302
    pub command: usize,
303
    pub probation: usize,
304
}
305
306
impl Budget {
307
45
    pub fn from_bytes(bytes: u64) -> Self {
308
45
        let resident = bytes.saturating_sub(FIXED_BYTES) / 4;
309
45
        let probation = resident / 10;
310
45
        let entries = usize::try_from((resident - probation) / ENTRY_BYTES).unwrap_or(usize::MAX);
311
45
        let tenth = entries / 10;
312
45
        Self {
313
45
            target: (entries - 2 * tenth).max(1),
314
45
            mnemonic: tenth.max(1),
315
45
            command: tenth.max(1),
316
45
            probation: usize::try_from(probation / PROBATION_ENTRY_BYTES)
317
45
                .unwrap_or(usize::MAX)
318
45
                .max(1),
319
45
        }
320
45
    }
321
}
322
323
/// Counters the admin view and the metrics read.
324
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
325
pub struct Stats {
326
    pub entries: [usize; 3],
327
    pub capacity: [usize; 3],
328
    pub evictions: [u64; 3],
329
    pub samples: [u64; 3],
330
    pub probation: usize,
331
}
332
333
struct Inner {
334
    families: [LruCache<SketchKey, Entry>; 3],
335
    probation: LruCache<Arc<str>, ()>,
336
    evictions: [u64; 3],
337
    samples: [u64; 3],
338
    bytes: [u64; 3],
339
    probation_bytes: u64,
340
}
341
342
40.2k
fn key_bytes(key: &SketchKey) -> u64 {
343
40.2k
    match key {
344
40.2k
        SketchKey::Target { target, mnemonic } => {
345
40.2k
            (target.len() + mnemonic.as_ref().map_or(0, |m| 
m40.2k
.
len40.2k
())) as u64
346
        }
347
51
        SketchKey::Mnemonic { mnemonic } => mnemonic.len() as u64,
348
9
        SketchKey::Command { digest } => digest.len() as u64,
349
    }
350
40.2k
}
351
352
40.2k
fn entry_charge(key: &SketchKey) -> u64 {
353
40.2k
    ENTRY_BYTES - HASH_SLOT_BYTES - 256 + key_bytes(key)
354
40.2k
}
355
356
9.93k
const fn probation_charge(digest: &str) -> u64 {
357
9.93k
    PROBATION_ENTRY_BYTES - HASH_SLOT_BYTES - 128 + digest.len() as u64
358
9.93k
}
359
360
impl Inner {
361
35.3k
    fn insert(&mut self, key: SketchKey, entry: Entry) -> bool {
362
35.3k
        let i = key.family().index();
363
35.3k
        let map = &mut self.families[i];
364
35.3k
        let allowance = map.cap().get() as u64 * (ENTRY_BYTES - HASH_SLOT_BYTES);
365
35.3k
        let charge = entry_charge(&key);
366
35.3k
        if charge > allowance {
367
1
            return false;
368
35.3k
        }
369
40.2k
        while self.bytes[i] + charge > allowance || 
map35.3k
.
len35.3k
() ==
map35.3k
.cap().get() {
370
4.96k
            let Some((removed, _)) = map.pop_lru() else {
371
0
                break;
372
            };
373
4.96k
            self.bytes[i] -= entry_charge(&removed);
374
4.96k
            self.evictions[i] += 1;
375
        }
376
35.3k
        map.put(key, entry);
377
35.3k
        self.bytes[i] += charge;
378
35.3k
        true
379
35.3k
    }
380
381
5.00k
    fn remember_probation(&mut self, digest: Arc<str>) {
382
5.00k
        let allowance =
383
5.00k
            self.probation.cap().get() as u64 * (PROBATION_ENTRY_BYTES - HASH_SLOT_BYTES);
384
5.00k
        let charge = probation_charge(&digest);
385
5.00k
        if charge > allowance {
386
0
            return;
387
5.00k
        }
388
9.92k
        while self.probation_bytes + charge > allowance
389
9.92k
            || self.probation.len() == self.probation.cap().get()
390
        {
391
4.92k
            let Some((removed, ())) = self.probation.pop_lru() else {
392
0
                break;
393
            };
394
4.92k
            self.probation_bytes -= probation_charge(&removed);
395
        }
396
5.00k
        self.probation.put(digest, ());
397
5.00k
        self.probation_bytes += charge;
398
5.00k
    }
399
}
400
401
/// The sketch: three LRU families per shard, each shard under its own lock
402
/// held for O(1) work or one shard's copy, never across an await.
403
pub struct ResourceSketch {
404
    shards: Box<[Mutex<Inner>]>,
405
    hasher: BuildHasherDefault<DefaultHasher>,
406
    policy: Policy,
407
    window: Duration,
408
    budget: Budget,
409
}
410
411
impl core::fmt::Debug for ResourceSketch {
412
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
413
0
        f.debug_struct("ResourceSketch")
414
0
            .field("budget", &self.budget)
415
0
            .field("window", &self.window)
416
0
            .field("shards", &self.shards.len())
417
0
            .field("stats", &self.stats())
418
0
            .finish_non_exhaustive()
419
0
    }
420
}
421
422
4.11k
fn cache(capacity: usize) -> LruCache<SketchKey, Entry> {
423
4.11k
    LruCache::new(NonZeroUsize::new(capacity.max(1)).unwrap_or(NonZeroUsize::MIN))
424
4.11k
}
425
426
impl ResourceSketch {
427
50
    pub fn new(policy: Policy, budget: Budget, window: Duration) -> Self {
428
50
        Self::with_shards(policy, budget, window, SHARDS)
429
50
    }
430
431
    /// `shards` is [`SHARDS`] in the scheduler; a test that wants one LRU
432
    /// order passes one. A family's capacity is spread over the shards by
433
    /// key hash, so it evicts a little before the whole budget is full.
434
66
    pub fn with_shards(policy: Policy, budget: Budget, window: Duration, shards: usize) -> Self {
435
66
        let shards = shards.clamp(1, SHARDS).min(
436
66
            budget
437
66
                .target
438
66
                .min(budget.mnemonic)
439
66
                .min(budget.command)
440
66
                .min(budget.probation)
441
66
                .max(1),
442
        );
443
5.48k
        let 
per66
= |capacity: usize, shard: usize| {
444
5.48k
            (capacity / shards + usize::from(shard < capacity % shards)).max(1)
445
5.48k
        };
446
66
        let inners = (0..shards)
447
1.37k
            .
map66
(|shard| {
448
1.37k
                Mutex::new(Inner {
449
1.37k
                    families: [
450
1.37k
                        cache(per(budget.target, shard)),
451
1.37k
                        cache(per(budget.mnemonic, shard)),
452
1.37k
                        cache(per(budget.command, shard)),
453
1.37k
                    ],
454
1.37k
                    probation: LruCache::new(
455
1.37k
                        NonZeroUsize::new(per(budget.probation, shard))
456
1.37k
                            .unwrap_or(NonZeroUsize::MIN),
457
1.37k
                    ),
458
1.37k
                    evictions: [0; 3],
459
1.37k
                    samples: [0; 3],
460
1.37k
                    bytes: [0; 3],
461
1.37k
                    probation_bytes: 0,
462
1.37k
                })
463
1.37k
            })
464
66
            .collect();
465
66
        Self {
466
66
            shards: inners,
467
66
            hasher: BuildHasherDefault::default(),
468
66
            policy,
469
66
            window,
470
66
            budget,
471
66
        }
472
66
    }
473
474
2.08M
    fn shard(&self, key: &SketchKey) -> &Mutex<Inner> {
475
2.08M
        let hash = self.hasher.hash_one(key);
476
2.08M
        let index = usize::try_from(hash % self.shards.len() as u64).unwrap_or(0);
477
2.08M
        &self.shards[index]
478
2.08M
    }
479
480
2.07M
    const fn window_s(&self) -> u64 {
481
2.07M
        self.window.as_secs()
482
2.07M
    }
483
484
0
    pub const fn policy(&self) -> &Policy {
485
0
        &self.policy
486
0
    }
487
488
16
    pub const fn budget(&self) -> Budget {
489
16
        self.budget
490
16
    }
491
492
0
    pub const fn window(&self) -> Duration {
493
0
        self.window
494
0
    }
495
496
    /// Records one attempt. A command key with no entry yet earns one on a
497
    /// kill or on its second sighting: a digest that ran once and was not
498
    /// killed is an action-cache hit next time, so a first sighting only
499
    /// goes on probation. An attempt that names a target and a mnemonic is
500
    /// recorded under both its target key and the mnemonic family, so a
501
    /// target the sketch has not seen is sized from its kind of action
502
    /// rather than the cold start. Returns whether a sample was recorded.
503
1.03M
    pub fn observe(&self, key: SketchKey, observation: &Observation) -> bool {
504
1.03M
        let mnemonic = match 
&key1.03M
{
505
            SketchKey::Target {
506
1.03M
                mnemonic: Some(mnemonic),
507
                ..
508
1.03M
            } => Some(mnemonic.clone()),
509
5.01k
            _ => None,
510
        };
511
1.03M
        let recorded = self.observe_one(key, observation);
512
1.03M
        if recorded && let Some(
mnemonic1.03M
) =
mnemonic1.03M
{
513
1.03M
            self.observe_one(SketchKey::Mnemonic { mnemonic }, observation);
514
1.03M
        
}5.01k
515
1.03M
        recorded
516
1.03M
    }
517
518
2.06M
    fn observe_one(&self, key: SketchKey, observation: &Observation) -> bool {
519
2.06M
        let family = key.family();
520
2.06M
        let window_s = self.window_s();
521
2.06M
        let mut inner = self.shard(&key).lock();
522
2.06M
        if family == Family::Command
523
5.00k
            && !inner.families[family.index()].contains(&key)
524
5.00k
            && !observation.killed
525
        {
526
5.00k
            let SketchKey::Command { digest } = &key else {
527
0
                unreachable!("family Command is a Command key");
528
            };
529
5.00k
            if inner.probation.pop(digest).is_some() {
530
2
                inner.probation_bytes -= probation_charge(digest);
531
2
            } else {
532
5.00k
                inner.remember_probation(digest.clone());
533
5.00k
                return false;
534
            }
535
2.06M
        }
536
2.06M
        inner.samples[family.index()] += 1;
537
2.06M
        let map = &mut inner.families[family.index()];
538
2.06M
        if let Some(
entry2.03M
) = map.get_mut(&key) {
539
2.03M
            entry.record(observation, window_s);
540
2.03M
            return true;
541
25.0k
        }
542
25.0k
        let mut entry = Entry::default();
543
25.0k
        entry.record(observation, window_s);
544
25.0k
        inner.insert(key, entry)
545
2.06M
    }
546
547
    /// Inserts a seeded entry when the key has none. Seeds never replace
548
    /// what was measured.
549
10.2k
    pub fn seed(&self, key: SketchKey, entry: Entry) -> bool {
550
10.2k
        let family = key.family();
551
10.2k
        let mut inner = self.shard(&key).lock();
552
10.2k
        let map = &mut inner.families[family.index()];
553
10.2k
        if map.contains(&key) {
554
1
            return false;
555
10.2k
        }
556
10.2k
        inner.insert(key, entry)
557
10.2k
    }
558
559
    /// The entry under `key`, promoted to recently used. A key not seen
560
    /// for the window is dropped here as well as on the age-out tick, so a
561
    /// lookup between ticks does not size from it. A seed stands until its
562
    /// first sample or its eviction.
563
10.2k
    pub fn lookup(&self, key: &SketchKey, now_s: u64) -> Option<Entry> {
564
10.2k
        let cutoff = now_s.saturating_sub(self.window_s());
565
10.2k
        let mut inner = self.shard(key).lock();
566
10.2k
        let map = &mut inner.families[key.family().index()];
567
10.2k
        let 
entry10.2k
= map.get(key)
?36
;
568
10.2k
        if !entry.seeded && 
entry.last_seen_s < cutoff44
{
569
1
            map.pop(key);
570
1
            inner.bytes[key.family().index()] -= entry_charge(key);
571
1
            return None;
572
10.2k
        }
573
10.2k
        Some(entry.clone())
574
10.2k
    }
575
576
    /// The reservation for an entry: `ShapeOne` of the hints producer,
577
    /// line for line.
578
38
    pub fn shape(&self, entry: &Entry) -> Shaped {
579
38
        shape(&self.policy, entry)
580
38
    }
581
582
31
    pub fn stats(&self) -> Stats {
583
31
        let mut stats = Stats::default();
584
637
        for shard in 
&self.shards31
{
585
637
            let inner = shard.lock();
586
637
            stats.probation += inner.probation.len();
587
1.91k
            for family in Family::ALL {
588
1.91k
                let i = family.index();
589
1.91k
                stats.entries[i] += inner.families[i].len();
590
1.91k
                stats.capacity[i] += inner.families[i].cap().get();
591
1.91k
                stats.evictions[i] += inner.evictions[i];
592
1.91k
                stats.samples[i] += inner.samples[i];
593
1.91k
            }
594
        }
595
31
        stats
596
31
    }
597
598
    /// Ages every entry to `now_s` (see [`Entry::settle`]) and drops the
599
    /// keys not seen for the window, one shard at a time. Runs on the
600
    /// scheduler's tick whether or not a snapshot store is configured.
601
    /// Returns how many keys were dropped.
602
3
    pub fn age_out(&self, now_s: u64) -> usize {
603
3
        let window_s = self.window_s();
604
3
        let cutoff = now_s.saturating_sub(window_s);
605
3
        let mut dropped = 0usize;
606
3
        for shard in &self.shards {
607
3
            let mut inner = shard.lock();
608
9
            for family in Family::ALL {
609
9
                let map = &mut inner.families[family.index()];
610
9
                let mut stale = Vec::new();
611
9
                for (
key8
,
entry8
) in map.iter_mut() {
612
8
                    if !entry.seeded && 
entry.last_seen_s < cutoff7
{
613
2
                        stale.push(key.clone());
614
6
                    } else {
615
6
                        entry.settle(now_s, window_s);
616
6
                    }
617
                }
618
9
                dropped += stale.len();
619
9
                let reclaimed = stale.iter().map(entry_charge).sum::<u64>();
620
9
                for 
key2
in stale {
621
2
                    map.pop(&key);
622
2
                }
623
9
                inner.bytes[family.index()] -= reclaimed;
624
            }
625
        }
626
3
        dropped
627
3
    }
628
629
    /// Every entry still in the window, least recently used first within
630
    /// each shard, so a loader that inserts in order ends with the same
631
    /// recency. One shard is locked at a time, for its copy only.
632
5
    pub fn snapshot(&self, now_s: u64) -> Snapshot {
633
5
        let cutoff = now_s.saturating_sub(self.window_s());
634
5
        let mut entries = Vec::new();
635
13
        for shard in 
&self.shards5
{
636
13
            let inner = shard.lock();
637
39
            for family in Family::ALL {
638
39
                let map = &inner.families[family.index()];
639
39
                let start = entries.len();
640
39
                entries.extend(
641
39
                    map.iter()
642
48
                        .
filter39
(|(_, entry)| entry.seeded || entry.last_seen_s >= cutoff)
643
46
                        .
map39
(|(key, entry)| (key.clone(), entry.clone())),
644
                );
645
                // `iter` is most recently used first.
646
39
                entries[start..].reverse();
647
            }
648
        }
649
5
        Snapshot {
650
5
            sampler_generation: SAMPLER_GENERATION,
651
5
            written_at_s: now_s,
652
5
            entries,
653
5
        }
654
5
    }
655
656
    /// Maximum snapshot payload, including its header, within the reserved
657
    /// transient part of the budget. Also bounds reads before decoding.
658
49
    pub const fn snapshot_byte_limit(&self) -> u64 {
659
49
        25 + (self.budget.target + self.budget.mnemonic + self.budget.command) as u64 * ENTRY_BYTES
660
49
    }
661
662
    /// Encode directly under one shard lock at a time. Reserve the output
663
    /// once, so map cloning and buffer growth cannot multiply peak memory.
664
24
    pub fn encode_snapshot(&self, now_s: u64) -> Vec<u8> {
665
24
        let limit = self.snapshot_byte_limit();
666
24
        let mut bytes = Vec::with_capacity(usize::try_from(limit).unwrap_or(usize::MAX));
667
24
        let header = Snapshot {
668
24
            sampler_generation: SAMPLER_GENERATION,
669
24
            written_at_s: now_s,
670
24
            entries: Vec::new(),
671
24
        }
672
24
        .encode();
673
24
        bytes.extend_from_slice(&header);
674
24
        let cutoff = now_s.saturating_sub(self.window_s());
675
24
        let mut count = 0u64;
676
652
        for shard in 
&self.shards24
{
677
652
            let inner = shard.lock();
678
1.95k
            for map in 
&inner.families652
{
679
1.95k
                for (
key83
,
entry83
) in map.iter().rev() {
680
83
                    if entry.seeded || entry.last_seen_s >= cutoff {
681
83
                        encode_entry(&mut bytes, key, entry);
682
83
                        count += 1;
683
83
                    
}0
684
                }
685
            }
686
        }
687
24
        bytes[17..25].copy_from_slice(&count.to_le_bytes());
688
24
        bytes
689
24
    }
690
691
    /// Conservative charged resident allocation, including unused hash slots.
692
4
    pub fn charged_bytes(&self) -> u64 {
693
4
        let mut bytes = FIXED_BYTES;
694
51
        for shard in 
&self.shards4
{
695
51
            let inner = shard.lock();
696
51
            bytes += inner.bytes.iter().sum::<u64>() + inner.probation_bytes;
697
153
            bytes += (
inner.families51
.
iter51
().
map51
(|m| m.cap().get()).
sum51
::<usize>()
698
51
                + inner.probation.cap().get()) as u64
699
                * HASH_SLOT_BYTES;
700
        }
701
4
        bytes
702
4
    }
703
704
    /// Restore a replica's snapshot at startup. Existing entries are left
705
    /// alone, making replay idempotent and preserving newer local observations.
706
    /// Skip expired keys and refuse an incompatible sampler generation.
707
19
    pub fn load(&self, snapshot: Snapshot, now_s: u64) -> Result<usize, Error> {
708
19
        if snapshot.sampler_generation != SAMPLER_GENERATION {
709
1
            return Err(make_err!(
710
1
                Code::FailedPrecondition,
711
1
                "resource sketch snapshot is of sampler generation {}, this build is {}",
712
1
                snapshot.sampler_generation,
713
1
                SAMPLER_GENERATION
714
1
            ));
715
18
        }
716
18
        let cutoff = now_s.saturating_sub(self.window_s());
717
18
        let mut loaded = 0usize;
718
38
        for (key, entry) in 
snapshot.entries18
{
719
38
            if !entry.seeded && entry.last_seen_s < cutoff {
720
0
                continue;
721
38
            }
722
38
            let family = key.family();
723
38
            let mut inner = self.shard(&key).lock();
724
38
            let map = &mut inner.families[family.index()];
725
38
            if map.contains(&key) || 
!35
inner35
.
insert35
(key, entry) {
726
3
                continue;
727
35
            }
728
35
            loaded += 1;
729
        }
730
18
        Ok(loaded)
731
19
    }
732
733
    /// The shaped hints of every entry of `family` (every family when
734
    /// `None`), most recently seen first, at most `limit`.
735
4
    pub fn hints(&self, family: Option<Family>, limit: usize) -> Vec<HintRow> {
736
4
        let mut rows = Vec::new();
737
4
        for shard in &self.shards {
738
4
            let inner = shard.lock();
739
12
            for f in Family::ALL {
740
12
                if family.is_some_and(|wanted| 
wanted6
!=
f6
) {
741
4
                    continue;
742
8
                }
743
8
                for (
key7
,
entry7
) in inner.families[f.index()].iter().take(limit) {
744
7
                    rows.push(HintRow {
745
7
                        key: key.clone(),
746
7
                        shaped: shape(&self.policy, entry),
747
7
                        samples: entry.samples,
748
7
                        last_seen_s: entry.last_seen_s,
749
7
                        seeded: entry.seeded,
750
7
                    });
751
7
                }
752
            }
753
        }
754
8
        
rows4
.
sort_by_key4
(|row| core::cmp::Reverse(row.last_seen_s));
755
4
        rows.truncate(limit);
756
4
        rows
757
4
    }
758
}
759
760
/// One line of the admin view: a key with what the scheduler reserves for
761
/// it now.
762
#[derive(Clone, Debug, PartialEq, Eq)]
763
pub struct HintRow {
764
    pub key: SketchKey,
765
    pub shaped: Shaped,
766
    pub samples: u32,
767
    pub last_seen_s: u64,
768
    pub seeded: bool,
769
}
770
771
impl Entry {
772
61
    pub fn max_memory_kb(&self) -> u64 {
773
61
        self.current.max_memory_kb.max(self.previous.max_memory_kb)
774
61
    }
775
776
45
    pub fn max_cpu_millicores(&self) -> u64 {
777
45
        self.current
778
45
            .max_cpu_millicores
779
45
            .max(self.previous.max_cpu_millicores)
780
45
    }
781
782
45
    pub fn max_disk_kb(&self) -> u64 {
783
45
        self.current.max_disk_kb.max(self.previous.max_disk_kb)
784
45
    }
785
786
47
    pub fn memory_p95_upper(&self) -> u64 {
787
47
        self.current.memory.p95_upper_with(&self.previous.memory)
788
47
    }
789
790
45
    pub fn cpu_p95_upper(&self) -> u64 {
791
45
        self.current.cpu.p95_upper_with(&self.previous.cpu)
792
45
    }
793
794
45
    pub fn disk_p95_upper(&self) -> u64 {
795
45
        self.current.disk.p95_upper_with(&self.previous.disk)
796
45
    }
797
798
    /// Every sample of the key still counted, both halves together.
799
18
    pub fn memory_samples(&self) -> u64 {
800
18
        self.current
801
18
            .memory
802
18
            .total()
803
18
            .saturating_add(self.previous.memory.total())
804
18
    }
805
806
2.08M
    fn half_s(window_s: u64) -> u64 {
807
2.08M
        (window_s / 2).max(1)
808
2.08M
    }
809
810
    /// Ages the entry to `now_s`. When the half-window turns, the current
811
    /// samples become the previous ones and the previous ones are gone; two
812
    /// turns at once clear both. So a sample counts for between half a
813
    /// window and a whole one, and a rarely seen key keeps its last few
814
    /// samples that long and no longer. The kill floor expires a window
815
    /// after the kill. A seed is left as it is: it stands until its first
816
    /// sample.
817
2.06M
    pub fn settle(&mut self, now_s: u64, window_s: u64) {
818
2.06M
        if self.seeded {
819
1
            return;
820
2.06M
        }
821
2.06M
        let period = now_s / Self::half_s(window_s);
822
2.06M
        if period > self.period {
823
5
            if period == self.period + 1 {
824
4
                self.previous = core::mem::take(&mut self.current);
825
4
            } else {
826
1
                self.previous = Half::default();
827
1
                self.current = Half::default();
828
1
            }
829
5
            self.period = period;
830
2.06M
        }
831
2.06M
        if self.killed_at_kb > 0 && 
now_s12
.
saturating_sub12
(self.killed_at_s) > window_s {
832
2
            self.killed_at_kb = 0;
833
2
            self.killed_at_s = 0;
834
2.06M
        }
835
2.06M
    }
836
837
2.06M
    fn record(&mut self, observation: &Observation, window_s: u64) {
838
2.06M
        if self.seeded {
839
2
            // The seed stood in for measurements; the first one replaces it
840
2
            // whole, so a seed above the truth neither caps nor lifts it.
841
2
            *self = Self::default();
842
2.06M
        }
843
2.06M
        if self.period == 0 {
844
27.0k
            self.period = observation.now_s / Self::half_s(window_s);
845
2.03M
        }
846
2.06M
        self.settle(observation.now_s, window_s);
847
2.06M
        self.samples = self.samples.saturating_add(1);
848
2.06M
        self.last_seen_s = self.last_seen_s.max(observation.now_s);
849
2.06M
        let half = &mut self.current;
850
2.06M
        if observation.peak_memory_kb > 0 {
851
2.06M
            half.memory.add(observation.peak_memory_kb);
852
2.06M
            half.max_memory_kb = half.max_memory_kb.max(observation.peak_memory_kb);
853
2.06M
        
}2
854
2.06M
        if let Some(
millicores6
) = observation.cpu_millicores {
855
6
            half.cpu.add(millicores);
856
6
            half.max_cpu_millicores = half.max_cpu_millicores.max(millicores);
857
2.06M
        }
858
2.06M
        if observation.peak_disk_kb > 0 {
859
4
            half.disk.add(observation.peak_disk_kb);
860
4
            half.max_disk_kb = half.max_disk_kb.max(observation.peak_disk_kb);
861
2.06M
        }
862
2.06M
        if let Some(
killed_at_kb11
) = observation.killed_at_kb
863
11
            && killed_at_kb > 0
864
11
            && killed_at_kb >= self.killed_at_kb
865
11
        {
866
11
            self.killed_at_kb = killed_at_kb;
867
11
            self.killed_at_s = observation.now_s;
868
2.06M
        }
869
2.06M
        self.seeded = false;
870
2.06M
    }
871
872
    /// An entry standing in for a hint from a file: one sample at each
873
    /// stated number, so the first real sample takes over.
874
10.2k
    pub fn seeded(cpu: u64, memory_kb: u64, disk_kb: u64, samples: u32, last_seen_s: u64) -> Self {
875
10.2k
        let mut entry = Self {
876
10.2k
            samples: samples.max(1),
877
10.2k
            last_seen_s,
878
10.2k
            seeded: true,
879
10.2k
            ..Self::default()
880
10.2k
        };
881
10.2k
        if memory_kb > 0 {
882
10.2k
            entry.current.memory.add(memory_kb);
883
10.2k
            entry.current.max_memory_kb = memory_kb;
884
10.2k
        
}3
885
10.2k
        if cpu > 0 {
886
7
            entry.current.cpu.add(cpu);
887
7
            entry.current.max_cpu_millicores = cpu;
888
10.2k
        }
889
10.2k
        if disk_kb > 0 {
890
7
            entry.current.disk.add(disk_kb);
891
7
            entry.current.max_disk_kb = disk_kb;
892
10.2k
        }
893
10.2k
        entry
894
10.2k
    }
895
}
896
897
/// The p95 bucket's upper bound, capped by the exact maximum: an entry with
898
/// one sample reserves what that sample used, one with a thousand less than
899
/// twice the true p95 and never less than it.
900
135
const fn p95_capped(upper: u64, max: u64) -> u64 {
901
135
    if max == 0 {
902
78
        return 0;
903
57
    }
904
57
    if upper == 0 || max < upper {
905
56
        max
906
    } else {
907
1
        upper
908
    }
909
135
}
910
911
139
const fn with_headroom(value: u64, headroom_percent: u64) -> u64 {
912
139
    if value == 0 {
913
78
        return 0;
914
61
    }
915
61
    value.saturating_mul(100 + headroom_percent) / 100
916
139
}
917
918
/// The first class on the ladder that holds the numbers on every dimension
919
/// it names and sits above a reservation the action was killed at. Past the
920
/// top of the ladder it is the top class, unless the action was killed at
921
/// or above the top's memory: no class then sits above the kill, and the
922
/// raw numbers stand rather than a class whose memory would be killed again.
923
43
fn smallest_dominating_class(
924
43
    classes: &[SizeClassSpec],
925
43
    memory_kb: u64,
926
43
    cpu: u64,
927
43
    disk_kb: u64,
928
43
    killed_at_kb: u64,
929
43
) -> Option<&SizeClassSpec> {
930
43
    let 
top15
= classes.last()
?28
;
931
15
    if top.memory_kb <= killed_at_kb {
932
1
        return None;
933
14
    }
934
21
    for class in 
classes14
{
935
21
        if class.memory_kb < memory_kb || 
class.memory_kb <= killed_at_kb15
{
936
6
            continue;
937
15
        }
938
15
        if class.cpu_count > 0 && cpu > 0 && 
class.cpu_count < cpu3
{
939
2
            continue;
940
13
        }
941
13
        if class.disk_kb > 0 && 
disk_kb > 00
&&
class.disk_kb < disk_kb0
{
942
0
            continue;
943
13
        }
944
13
        return Some(class);
945
    }
946
1
    Some(top)
947
43
}
948
949
45
fn shape(policy: &Policy, entry: &Entry) -> Shaped {
950
45
    let headroom = policy.headroom_percent;
951
45
    let mut memory_kb = with_headroom(
952
45
        p95_capped(entry.memory_p95_upper(), entry.max_memory_kb()),
953
45
        headroom,
954
    );
955
45
    if entry.killed_at_kb > 0 && 
memory_kb <= entry.killed_at_kb7
{
956
4
        // Killed at that reservation: the next one must be larger, however
957
4
        // small the samples that survived were, by the same headroom a
958
4
        // measurement gets.
959
4
        memory_kb = with_headroom(entry.killed_at_kb, headroom) + 1;
960
41
    }
961
    // A CPU p95 of zero is an action that used no measurable CPU or a
962
    // worker that reported none; neither keeps the action off a small
963
    // class, so no CPU need is stated and the class goes by memory.
964
45
    let mut cpu = with_headroom(
965
45
        p95_capped(entry.cpu_p95_upper(), entry.max_cpu_millicores()),
966
45
        headroom,
967
    );
968
45
    let mut disk_kb = with_headroom(
969
45
        p95_capped(entry.disk_p95_upper(), entry.max_disk_kb()),
970
45
        headroom,
971
    );
972
    // A number on a dimension the fleet does not reserve says nothing about
973
    // the ones it does, so it is dropped before anything is decided from
974
    // it: a disk-only measurement is no measurement, and the cold start
975
    // applies, rather than the smallest class.
976
45
    if !policy.reserve_cpu {
977
0
        cpu = 0;
978
45
    }
979
45
    if !policy.reserve_memory {
980
0
        memory_kb = 0;
981
45
    }
982
45
    if !policy.reserve_disk {
983
7
        disk_kb = 0;
984
38
    }
985
45
    if memory_kb == 0 && 
cpu == 03
&&
disk_kb == 02
{
986
2
        return Shaped::default();
987
43
    }
988
43
    let mut class = None;
989
14
    if let Some(found) =
990
43
        smallest_dominating_class(&policy.classes, memory_kb, cpu, disk_kb, entry.killed_at_kb)
991
    {
992
14
        class = Some(found.name.clone());
993
14
        if found.memory_kb > 0 {
994
14
            memory_kb = found.memory_kb;
995
14
        
}0
996
14
        if found.cpu_count > 0 {
997
14
            cpu = found.cpu_count;
998
14
        
}0
999
14
        if found.disk_kb > 0 {
1000
1
            disk_kb = found.disk_kb;
1001
13
        }
1002
29
    }
1003
    Shaped {
1004
43
        cpu_count: if policy.reserve_cpu { cpu } else { 
00
},
1005
43
        memory_kb: if policy.reserve_memory { memory_kb } else { 
00
},
1006
43
        disk_kb: if policy.reserve_disk { 
disk_kb38
} else {
05
},
1007
43
        class,
1008
    }
1009
45
}
1010
1011
/// What each replica writes every `snapshot_interval_s` under its own key
1012
/// and restores on restart. History from other replicas is not merged.
1013
#[derive(Clone, Debug, PartialEq, Eq)]
1014
pub struct Snapshot {
1015
    pub sampler_generation: u32,
1016
    pub written_at_s: u64,
1017
    /// Least recently used first within a shard, so a loader that inserts
1018
    /// in order ends with the same recency.
1019
    pub entries: Vec<(SketchKey, Entry)>,
1020
}
1021
1022
const SNAPSHOT_MAGIC: &[u8; 4] = b"NLHS";
1023
const SNAPSHOT_VERSION: u8 = 3;
1024
1025
impl Snapshot {
1026
    /// A compact binary form: histograms as their non-zero buckets, since
1027
    /// most of the 25 are zero. No compression and no dependency; the budget
1028
    /// bounds the size.
1029
25
    pub fn encode(&self) -> Vec<u8> {
1030
25
        let mut out = Vec::with_capacity(32 + self.entries.len() * 160);
1031
25
        out.extend_from_slice(SNAPSHOT_MAGIC);
1032
25
        out.push(SNAPSHOT_VERSION);
1033
25
        out.extend_from_slice(&self.sampler_generation.to_le_bytes());
1034
25
        out.extend_from_slice(&self.written_at_s.to_le_bytes());
1035
25
        out.extend_from_slice(
1036
25
            &u64::try_from(self.entries.len())
1037
25
                .unwrap_or(u64::MAX)
1038
25
                .to_le_bytes(),
1039
        );
1040
25
        for (
key3
,
entry3
) in &self.entries {
1041
3
            encode_entry(&mut out, key, entry);
1042
3
        }
1043
25
        out
1044
25
    }
1045
1046
    /// Header bytes needed to select a restart snapshot without reading its payload.
1047
    pub const HEADER_BYTES: u64 = 17;
1048
1049
49
    fn read_header(r: &mut Reader<'_>) -> Result<(u32, u64), Error> {
1050
49
        if r.take(4)
?0
!= SNAPSHOT_MAGIC {
1051
0
            return Err(make_err!(
1052
0
                Code::InvalidArgument,
1053
0
                "resource sketch snapshot: not a snapshot"
1054
0
            ));
1055
49
        }
1056
49
        let version = r.u8()
?0
;
1057
49
        if version != SNAPSHOT_VERSION {
1058
1
            return Err(make_err!(
1059
1
                Code::InvalidArgument,
1060
1
                "resource sketch snapshot: version {version}, this build reads {SNAPSHOT_VERSION}"
1061
1
            ));
1062
48
        }
1063
48
        Ok((r.u32()
?0
, r.u64()
?0
))
1064
49
    }
1065
1066
    /// The write time only when this build understands the snapshot's format
1067
    /// and sampler. Discovery never needs to allocate full histogram entries.
1068
24
    pub fn compatible_written_at(bytes: &[u8]) -> Result<u64, Error> {
1069
24
        let (generation, written_at) = Self::read_header(&mut Reader { bytes, at: 0 })
?0
;
1070
24
        if generation != SAMPLER_GENERATION {
1071
4
            return Err(make_err!(
1072
4
                Code::FailedPrecondition,
1073
4
                "resource sketch snapshot: incompatible sampler generation {generation}"
1074
4
            ));
1075
20
        }
1076
20
        Ok(written_at)
1077
24
    }
1078
1079
    /// Bound the decoded entries as well as the encoded bytes: sparse
1080
    /// histograms occupy much more memory when expanded than on disk.
1081
25
    pub fn decode(bytes: &[u8], max_entries: usize) -> Result<Self, Error> {
1082
        const MIN_ENTRY_BYTES: usize = 96;
1083
25
        let mut r = Reader { bytes, at: 0 };
1084
25
        let (
sampler_generation24
,
written_at_s24
) = Self::read_header(&mut r)
?1
;
1085
24
        let count = usize::try_from(r.u64()
?0
)
1086
24
            .map_err(|_| 
make_err!0
(
Code::InvalidArgument0
, "resource sketch snapshot: count"))
?0
;
1087
24
        if count > max_entries || 
count23
> (
bytes23
.len() - r.at) / MIN_ENTRY_BYTES {
1088
2
            return Err(make_err!(
1089
2
                Code::InvalidArgument,
1090
2
                "resource sketch snapshot: entry count exceeds budget or payload"
1091
2
            ));
1092
22
        }
1093
22
        let mut entries = Vec::with_capacity(count);
1094
22
        for _ in 0..count {
1095
81
            let key = match r.u8()
?0
{
1096
                0 => {
1097
58
                    let target = r.str()
?0
;
1098
58
                    let mnemonic = if r.u8()
?0
== 1 { Some(r.str()
?0
) } else {
None0
};
1099
58
                    SketchKey::Target { target, mnemonic }
1100
                }
1101
21
                1 => SketchKey::Mnemonic { mnemonic: r.str()
?0
},
1102
2
                2 => SketchKey::Command { digest: r.str()
?0
},
1103
0
                tag => {
1104
0
                    return Err(make_err!(
1105
0
                        Code::InvalidArgument,
1106
0
                        "resource sketch snapshot: key tag {tag}"
1107
0
                    ));
1108
                }
1109
            };
1110
81
            let samples = r.u32()
?0
;
1111
81
            let current = r.half()
?0
;
1112
81
            let previous = r.half()
?0
;
1113
81
            let killed_at_kb = r.u64()
?0
;
1114
81
            let killed_at_s = r.u64()
?0
;
1115
81
            let last_seen_s = r.u64()
?0
;
1116
81
            let 
period80
= r.u64()
?1
;
1117
80
            let 
seeded79
= r.u8()
?1
== 1;
1118
79
            entries.push((
1119
79
                key,
1120
79
                Entry {
1121
79
                    samples,
1122
79
                    current,
1123
79
                    previous,
1124
79
                    killed_at_kb,
1125
79
                    killed_at_s,
1126
79
                    last_seen_s,
1127
79
                    period,
1128
79
                    seeded,
1129
79
                },
1130
79
            ));
1131
        }
1132
20
        if r.at != bytes.len() {
1133
0
            return Err(make_err!(
1134
0
                Code::InvalidArgument,
1135
0
                "resource sketch snapshot: {} trailing bytes",
1136
0
                bytes.len() - r.at
1137
0
            ));
1138
20
        }
1139
20
        Ok(Self {
1140
20
            sampler_generation,
1141
20
            written_at_s,
1142
20
            entries,
1143
20
        })
1144
25
    }
1145
}
1146
1147
147
fn put_str(out: &mut Vec<u8>, s: &str) {
1148
147
    let len = u32::try_from(s.len()).expect("key fits within the sketch budget");
1149
147
    out.extend_from_slice(&len.to_le_bytes());
1150
147
    out.extend_from_slice(&s.as_bytes()[..len as usize]);
1151
147
}
1152
1153
516
fn put_hist(out: &mut Vec<u8>, hist: &Hist) {
1154
12.9k
    let 
nonzero516
=
hist.0516
.
iter516
().
filter516
(|&&n| n > 0).
count516
();
1155
516
    out.push(u8::try_from(nonzero).unwrap_or(u8::MAX));
1156
12.9k
    for (i, &n) in 
hist.0516
.
iter516
().
enumerate516
() {
1157
12.9k
        if n > 0 {
1158
241
            out.push(u8::try_from(i).unwrap_or(u8::MAX));
1159
241
            out.extend_from_slice(&n.to_le_bytes());
1160
12.6k
        }
1161
    }
1162
516
}
1163
1164
struct Reader<'a> {
1165
    bytes: &'a [u8],
1166
    at: usize,
1167
}
1168
1169
impl<'a> Reader<'a> {
1170
2.57k
    fn take(&mut self, n: usize) -> Result<&'a [u8], Error> {
1171
2.57k
        let end = self
1172
2.57k
            .at
1173
2.57k
            .checked_add(n)
1174
2.57k
            .filter(|&end| end <= self.bytes.len());
1175
2.57k
        let Some(
end2.57k
) = end else {
1176
2
            return Err(make_err!(
1177
2
                Code::InvalidArgument,
1178
2
                "resource sketch snapshot: truncated at byte {}",
1179
2
                self.at
1180
2
            ));
1181
        };
1182
2.57k
        let out = &self.bytes[self.at..end];
1183
2.57k
        self.at = end;
1184
2.57k
        Ok(out)
1185
2.57k
    }
1186
1187
994
    fn u8(&mut self) -> Result<u8, Error> {
1188
994
        Ok(self.take(1)
?1
[
0993
])
1189
994
    }
1190
1191
268
    fn u32(&mut self) -> Result<u32, Error> {
1192
268
        let b = self.take(4)
?0
;
1193
268
        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1194
268
    }
1195
1196
1.12k
    fn u64(&mut self) -> Result<u64, Error> {
1197
1.12k
        let 
b1.12k
= self.take(8)
?1
;
1198
1.12k
        let mut arr = [0u8; 8];
1199
1.12k
        arr.copy_from_slice(b);
1200
1.12k
        Ok(u64::from_le_bytes(arr))
1201
1.12k
    }
1202
1203
139
    fn str(&mut self) -> Result<Arc<str>, Error> {
1204
139
        let len = self.u32()
?0
as usize;
1205
139
        let b = self.take(len)
?0
;
1206
139
        core::str::from_utf8(b)
1207
139
            .map(Arc::from)
1208
139
            .map_err(|_| 
make_err!0
(
Code::InvalidArgument0
, "resource sketch snapshot: utf8"))
1209
139
    }
1210
1211
162
    fn half(&mut self) -> Result<Half, Error> {
1212
        Ok(Half {
1213
162
            memory: self.hist()
?0
,
1214
162
            cpu: self.hist()
?0
,
1215
162
            disk: self.hist()
?0
,
1216
162
            max_memory_kb: self.u64()
?0
,
1217
162
            max_cpu_millicores: self.u64()
?0
,
1218
162
            max_disk_kb: self.u64()
?0
,
1219
        })
1220
162
    }
1221
1222
486
    fn hist(&mut self) -> Result<Hist, Error> {
1223
486
        let nonzero = usize::from(self.u8()
?0
);
1224
486
        if nonzero > HIST_BUCKETS {
1225
0
            return Err(make_err!(
1226
0
                Code::InvalidArgument,
1227
0
                "resource sketch snapshot: {nonzero} histogram buckets"
1228
0
            ));
1229
486
        }
1230
486
        let mut hist = Hist::default();
1231
486
        for _ in 0..nonzero {
1232
240
            let i = usize::from(self.u8()
?0
);
1233
240
            let n = self.u64()
?0
;
1234
240
            if i >= HIST_BUCKETS {
1235
0
                return Err(make_err!(
1236
0
                    Code::InvalidArgument,
1237
0
                    "resource sketch snapshot: histogram bucket {i}"
1238
0
                ));
1239
240
            }
1240
240
            hist.0[i] = n;
1241
        }
1242
486
        Ok(hist)
1243
486
    }
1244
}
1245
1246
86
fn encode_entry(out: &mut Vec<u8>, key: &SketchKey, entry: &Entry) {
1247
86
    match key {
1248
61
        SketchKey::Target { target, mnemonic } => {
1249
61
            out.push(0);
1250
61
            put_str(out, target);
1251
61
            match mnemonic {
1252
61
                Some(mnemonic) => {
1253
61
                    out.push(1);
1254
61
                    put_str(out, mnemonic);
1255
61
                }
1256
0
                None => out.push(0),
1257
            }
1258
        }
1259
24
        SketchKey::Mnemonic { mnemonic } => {
1260
24
            out.push(1);
1261
24
            put_str(out, mnemonic);
1262
24
        }
1263
1
        SketchKey::Command { digest } => {
1264
1
            out.push(2);
1265
1
            put_str(out, digest);
1266
1
        }
1267
    }
1268
86
    out.extend_from_slice(&entry.samples.to_le_bytes());
1269
172
    for half in 
[&entry.current, &entry.previous]86
{
1270
516
        for hist in 
[172
&half.memory172
, &half.cpu, &half.disk] {
1271
516
            put_hist(out, hist);
1272
516
        }
1273
516
        for value in [
1274
172
            half.max_memory_kb,
1275
172
            half.max_cpu_millicores,
1276
172
            half.max_disk_kb,
1277
516
        ] {
1278
516
            out.extend_from_slice(&value.to_le_bytes());
1279
516
        }
1280
    }
1281
344
    for value in [
1282
86
        entry.killed_at_kb,
1283
86
        entry.killed_at_s,
1284
86
        entry.last_seen_s,
1285
86
        entry.period,
1286
344
    ] {
1287
344
        out.extend_from_slice(&value.to_le_bytes());
1288
344
    }
1289
86
    out.push(u8::from(entry.seeded));
1290
86
}