Skip to content

Commit a9b1a3f

Browse files
authored
refactor: extract sampling primitives into shared module (#418)
* 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). * address CR feedback: return u64 from draw_exponential, remove duplicated tests - Change draw_exponential return type from i64 to u64 since the value is guaranteed >= 1 - Remove duplicated sampling tests from task_dumped.rs (already covered by sampling.rs with stricter tolerances)
1 parent baab26f commit a9b1a3f

3 files changed

Lines changed: 94 additions & 72 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 u64 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) -> u64 {
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 u64).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: 3 additions & 72 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) as i64;
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) as i64;
174145
}
175146
match &result {
176147
Poll::Ready(_) => {
@@ -316,43 +287,3 @@ impl Encodable for TaskDumpData<'_> {
316287
});
317288
}
318289
}
319-
320-
#[cfg(test)]
321-
mod tests {
322-
use super::SplitMix64;
323-
324-
#[test]
325-
fn splitmix64_deterministic() {
326-
let mut rng = SplitMix64::new(42);
327-
let a = rng.next_u64();
328-
let b = rng.next_u64();
329-
330-
let mut rng2 = SplitMix64::new(42);
331-
assert_eq!(a, rng2.next_u64());
332-
assert_eq!(b, rng2.next_u64());
333-
}
334-
335-
#[test]
336-
fn draw_exponential_ns_mean_is_reasonable() {
337-
let mut rng = SplitMix64::new(123);
338-
let mean_ns: u64 = 10_000_000; // 10ms
339-
let n = 10_000;
340-
let sum: f64 = (0..n)
341-
.map(|_| rng.draw_exponential_ns(mean_ns) as f64)
342-
.sum();
343-
let observed_mean = sum / n as f64;
344-
// Within 10% of the configured mean.
345-
assert!(
346-
(observed_mean - mean_ns as f64).abs() < mean_ns as f64 * 0.1,
347-
"observed mean {observed_mean} too far from expected {mean_ns}"
348-
);
349-
}
350-
351-
#[test]
352-
fn draw_exponential_ns_always_positive() {
353-
let mut rng = SplitMix64::new(0);
354-
for _ in 0..10_000 {
355-
assert!(rng.draw_exponential_ns(1_000_000) >= 1);
356-
}
357-
}
358-
}

0 commit comments

Comments
 (0)