1use std::path::PathBuf;
2use std::time::{Duration, Instant};
3
4use super::trace::{self, Source, Sources};
5use super::{Frozen, TURN_WALL_MS, gpu_ms, turn_args, unfrozen_ms};
6use crate::PassTiming;
7
8pub const CAP_FPS: f64 = 60.0;
9pub const TURN_AFTER_MS: f64 = 50.0;
10pub const UNCAPPED_VAR: &str = "PFX_UNCAPPED";
11pub const TURN_WALL_VAR: &str = "PFX_TURN_WALL_MS";
12pub const TURN_MS_VAR: &str = "PFX_TURN_MS";
13pub const TURN_VAR: &str = "GPU_QUEUE_TURN";
14
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct TurnReport {
17 pub work_ms: f64,
18 pub longest_ms: f64,
19}
20
21impl TurnReport {
22 pub fn args(&self) -> [String; 5] {
23 turn_args(self.work_ms, self.longest_ms)
24 }
25}
26
27pub trait FrameClock {
28 fn now_ms(&mut self) -> f64;
29 fn sleep_ms(&mut self, ms: f64);
30 fn turn(&mut self, report: TurnReport);
31 fn frozen_ms(&mut self) -> f64 {
32 0.0
33 }
34}
35
36pub fn frozen_file_ms(path: Option<&std::path::Path>) -> f64 {
37 match path {
38 Some(path) => Frozen::at(path).ms(),
39 None => 0.0,
40 }
41}
42
43pub trait FrameGpu {
44 fn gpu_ms(&self) -> Option<f64>;
45}
46
47impl FrameGpu for () {
48 fn gpu_ms(&self) -> Option<f64> {
49 None
50 }
51}
52
53impl FrameGpu for Vec<PassTiming> {
54 fn gpu_ms(&self) -> Option<f64> {
55 gpu_ms(self)
56 }
57}
58
59impl FrameGpu for [PassTiming] {
60 fn gpu_ms(&self) -> Option<f64> {
61 gpu_ms(self)
62 }
63}
64
65pub trait Sink {
66 fn progress(&mut self, done: u64, total: u64, fps: f64);
67 fn end(&mut self, ok: bool);
68}
69
70pub struct Tally {
71 sink: Option<Box<dyn Sink>>,
72 planned: u64,
73 done: u64,
74 started_ms: f64,
75}
76
77impl Tally {
78 pub fn none() -> Self {
79 Self {
80 sink: None,
81 planned: 0,
82 done: 0,
83 started_ms: 0.0,
84 }
85 }
86
87 pub fn with_sink(sink: Box<dyn Sink>, planned: u64, now_ms: f64) -> Self {
88 Self {
89 sink: Some(sink),
90 planned,
91 done: 0,
92 started_ms: now_ms,
93 }
94 }
95
96 pub fn frame(&mut self) {
97 self.done += 1;
98 }
99
100 pub fn report(&mut self, now_ms: f64) {
101 let Some(sink) = self.sink.as_mut() else {
102 return;
103 };
104 let elapsed_ms = now_ms - self.started_ms;
105 let fps = if elapsed_ms > 0.0 {
106 self.done as f64 * 1000.0 / elapsed_ms
107 } else {
108 0.0
109 };
110 sink.progress(self.done, self.planned, fps);
111 }
112}
113
114impl Drop for Tally {
115 fn drop(&mut self) {
116 if let Some(sink) = self.sink.as_mut() {
117 sink.end(!std::thread::panicking());
118 }
119 }
120}
121
122pub struct WallClock {
123 start: Instant,
124 frozen: Option<PathBuf>,
125}
126
127impl WallClock {
128 pub fn new() -> Self {
129 Self {
130 start: Instant::now(),
131 frozen: std::env::var_os(super::FROZEN_VAR)
132 .filter(|path| !path.is_empty())
133 .map(PathBuf::from),
134 }
135 }
136}
137
138impl Default for WallClock {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144impl FrameClock for WallClock {
145 fn now_ms(&mut self) -> f64 {
146 self.start.elapsed().as_secs_f64() * 1000.0
147 }
148
149 fn sleep_ms(&mut self, ms: f64) {
150 std::thread::sleep(Duration::from_secs_f64(ms / 1000.0));
151 }
152
153 fn turn(&mut self, report: TurnReport) {
154 if std::env::var_os(TURN_VAR).is_some() {
155 let _ = std::process::Command::new("pgpu")
156 .args(report.args())
157 .stdout(std::process::Stdio::null())
158 .status();
159 }
160 }
161
162 fn frozen_ms(&mut self) -> f64 {
163 frozen_file_ms(self.frozen.as_deref())
164 }
165}
166
167pub fn sleep_to(boundary_ms: f64, now_ms: f64) -> f64 {
168 (boundary_ms - now_ms).max(0.0)
169}
170
171pub fn turn_wall_ms(get: &impl Fn(&str) -> Option<String>) -> f64 {
172 get(TURN_WALL_VAR)
173 .and_then(|value| value.trim().parse::<f64>().ok())
174 .filter(|value| value.is_finite() && *value > 0.0)
175 .unwrap_or(TURN_WALL_MS)
176}
177
178pub fn uncapped(get: &impl Fn(&str) -> Option<String>) -> bool {
179 get(UNCAPPED_VAR).is_some_and(|value| value == "1")
180}
181
182pub struct Pace<C: FrameClock> {
183 pub clock: C,
184 wall_ms: f64,
185 tally: Tally,
186 period_ms: Option<f64>,
187 boundary_ms: f64,
188 pending_ms: f64,
189 longest_ms: f64,
190 last_turn_ms: f64,
191 mark_ms: f64,
192 mark_frozen_ms: f64,
193 frames: u64,
194 sources: Sources,
195}
196
197impl Pace<WallClock> {
198 pub fn new() -> Self {
199 let get = |key: &str| std::env::var(key).ok();
200 let cap = if uncapped(&get) { None } else { Some(CAP_FPS) };
201 let mut pace = Self::with_clock(WallClock::new(), cap);
202 pace.wall_ms = turn_wall_ms(&get);
203 pace
204 }
205}
206
207impl Default for Pace<WallClock> {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl<C: FrameClock> Pace<C> {
214 pub fn with_clock(mut clock: C, cap_fps: Option<f64>) -> Self {
215 trace::arm();
216 let now = clock.now_ms();
217 let frozen = clock.frozen_ms();
218 Self {
219 clock,
220 wall_ms: TURN_WALL_MS,
221 tally: Tally::none(),
222 period_ms: cap_fps
223 .filter(|fps| fps.is_finite() && *fps > 0.0)
224 .map(|fps| 1000.0 / fps),
225 boundary_ms: now,
226 pending_ms: 0.0,
227 longest_ms: 0.0,
228 last_turn_ms: now,
229 mark_ms: now,
230 mark_frozen_ms: frozen,
231 frames: 0,
232 sources: Sources::default(),
233 }
234 }
235
236 pub fn with_tally(mut self, tally: Tally) -> Self {
237 self.tally = tally;
238 self
239 }
240
241 pub fn recording(mut self, sink: Box<dyn Sink>, planned: u64) -> Self {
242 let now = self.clock.now_ms();
243 self.tally = Tally::with_sink(sink, planned, now);
244 self
245 }
246
247 #[track_caller]
248 pub fn frame<R: FrameGpu>(&mut self, work: impl FnOnce() -> R) -> R {
249 let frozen_before = self.clock.frozen_ms();
250 let before = self.clock.now_ms();
251 let result = work();
252 let after = self.clock.now_ms();
253 let work_ms = unfrozen_ms(after - before, frozen_before, self.clock.frozen_ms());
254 self.after_gpu(work_ms, result.gpu_ms());
255 result
256 }
257
258 #[track_caller]
259 pub fn after(&mut self, frame_ms: f64) {
260 self.after_gpu(frame_ms, None);
261 }
262
263 #[track_caller]
264 pub fn after_gpu(&mut self, frame_ms: f64, gpu_ms: Option<f64>) {
265 let site = std::panic::Location::caller();
266 let since_mark = unfrozen_ms(
267 self.clock.now_ms() - self.mark_ms,
268 self.mark_frozen_ms,
269 self.clock.frozen_ms(),
270 );
271 let first = self.frames == 0;
272 self.frames += 1;
273 let gpu_ms = gpu_ms.filter(|ms| ms.is_finite() && *ms > 0.0);
274 let work_ms = match gpu_ms {
275 Some(ms) => ms,
276 None if first => 0.0,
277 None => frame_ms.min(since_mark),
278 };
279 self.sources.add(match gpu_ms {
280 Some(_) => Source::Gpu,
281 None if first => Source::Untimed,
282 None => Source::Wall,
283 });
284 if work_ms.is_finite() && work_ms > 0.0 {
285 self.pending_ms += work_ms;
286 self.longest_ms = self.longest_ms.max(work_ms);
287 }
288 self.tally.frame();
289 let now = self.clock.now_ms();
290 if self.pending_ms >= TURN_AFTER_MS || now - self.last_turn_ms >= self.wall_ms {
291 let report = TurnReport {
292 work_ms: self.pending_ms,
293 longest_ms: self.longest_ms,
294 };
295 trace::record(
296 "pace",
297 site,
298 report.work_ms,
299 report.longest_ms,
300 self.sources,
301 );
302 self.sources = Sources::default();
303 self.pending_ms = 0.0;
304 self.longest_ms = 0.0;
305 self.clock.turn(report);
306 trace::turned();
307 self.last_turn_ms = self.clock.now_ms();
308 self.tally.report(self.last_turn_ms);
309 }
310 if let Some(period) = self.period_ms {
311 let now = self.clock.now_ms();
312 let boundary = self.boundary_ms + period;
313 let wait = sleep_to(boundary, now);
314 if wait > 0.0 {
315 self.clock.sleep_ms(wait);
316 self.boundary_ms = boundary;
317 } else {
318 self.boundary_ms = now;
319 }
320 }
321 self.mark_ms = self.clock.now_ms();
322 self.mark_frozen_ms = self.clock.frozen_ms();
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq)]
327pub struct Frames {
328 pub p50: f64,
329 pub p95: f64,
330 pub p99: f64,
331 pub max: f64,
332 pub fps: f64,
333}
334
335impl Frames {
336 pub fn of(sorted_ms: &[f64]) -> Self {
337 let at =
338 |fraction: f64| sorted_ms[((sorted_ms.len() - 1) as f64 * fraction).round() as usize];
339 let p50 = at(0.50);
340 Self {
341 p50,
342 p95: at(0.95),
343 p99: at(0.99),
344 max: sorted_ms[sorted_ms.len() - 1],
345 fps: 1000.0 / p50,
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests;