Skip to repository content831 lines · 26.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:33:51.433Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
profiler.rs
1use itertools::Itertools;
2use scheduler::{Instant, SpawnTime};
3use std::{
4 cell::LazyCell,
5 collections::{HashMap, VecDeque},
6 hash::{DefaultHasher, Hash, Hasher},
7 sync::{
8 Arc,
9 atomic::{AtomicBool, Ordering},
10 },
11 thread::ThreadId,
12 time::Duration,
13};
14
15mod actions;
16pub use actions::{ActionStatistics, ActionTiming, take_action_stats};
17pub(crate) use actions::{save_action_timing, update_running_action};
18
19use serde::{Deserialize, Serialize};
20
21use crate::{SharedString, TasksIncluded, WindowId};
22
23#[cfg(feature = "profiler")]
24#[doc(hidden)]
25pub fn get_all_timings(included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
26 ThreadTaskTimings::collect(upgraded_thread_timings(), included)
27}
28
29#[cfg(feature = "profiler")]
30#[doc(hidden)]
31pub fn get_current_thread_timings(included: TasksIncluded) -> gpui::ThreadTaskTimings {
32 gpui::profiler::get_current_thread_task_timings(included)
33}
34
35#[cfg(feature = "profiler")]
36#[doc(hidden)]
37pub fn take_all_stats(included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
38 ThreadTaskStatistics::collect_and_reset(upgraded_thread_timings(), included)
39}
40
41#[cfg(not(feature = "profiler"))]
42#[doc(hidden)]
43pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
44 Vec::new()
45}
46#[cfg(not(feature = "profiler"))]
47#[doc(hidden)]
48pub fn get_current_thread_timings(_included: TasksIncluded) -> gpui::ThreadTaskTimings {
49 gpui::ThreadTaskTimings {
50 thread_name: None,
51 thread_id: std::thread::current().id(),
52 timings: Vec::new(),
53 stats: TaskStatistics::default(),
54 total_pushed: 0,
55 }
56}
57#[cfg(not(feature = "profiler"))]
58#[doc(hidden)]
59pub fn take_all_stats(_included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
60 Vec::new()
61}
62
63#[doc(hidden)]
64#[derive(Debug, Copy, Clone)]
65pub struct YieldTime(pub Instant);
66
67#[doc(hidden)]
68#[derive(Copy, Clone)]
69pub struct TaskTiming {
70 pub location: &'static core::panic::Location<'static>,
71 pub spawned: SpawnTime,
72 pub start: Instant,
73 pub end: YieldTime,
74}
75
76impl std::fmt::Debug for TaskTiming {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("TaskTiming")
79 .field("location", &self.location)
80 .field("since_spawned", &self.spawned.0.elapsed())
81 .field("last_poll_duration", &self.poll_duration())
82 .field("total_runtime", &self.since_spawn())
83 .finish()
84 }
85}
86
87#[doc(hidden)]
88#[derive(Debug, Copy, Clone)]
89pub struct ActiveTiming {
90 pub location: &'static core::panic::Location<'static>,
91 pub spawned: SpawnTime,
92 pub start: Instant,
93}
94
95impl TaskTiming {
96 /// A task timing with a duration of zero. Any task will replace this in history.
97 pub fn placeholder() -> Self {
98 let now = Instant::now();
99 Self {
100 location: std::panic::Location::caller(),
101 spawned: SpawnTime(now),
102 start: now,
103 end: YieldTime(now),
104 }
105 }
106
107 #[inline(always)]
108 pub fn poll_duration(&self) -> Duration {
109 self.end.0 - self.start
110 }
111
112 #[inline(always)]
113 fn since_spawn(&self) -> Duration {
114 self.end.0 - self.spawned.0
115 }
116}
117
118#[doc(hidden)]
119#[derive(Debug, Clone)]
120pub struct ThreadTaskTimings {
121 pub thread_name: Option<String>,
122 pub thread_id: ThreadId,
123 pub timings: Vec<TaskTiming>,
124 pub stats: TaskStatistics,
125 pub total_pushed: u64,
126}
127
128impl ThreadTaskTimings {
129 /// Convert upgraded per-thread timings into their structured format.
130 pub fn collect(
131 timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
132 included: TasksIncluded,
133 ) -> Vec<Self> {
134 timings
135 .into_iter()
136 .map(|(thread_id, timings)| {
137 let timings = timings.lock();
138 let thread_name = timings.thread_name.clone();
139 let total_pushed = timings.total_pushed;
140 let completed = &timings.timings;
141
142 let mut vec = Vec::with_capacity(completed.len() + 1); // +1 for running task
143 let (s1, s2) = completed.as_slices();
144 vec.extend_from_slice(s1);
145 vec.extend_from_slice(s2);
146 if let TasksIncluded::CompletedAndRunning = included
147 && let Some(running) = timings.running
148 {
149 vec.push(TaskTiming {
150 location: running.location,
151 spawned: running.spawned,
152 start: running.start,
153 end: YieldTime(Instant::now()),
154 })
155 }
156
157 ThreadTaskTimings {
158 thread_name,
159 thread_id,
160 timings: vec,
161 stats: timings.stats.clone(),
162 total_pushed,
163 }
164 })
165 .collect()
166 }
167}
168
169#[doc(hidden)]
170#[derive(Debug)]
171pub struct ThreadTaskStatistics {
172 pub thread_name: Option<String>,
173 pub thread_id: ThreadId,
174 pub stats: TaskStatistics,
175}
176
177impl ThreadTaskStatistics {
178 pub fn collect_and_reset(
179 timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
180 include_running: TasksIncluded,
181 ) -> Vec<Self> {
182 timings
183 .into_iter()
184 .map(|(thread_id, timings)| {
185 let mut timings = timings.lock();
186 let thread_name = timings.thread_name.clone();
187
188 let mut stats = std::mem::take(&mut timings.stats);
189 if let TasksIncluded::CompletedAndRunning = include_running
190 && let Some(ActiveTiming {
191 location,
192 spawned,
193 start,
194 }) = timings.running
195 {
196 let end = YieldTime(Instant::now());
197 let timing = TaskTiming {
198 location,
199 spawned,
200 start,
201 end,
202 };
203 stats.add_runtime(timing);
204 stats.add_yield_timing(timing);
205 }
206
207 Self {
208 thread_name,
209 thread_id,
210 stats,
211 }
212 })
213 .collect()
214 }
215}
216
217/// Serializable variant of [`core::panic::Location`]
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SerializedLocation {
220 /// Name of the source file
221 pub file: SharedString,
222 /// Line in the source file
223 pub line: u32,
224 /// Column in the source file
225 pub column: u32,
226}
227
228impl From<&core::panic::Location<'static>> for SerializedLocation {
229 fn from(value: &core::panic::Location<'static>) -> Self {
230 SerializedLocation {
231 file: value.file().into(),
232 line: value.line(),
233 column: value.column(),
234 }
235 }
236}
237
238/// Serializable variant of [`TaskTiming`]
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct SerializedTaskTiming {
241 /// Location of the timing
242 pub location: SerializedLocation,
243 /// Time at which the measurement was reported in nanoseconds
244 pub start: u128,
245 /// Duration of the measurement in nanoseconds
246 pub duration: u128,
247}
248
249impl SerializedTaskTiming {
250 /// Convert an array of [`TaskTiming`] into their serializable format
251 ///
252 /// # Params
253 ///
254 /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
255 pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec<SerializedTaskTiming> {
256 let serialized = timings
257 .iter()
258 .map(|timing| {
259 let start = timing.start.duration_since(anchor).as_nanos();
260 let duration = timing.end.0.duration_since(timing.start).as_nanos();
261 SerializedTaskTiming {
262 location: timing.location.into(),
263 start,
264 duration,
265 }
266 })
267 .collect::<Vec<_>>();
268
269 serialized
270 }
271
272 /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
273 pub fn from(anchor: Instant, timing: TaskTiming) -> SerializedTaskTiming {
274 let start = timing.start.duration_since(anchor).as_nanos();
275 let duration = timing.end.0.duration_since(timing.start).as_nanos();
276 SerializedTaskTiming {
277 location: timing.location.into(),
278 start,
279 duration,
280 }
281 }
282}
283
284/// Serializable variant of [`ThreadTaskTimings`]
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct SerializedThreadTaskTimings {
287 /// Thread name
288 pub thread_name: Option<String>,
289 /// Hash of the thread id
290 pub thread_id: u64,
291 /// Timing records for this thread
292 pub timings: Vec<SerializedTaskTiming>,
293}
294
295impl SerializedThreadTaskTimings {
296 /// Convert [`ThreadTaskTimings`] into their serializable format
297 ///
298 /// # Params
299 ///
300 /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
301 pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings {
302 let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings);
303
304 let mut hasher = DefaultHasher::new();
305 timings.thread_id.hash(&mut hasher);
306 let thread_id = hasher.finish();
307
308 SerializedThreadTaskTimings {
309 thread_name: timings.thread_name,
310 thread_id,
311 timings: serialized_timings,
312 }
313 }
314}
315
316#[doc(hidden)]
317#[derive(Debug, Clone)]
318pub struct ThreadTimingsDelta {
319 /// Hashed thread id
320 pub thread_id: u64,
321 /// Thread name, if known
322 pub thread_name: Option<String>,
323 /// New timings since the last call. If the circular buffer wrapped around
324 /// since the previous poll, some entries may have been lost.
325 pub new_timings: Vec<SerializedTaskTiming>,
326}
327
328/// Tracks which timing events have already been seen so that callers can request only unseen events.
329#[doc(hidden)]
330pub struct ProfilingCollector {
331 startup_time: Instant,
332 cursors: HashMap<ThreadId, u64>,
333}
334
335impl ProfilingCollector {
336 pub fn new(startup_time: Instant) -> Self {
337 Self {
338 startup_time,
339 cursors: HashMap::default(),
340 }
341 }
342
343 pub fn startup_time(&self) -> Instant {
344 self.startup_time
345 }
346
347 pub fn collect_unseen(
348 &mut self,
349 all_timings: Vec<ThreadTaskTimings>,
350 ) -> Vec<ThreadTimingsDelta> {
351 let mut deltas = Vec::with_capacity(all_timings.len());
352
353 for thread in all_timings {
354 let mut hasher = DefaultHasher::new();
355 thread.thread_id.hash(&mut hasher);
356 let hashed_id = hasher.finish();
357
358 let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0);
359 let buffer_len = thread.timings.len() as u64;
360 let buffer_start = thread.total_pushed.saturating_sub(buffer_len);
361
362 let mut slice = if prev_cursor < buffer_start {
363 // Cursor fell behind the buffer — some entries were evicted.
364 // Return everything still in the buffer.
365 thread.timings.as_slice()
366 } else {
367 let skip = (prev_cursor - buffer_start) as usize;
368 &thread.timings[skip.min(thread.timings.len())..]
369 };
370
371 let cursor_advance = thread.total_pushed;
372 self.cursors.insert(thread.thread_id, cursor_advance);
373
374 if slice.is_empty() {
375 continue;
376 }
377
378 let new_timings = SerializedTaskTiming::convert(self.startup_time, slice);
379
380 deltas.push(ThreadTimingsDelta {
381 thread_id: hashed_id,
382 thread_name: thread.thread_name,
383 new_timings,
384 });
385 }
386
387 deltas
388 }
389
390 pub fn reset(&mut self) {
391 self.cursors.clear();
392 }
393}
394
395// Allow 16MiB of task timing entries.
396// VecDeque grows by doubling its capacity when full, so keep this a power of 2 to avoid wasting
397// memory.
398#[cfg(feature = "profiler")]
399const MAX_TASK_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<TaskTiming>();
400
401#[doc(hidden)]
402pub(crate) type TaskTimings = VecDeque<TaskTiming>;
403
404#[doc(hidden)]
405pub type GuardedTaskTimings = spin::Mutex<ThreadTimings>;
406
407#[doc(hidden)]
408pub struct GlobalThreadTimings {
409 pub thread_id: ThreadId,
410 pub timings: std::sync::Weak<GuardedTaskTimings>,
411}
412
413#[doc(hidden)]
414#[derive(Debug, Clone)]
415pub struct TaskStatistics {
416 pub poll_time_to_beat: Duration,
417 pub runtime_to_beat: Duration,
418 pub longest_poll_times: [TaskTiming; 5],
419 pub longest_runtimes: [TaskTiming; 5],
420}
421
422impl std::fmt::Display for TaskStatistics {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 f.write_str("Tasks that blocked the longest before yielding\n")?;
425 for timing in self.longest_poll_times {
426 f.write_fmt(format_args!(
427 "{:<20} - {}:{}\n",
428 format!("{:?}", timing.poll_duration()),
429 timing.location.file(),
430 timing.location.column()
431 ))?;
432 }
433 f.write_str("Tasks that ran the longest\n")?;
434 for timing in self.longest_runtimes {
435 f.write_fmt(format_args!(
436 "{:<20} - {}:{}\n",
437 format!("{:?}", timing.since_spawn()),
438 timing.location.file(),
439 timing.location.column()
440 ))?;
441 }
442 Ok(())
443 }
444}
445
446impl Default for TaskStatistics {
447 fn default() -> Self {
448 Self {
449 // Do not track polls that are not problematic
450 // this keeps more calls on the fast path
451 poll_time_to_beat: Duration::from_micros(100),
452 runtime_to_beat: Duration::from_micros(100),
453 longest_poll_times: [TaskTiming::placeholder(); 5],
454 longest_runtimes: [TaskTiming::placeholder(); 5],
455 }
456 }
457}
458
459impl TaskStatistics {
460 #[inline(always)]
461 fn add_yield_timing(&mut self, task: TaskTiming) {
462 let yielded_after = task.poll_duration();
463 if yielded_after >= self.poll_time_to_beat {
464 std::hint::cold_path(); // most tasks are not the worst, optimize for that
465 let to_replace = self
466 .longest_poll_times
467 .iter()
468 .position_min_by_key(|task| task.since_spawn())
469 .expect("guarded by the comparison with nth_longest_yield_time");
470 self.longest_poll_times[to_replace] = task;
471
472 self.poll_time_to_beat = self
473 .longest_poll_times
474 .iter()
475 .map(|task| task.since_spawn())
476 .min()
477 .expect("never empty");
478 }
479 }
480
481 #[inline(always)]
482 fn add_runtime(&mut self, task: TaskTiming) {
483 let runtime = task.since_spawn();
484 if runtime >= self.runtime_to_beat {
485 std::hint::cold_path(); // most tasks are not the worst, optimize for that
486 let to_replace = self
487 .longest_runtimes
488 .iter()
489 .position_min_by_key(|task| task.since_spawn())
490 .expect("guarded by the comparison with nth_longest_yield_time");
491 self.longest_runtimes[to_replace] = task;
492
493 self.runtime_to_beat = self
494 .longest_runtimes
495 .iter()
496 .map(|task| task.since_spawn())
497 .min()
498 .expect("never empty");
499 }
500 }
501}
502
503#[doc(hidden)]
504pub static GLOBAL_THREAD_TIMINGS: spin::Mutex<Vec<GlobalThreadTimings>> =
505 spin::Mutex::new(Vec::new());
506
507/// Upgrades all live per-thread timing handles, holding the global registry
508/// lock only for the duration of the upgrades.
509///
510/// The upgraded `Arc`s must never be dropped while `GLOBAL_THREAD_TIMINGS` is
511/// locked: dropping the last strong reference runs [`ThreadTimings::drop`],
512/// which locks `GLOBAL_THREAD_TIMINGS` again and would deadlock the
513/// non-reentrant spinlock. A thread exiting concurrently can hand off its last
514/// reference to us at any time, so callers of this function process (lock,
515/// read, drop) the returned handles only after the global lock is released.
516fn upgraded_thread_timings() -> Vec<(ThreadId, Arc<GuardedTaskTimings>)> {
517 let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock();
518 global_thread_timings
519 .iter()
520 .filter_map(|t| Some((t.thread_id, t.timings.upgrade()?)))
521 .collect()
522}
523
524thread_local! {
525 #[doc(hidden)]
526 pub static THREAD_TIMINGS: LazyCell<Arc<GuardedTaskTimings>> = LazyCell::new(|| {
527 let current_thread = std::thread::current();
528 let thread_name = current_thread.name();
529 let thread_id = current_thread.id();
530 let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id);
531 let timings = Arc::new(spin::Mutex::new(timings));
532
533 {
534 let timings = Arc::downgrade(&timings);
535 let global_timings = GlobalThreadTimings {
536 thread_id: std::thread::current().id(),
537 timings,
538 };
539 GLOBAL_THREAD_TIMINGS.lock().push(global_timings);
540 }
541
542 timings
543 });
544}
545
546#[doc(hidden)]
547pub struct ThreadTimings {
548 pub thread_name: Option<String>,
549 pub thread_id: ThreadId,
550 pub timings: TaskTimings,
551 pub running: Option<ActiveTiming>,
552 pub stats: TaskStatistics,
553 pub total_pushed: u64,
554}
555
556impl ThreadTimings {
557 pub fn new(thread_name: Option<String>, thread_id: ThreadId) -> Self {
558 ThreadTimings {
559 thread_name,
560 thread_id,
561 timings: TaskTimings::new(),
562 stats: TaskStatistics::default(),
563 total_pushed: 0,
564 running: None,
565 }
566 }
567
568 #[cfg(feature = "profiler")]
569 pub fn update_running_task(
570 &mut self,
571 spawned: SpawnTime,
572 location: &'static std::panic::Location<'_>,
573 ) {
574 let start = Instant::now();
575 self.running = Some(ActiveTiming {
576 spawned,
577 location,
578 start,
579 });
580 }
581 #[cfg(not(feature = "profiler"))]
582 pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {}
583
584 #[cfg(feature = "profiler")]
585 pub fn save_task_timing(&mut self, ended: YieldTime) {
586 let ActiveTiming {
587 location,
588 start,
589 spawned,
590 } = self
591 .running
592 .take()
593 .expect("this function is only ever called after register_task_start");
594
595 let timing = TaskTiming {
596 location,
597 spawned,
598 start,
599 end: ended,
600 };
601 self.stats.add_yield_timing(timing);
602 self.stats.add_runtime(timing);
603
604 if trace_enabled() {
605 std::hint::cold_path(); // optimize for when the profiling is off
606 if self.timings.len() >= MAX_TASK_TIMINGS {
607 self.timings.pop_front();
608 }
609 self.timings.push_back(timing);
610 self.total_pushed += 1;
611 }
612 }
613 #[cfg(not(feature = "profiler"))]
614 pub fn save_task_timing(&mut self, _: YieldTime) {}
615
616 // Running tasks are included in the reliability trace, which is written
617 // whenever the foreground executor makes no progress for > n seconds
618 pub fn get_thread_task_timings(&self, includes: TasksIncluded) -> ThreadTaskTimings {
619 ThreadTaskTimings {
620 thread_name: self.thread_name.clone(),
621 thread_id: self.thread_id,
622 timings: self
623 .timings
624 .iter()
625 .cloned()
626 .chain(
627 self.running
628 .filter(|_| matches!(includes, TasksIncluded::CompletedAndRunning))
629 .map(|running| TaskTiming {
630 spawned: running.spawned,
631 location: running.location,
632 start: running.start,
633 end: YieldTime(Instant::now()),
634 }),
635 )
636 .collect(),
637 stats: self.stats.clone(),
638 total_pushed: self.total_pushed,
639 }
640 }
641}
642
643impl Drop for ThreadTimings {
644 fn drop(&mut self) {
645 let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock();
646
647 let Some((index, _)) = thread_timings
648 .iter()
649 .enumerate()
650 .find(|(_, t)| t.thread_id == self.thread_id)
651 else {
652 return;
653 };
654 thread_timings.swap_remove(index);
655 }
656}
657
658#[doc(hidden)]
659pub fn update_running_task(spawned: SpawnTime, location: &'static std::panic::Location<'_>) {
660 THREAD_TIMINGS.with(|timings| {
661 timings.lock().update_running_task(spawned, location);
662 });
663}
664
665#[doc(hidden)]
666pub fn save_task_timing() {
667 let yielded_at = YieldTime(Instant::now());
668 THREAD_TIMINGS.with(|timings| {
669 timings.lock().save_task_timing(yielded_at);
670 });
671}
672
673#[doc(hidden)]
674pub fn get_current_thread_task_timings(include_running: TasksIncluded) -> ThreadTaskTimings {
675 THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings(include_running))
676}
677
678static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false);
679
680/// Enables or disables task timing trace collection at runtime.
681///
682/// When transitioning from enabled to disabled, `add_task_timing` becomes a
683/// cheaper since only cheap statistics are gathered. The existing per-thread
684/// buffers for traces are cleared so stale data isn't reported after a later
685/// re-enable. Calls with the current value are a no-op.
686pub fn set_trace_enabled(enabled: bool) -> bool {
687 if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled {
688 return false;
689 }
690
691 if !enabled {
692 for (_, timings) in upgraded_thread_timings() {
693 let mut timings = timings.lock();
694 timings.timings.clear();
695 timings.timings.shrink_to_fit();
696 timings.total_pushed = 0;
697 }
698 }
699 true
700}
701
702/// Returns whether task timing tracing is enabled.
703pub fn trace_enabled() -> bool {
704 PROFILER_ENABLED.load(Ordering::Relaxed)
705}
706
707/// Timing for a single drawn window frame.
708#[derive(Debug, Copy, Clone)]
709pub struct FrameTiming {
710 /// The window that was drawn.
711 pub window_id: WindowId,
712 /// When the frame first became dirty (its first invalidation). `None` if
713 /// frame tracing was not yet enabled when the invalidation occurred.
714 pub dirty_at: Option<Instant>,
715 /// Number of invalidations coalesced into this frame.
716 pub invalidations: u64,
717 /// When `Window::draw` started.
718 pub draw_start: Instant,
719 /// When `Window::draw` finished.
720 pub draw_end: Instant,
721}
722
723impl FrameTiming {
724 /// Time spent inside `Window::draw`.
725 pub fn draw_duration(&self) -> Duration {
726 self.draw_end.duration_since(self.draw_start)
727 }
728
729 /// Time from the frame's first invalidation to the end of its draw, if the
730 /// first invalidation was observed.
731 pub fn dirty_to_draw_duration(&self) -> Option<Duration> {
732 self.dirty_at
733 .map(|dirty_at| self.draw_end.duration_since(dirty_at))
734 }
735}
736
737// Allow 16MiB of frame timing entries.
738const MAX_FRAME_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<FrameTiming>();
739
740struct FrameTimings {
741 timings: VecDeque<FrameTiming>,
742 total_pushed: u64,
743}
744
745static FRAME_TIMINGS: spin::Mutex<FrameTimings> = spin::Mutex::new(FrameTimings {
746 timings: VecDeque::new(),
747 total_pushed: 0,
748});
749
750static FRAME_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
751
752/// Enables or disables frame timing collection at runtime.
753///
754/// When transitioning from enabled to disabled, the buffered frame timings are
755/// cleared so stale data isn't reported after a later re-enable. Returns false
756/// if the value was unchanged.
757pub fn set_frame_trace_enabled(enabled: bool) -> bool {
758 if FRAME_TRACE_ENABLED.swap(enabled, Ordering::AcqRel) == enabled {
759 return false;
760 }
761
762 if !enabled {
763 let mut frames = FRAME_TIMINGS.lock();
764 frames.timings.clear();
765 frames.timings.shrink_to_fit();
766 frames.total_pushed = 0;
767 }
768 true
769}
770
771/// Returns whether frame timing collection is enabled.
772pub fn frame_trace_enabled() -> bool {
773 FRAME_TRACE_ENABLED.load(Ordering::Relaxed)
774}
775
776/// Records the timing of a drawn window frame.
777///
778/// No-op unless frame tracing is enabled via [`set_frame_trace_enabled`].
779pub fn record_frame_timing(timing: FrameTiming) {
780 if !frame_trace_enabled() {
781 return;
782 }
783 std::hint::cold_path(); // optimize for when profiling is off
784
785 let mut frames = FRAME_TIMINGS.lock();
786 if frames.timings.len() >= MAX_FRAME_TIMINGS {
787 frames.timings.pop_front();
788 }
789 frames.timings.push_back(timing);
790 frames.total_pushed += 1;
791}
792
793/// Drains frame timings recorded after this collector was created, tracking a
794/// cursor so each call to [`Self::collect_unseen`] returns only new entries.
795pub struct FrameTimingCollector {
796 cursor: u64,
797}
798
799impl Default for FrameTimingCollector {
800 fn default() -> Self {
801 Self::new()
802 }
803}
804
805impl FrameTimingCollector {
806 /// Creates a collector that only sees frames recorded from this point on.
807 pub fn new() -> Self {
808 Self {
809 cursor: FRAME_TIMINGS.lock().total_pushed,
810 }
811 }
812
813 /// Returns frame timings recorded since the previous call (or since the
814 /// collector was created). If the ring buffer wrapped around since the
815 /// previous poll, the evicted entries are lost.
816 pub fn collect_unseen(&mut self) -> Vec<FrameTiming> {
817 let frames = FRAME_TIMINGS.lock();
818 let buffer_len = frames.timings.len() as u64;
819 let buffer_start = frames.total_pushed.saturating_sub(buffer_len);
820 let skip = self.cursor.saturating_sub(buffer_start) as usize;
821 let unseen = frames
822 .timings
823 .iter()
824 .skip(skip.min(frames.timings.len()))
825 .copied()
826 .collect();
827 self.cursor = frames.total_pushed;
828 unseen
829 }
830}
831