Skip to content

Commit 94fb078

Browse files
committed
refactor: extract sampling primitives into shared module
Move SplitMix64 and draw_exponential from task_dumped.rs into a new pub(crate) sampling module. Rename draw_exponential_ns → draw_exponential since the math is unit-agnostic (the caller decides whether 'mean' is nanoseconds, bytes, or anything else). This prepares for the memory profiler (commit 2+), which will reuse the same geometric/Poisson sampling primitive to sample on bytes allocated rather than nanoseconds idle. Pure refactor — no behavior change. Existing task-dump tests pass unmodified (only the function name changed).
1 parent 6f0fd96 commit 94fb078

3 files changed

Lines changed: 99 additions & 39 deletions

File tree

dial9-tokio-telemetry/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod background_task;
1616
pub(crate) mod metrics;
1717
pub(crate) mod primitives;
1818
pub(crate) mod rate_limit;
19+
pub(crate) mod sampling;
1920
#[cfg(feature = "taskdump")]
2021
pub(crate) mod task_dumped;
2122
/// Core telemetry types, recording, and trace I/O.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Shared geometric/Poisson sampling primitives.
2+
//!
3+
//! Used by the task-dump idle sampler (sampling on nanoseconds) and by the
4+
//! memory profiler (sampling on bytes). The unit is opaque to the math —
5+
//! callers pass the mean and treat the returned i64 as a counter in their
6+
//! native unit.
7+
8+
/// Minimal splitmix64 PRNG. Fast, no dependencies, good enough for sampling.
9+
pub(crate) struct SplitMix64(u64);
10+
11+
impl SplitMix64 {
12+
pub(crate) fn new(seed: u64) -> Self {
13+
Self(seed)
14+
}
15+
16+
pub(crate) fn next_u64(&mut self) -> u64 {
17+
self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
18+
let mut z = self.0;
19+
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
20+
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
21+
z ^ (z >> 31)
22+
}
23+
24+
/// Draw from an exponential distribution with the given mean.
25+
/// Returns at least 1 to avoid immediate re-trigger when the
26+
/// counter goes to zero.
27+
///
28+
/// The unit is whatever the caller treats `mean` as (nanoseconds,
29+
/// bytes, etc.).
30+
pub(crate) fn draw_exponential(&mut self, mean: u64) -> i64 {
31+
// Generate a uniform float in (0, 1] — avoid exact 0 to prevent ln(0).
32+
let u = (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64);
33+
let u = if u == 0.0 { f64::MIN_POSITIVE } else { u };
34+
let sample = -u.ln() * (mean as f64);
35+
(sample as i64).max(1)
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use super::SplitMix64;
42+
43+
#[test]
44+
fn splitmix_deterministic_with_fixed_seed() {
45+
let mut rng = SplitMix64::new(42);
46+
let a = rng.next_u64();
47+
let b = rng.next_u64();
48+
49+
let mut rng2 = SplitMix64::new(42);
50+
assert_eq!(a, rng2.next_u64());
51+
assert_eq!(b, rng2.next_u64());
52+
}
53+
54+
#[test]
55+
fn draw_exponential_returns_at_least_1() {
56+
let mut rng = SplitMix64::new(0);
57+
for _ in 0..1000 {
58+
assert!(rng.draw_exponential(1) >= 1);
59+
}
60+
}
61+
62+
#[test]
63+
fn draw_exponential_mean_approximates_target() {
64+
let mut rng = SplitMix64::new(123);
65+
let mean: u64 = 1024;
66+
let n = 100_000;
67+
let sum: f64 = (0..n).map(|_| rng.draw_exponential(mean) as f64).sum();
68+
let observed_mean = sum / n as f64;
69+
// Within ±5% of the configured mean.
70+
assert!(
71+
(observed_mean - mean as f64).abs() < mean as f64 * 0.05,
72+
"observed mean {observed_mean} too far from expected {mean}"
73+
);
74+
}
75+
76+
#[test]
77+
fn draw_exponential_handles_large_mean() {
78+
let mut rng = SplitMix64::new(999);
79+
let mean: u64 = 1_000_000_000;
80+
let mut saw_large = false;
81+
for _ in 0..1000 {
82+
let v = rng.draw_exponential(mean);
83+
assert!(v >= 1);
84+
if v > 1_000_000 {
85+
saw_large = true;
86+
}
87+
}
88+
assert!(saw_large, "expected some values much larger than 1");
89+
}
90+
}

dial9-tokio-telemetry/src/task_dumped.rs

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
//! yield points hit during a capture, with offsets recording each callchain's
3232
//! start. The buffers are reused across polls.
3333
34+
use crate::sampling::SplitMix64;
3435
use crate::telemetry::format::TaskDumpEvent;
3536
use crate::telemetry::recorder::SharedState;
3637
use crate::telemetry::task_metadata::TaskId;
@@ -47,36 +48,6 @@ use std::task::{Context, Poll};
4748
/// Initial heap reservation for the instruction-pointer buffer on first capture.
4849
const FRAME_BUF_INITIAL_CAPACITY: usize = 256;
4950

50-
// ─── Minimal PRNG (splitmix64) ──────────────────────────────────────────────
51-
52-
/// Minimal splitmix64 PRNG. Fast, no dependencies, good enough for sampling.
53-
struct SplitMix64(u64);
54-
55-
impl SplitMix64 {
56-
fn new(seed: u64) -> Self {
57-
Self(seed)
58-
}
59-
60-
fn next_u64(&mut self) -> u64 {
61-
self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
62-
let mut z = self.0;
63-
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
64-
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
65-
z ^ (z >> 31)
66-
}
67-
68-
/// Draw from exponential distribution with given mean (in nanoseconds).
69-
/// Returns at least 1 to avoid immediate re-trigger.
70-
fn draw_exponential_ns(&mut self, mean_ns: u64) -> i64 {
71-
// Generate a uniform float in (0, 1] — avoid exact 0 to prevent ln(0).
72-
let u = (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64);
73-
let u = if u == 0.0 { f64::MIN_POSITIVE } else { u };
74-
let sample = -u.ln() * (mean_ns as f64);
75-
// Clamp to at least 1ns so we never immediately re-trigger.
76-
(sample as i64).max(1)
77-
}
78-
}
79-
8051
// ─── TaskDumped future wrapper ──────────────────────────────────────────────
8152

8253
pin_project! {
@@ -121,7 +92,7 @@ impl<F> TaskDumped<F> {
12192
}
12293
};
12394
let mut rng = SplitMix64::new(seed);
124-
let next_sample_ns = rng.draw_exponential_ns(sample_mean_ns);
95+
let next_sample_ns = rng.draw_exponential(sample_mean_ns);
12596
Self {
12697
inner,
12798
shared,
@@ -170,7 +141,7 @@ impl<F: Future> Future for TaskDumped<F> {
170141
.expect("checked in match above")
171142
.get();
172143
this.frames.emit(this.shared, *this.task_id, ts);
173-
*this.next_sample_ns = this.rng.draw_exponential_ns(*this.sample_mean_ns);
144+
*this.next_sample_ns = this.rng.draw_exponential(*this.sample_mean_ns);
174145
}
175146
match &result {
176147
Poll::Ready(_) => {
@@ -319,7 +290,7 @@ impl Encodable for TaskDumpData<'_> {
319290

320291
#[cfg(test)]
321292
mod tests {
322-
use super::SplitMix64;
293+
use crate::sampling::SplitMix64;
323294

324295
#[test]
325296
fn splitmix64_deterministic() {
@@ -333,13 +304,11 @@ mod tests {
333304
}
334305

335306
#[test]
336-
fn draw_exponential_ns_mean_is_reasonable() {
307+
fn draw_exponential_mean_is_reasonable() {
337308
let mut rng = SplitMix64::new(123);
338309
let mean_ns: u64 = 10_000_000; // 10ms
339310
let n = 10_000;
340-
let sum: f64 = (0..n)
341-
.map(|_| rng.draw_exponential_ns(mean_ns) as f64)
342-
.sum();
311+
let sum: f64 = (0..n).map(|_| rng.draw_exponential(mean_ns) as f64).sum();
343312
let observed_mean = sum / n as f64;
344313
// Within 10% of the configured mean.
345314
assert!(
@@ -349,10 +318,10 @@ mod tests {
349318
}
350319

351320
#[test]
352-
fn draw_exponential_ns_always_positive() {
321+
fn draw_exponential_always_positive() {
353322
let mut rng = SplitMix64::new(0);
354323
for _ in 0..10_000 {
355-
assert!(rng.draw_exponential_ns(1_000_000) >= 1);
324+
assert!(rng.draw_exponential(1_000_000) >= 1);
356325
}
357326
}
358327
}

0 commit comments

Comments
 (0)