Skip to main content

py_spy/
timer.rs

1use std::time::{Duration, Instant};
2#[cfg(windows)]
3use winapi::um::timeapi;
4
5use rand::Rng;
6use rand_distr::Exp;
7
8/// Timer is an iterator that sleeps an appropriate amount of time between iterations
9/// so that we can sample the process a certain number of times a second.
10/// We're using an irregular sampling strategy to avoid aliasing effects that can happen
11/// if the target process runs code at a similar schedule as the profiler:
12/// https://github.com/benfred/py-spy/issues/94
13pub struct Timer {
14    start: Instant,
15    desired: Duration,
16    exp: Exp<f64>,
17}
18
19impl Timer {
20    pub fn new(rate: f64) -> Timer {
21        // This changes a system-wide setting on Windows so that the OS wakes up every 1ms
22        // instead of the default 15.6ms. This is required to have a sleep call
23        // take less than 15ms, which we need since we usually profile at more than 64hz.
24        // The downside is that this will increase power usage: good discussions are:
25        // https://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/
26        // and http://www.belshe.com/2010/06/04/chrome-cranking-up-the-clock/
27        #[cfg(windows)]
28        unsafe {
29            timeapi::timeBeginPeriod(1);
30        }
31
32        let start = Instant::now();
33        Timer {
34            start,
35            desired: Duration::from_secs(0),
36            exp: Exp::new(rate).unwrap(),
37        }
38    }
39}
40
41impl Iterator for Timer {
42    type Item = Result<Duration, Duration>;
43
44    fn next(&mut self) -> Option<Self::Item> {
45        let elapsed = self.start.elapsed();
46
47        // figure out how many nanoseconds should come between the previous and
48        // the next sample using an exponential distribution to avoid aliasing
49        let nanos = 1_000_000_000.0 * rand::rng().sample(self.exp);
50
51        // since we want to account for the amount of time the sampling takes
52        // we keep track of when we should sleep to (rather than just sleeping
53        // the amount of time from the previous line).
54        self.desired += Duration::from_nanos(nanos as u64);
55
56        // sleep if appropriate, or warn if we are behind in sampling
57        if self.desired > elapsed {
58            std::thread::sleep(self.desired - elapsed);
59            Some(Ok(self.desired - elapsed))
60        } else {
61            Some(Err(elapsed - self.desired))
62        }
63    }
64}
65
66impl Drop for Timer {
67    fn drop(&mut self) {
68        #[cfg(windows)]
69        unsafe {
70            timeapi::timeEndPeriod(1);
71        }
72    }
73}