1#[cfg(feature = "profiler")]
2use hdrhistogram::Histogram;
3use itertools::Itertools;
4use scheduler::{Instant, SpawnTime};
5#[cfg(feature = "profiler")]
6use smallvec::SmallVec;
7use std::{
8 cell::LazyCell,
9 collections::{HashMap, VecDeque},
10 hash::{DefaultHasher, Hash, Hasher},
11 sync::{
12 Arc,
13 atomic::{AtomicU64, Ordering},
14 },
15 thread::ThreadId,
16 time::Duration,
17};
18
19#[inline(always)]
22pub(crate) fn cold_path() {}
23
24mod actions;
25#[cfg(feature = "profiler")]
26pub mod hang;
27#[cfg(feature = "profiler")]
28pub mod journal;
29pub use actions::{ActionStatistics, ActionTiming, take_action_stats};
30
31use serde::{Deserialize, Serialize};
32
33#[cfg(feature = "profiler")]
34use crate::{Action, App, WindowId};
35use crate::{SharedString, TasksIncluded};
36
37#[cfg(feature = "profiler")]
38#[doc(hidden)]
39pub fn get_all_timings(included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
40 ThreadTaskTimings::collect(upgraded_thread_timings(), included)
41}
42
43#[cfg(feature = "profiler")]
44#[doc(hidden)]
45pub fn get_current_thread_timings(included: TasksIncluded) -> gpui::ThreadTaskTimings {
46 gpui::profiler::get_current_thread_task_timings(included)
47}
48
49#[cfg(feature = "profiler")]
50#[doc(hidden)]
51pub fn take_all_stats(included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
52 ThreadTaskStatistics::collect_and_reset(upgraded_thread_timings(), included)
53}
54
55#[cfg(not(feature = "profiler"))]
56#[doc(hidden)]
57pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
58 Vec::new()
59}
60#[cfg(not(feature = "profiler"))]
61#[doc(hidden)]
62pub fn get_current_thread_timings(_included: TasksIncluded) -> gpui::ThreadTaskTimings {
63 gpui::ThreadTaskTimings {
64 thread_name: None,
65 thread_id: std::thread::current().id(),
66 timings: Vec::new(),
67 stats: TaskStatistics::default(),
68 total_pushed: 0,
69 }
70}
71#[cfg(not(feature = "profiler"))]
72#[doc(hidden)]
73pub fn take_all_stats(_included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
74 Vec::new()
75}
76
77#[doc(hidden)]
78#[derive(Debug, Copy, Clone)]
79pub struct YieldTime(pub Instant);
80
81#[doc(hidden)]
82#[derive(Copy, Clone)]
83pub struct TaskTiming {
84 pub location: &'static core::panic::Location<'static>,
85 pub spawned: SpawnTime,
86 pub start: Instant,
87 pub end: YieldTime,
88}
89
90impl std::fmt::Debug for TaskTiming {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("TaskTiming")
93 .field("location", &self.location)
94 .field("since_spawned", &self.spawned.0.elapsed())
95 .field("last_poll_duration", &self.poll_duration())
96 .field("total_runtime", &self.since_spawn())
97 .finish()
98 }
99}
100
101#[doc(hidden)]
102#[derive(Debug, Copy, Clone)]
103pub struct ActiveTiming {
104 pub location: &'static core::panic::Location<'static>,
105 pub spawned: SpawnTime,
106 pub start: Instant,
107}
108
109impl TaskTiming {
110 pub fn placeholder() -> Self {
112 let now = Instant::now();
113 Self {
114 location: std::panic::Location::caller(),
115 spawned: SpawnTime(now),
116 start: now,
117 end: YieldTime(now),
118 }
119 }
120
121 #[inline(always)]
122 pub fn poll_duration(&self) -> Duration {
123 self.end.0 - self.start
124 }
125
126 #[inline(always)]
127 fn since_spawn(&self) -> Duration {
128 self.end.0 - self.spawned.0
129 }
130}
131
132#[doc(hidden)]
133#[derive(Debug, Clone)]
134pub struct ThreadTaskTimings {
135 pub thread_name: Option<String>,
136 pub thread_id: ThreadId,
137 pub timings: Vec<TaskTiming>,
138 pub stats: TaskStatistics,
139 pub total_pushed: u64,
140}
141
142impl ThreadTaskTimings {
143 pub fn collect(
145 timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
146 included: TasksIncluded,
147 ) -> Vec<Self> {
148 timings
149 .into_iter()
150 .map(|(thread_id, timings)| {
151 let timings = timings.lock();
152 let thread_name = timings.thread_name.clone();
153 let total_pushed = timings.total_pushed;
154 let completed = &timings.timings;
155
156 let mut vec = Vec::with_capacity(completed.len() + 1); let (s1, s2) = completed.as_slices();
158 vec.extend_from_slice(s1);
159 vec.extend_from_slice(s2);
160 if let TasksIncluded::CompletedAndRunning = included
161 && let Some(running) = timings.running
162 {
163 vec.push(TaskTiming {
164 location: running.location,
165 spawned: running.spawned,
166 start: running.start,
167 end: YieldTime(Instant::now()),
168 })
169 }
170
171 ThreadTaskTimings {
172 thread_name,
173 thread_id,
174 timings: vec,
175 stats: timings.stats.clone(),
176 total_pushed,
177 }
178 })
179 .collect()
180 }
181}
182
183#[doc(hidden)]
184#[derive(Debug)]
185pub struct ThreadTaskStatistics {
186 pub thread_name: Option<String>,
187 pub thread_id: ThreadId,
188 pub stats: TaskStatistics,
189}
190
191impl ThreadTaskStatistics {
192 pub fn collect_and_reset(
193 timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
194 include_running: TasksIncluded,
195 ) -> Vec<Self> {
196 timings
197 .into_iter()
198 .map(|(thread_id, timings)| {
199 let mut timings = timings.lock();
200 let thread_name = timings.thread_name.clone();
201
202 let mut stats = std::mem::take(&mut timings.stats);
203 if let TasksIncluded::CompletedAndRunning = include_running
204 && let Some(ActiveTiming {
205 location,
206 spawned,
207 start,
208 }) = timings.running
209 {
210 let end = YieldTime(Instant::now());
211 let timing = TaskTiming {
212 location,
213 spawned,
214 start,
215 end,
216 };
217 stats.add_runtime(timing);
218 stats.add_yield_timing(timing);
219 }
220
221 Self {
222 thread_name,
223 thread_id,
224 stats,
225 }
226 })
227 .collect()
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SerializedLocation {
234 pub file: SharedString,
236 pub line: u32,
238 pub column: u32,
240}
241
242impl From<&core::panic::Location<'static>> for SerializedLocation {
243 fn from(value: &core::panic::Location<'static>) -> Self {
244 SerializedLocation {
245 file: value.file().into(),
246 line: value.line(),
247 column: value.column(),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct SerializedTaskTiming {
255 pub location: SerializedLocation,
257 pub start: u128,
259 pub duration: u128,
261}
262
263impl SerializedTaskTiming {
264 pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec<SerializedTaskTiming> {
270 let serialized = timings
271 .iter()
272 .map(|timing| {
273 let start = timing.start.duration_since(anchor).as_nanos();
274 let duration = timing.end.0.duration_since(timing.start).as_nanos();
275 SerializedTaskTiming {
276 location: timing.location.into(),
277 start,
278 duration,
279 }
280 })
281 .collect::<Vec<_>>();
282
283 serialized
284 }
285
286 pub fn from(anchor: Instant, timing: TaskTiming) -> SerializedTaskTiming {
288 let start = timing.start.duration_since(anchor).as_nanos();
289 let duration = timing.end.0.duration_since(timing.start).as_nanos();
290 SerializedTaskTiming {
291 location: timing.location.into(),
292 start,
293 duration,
294 }
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct SerializedThreadTaskTimings {
301 pub thread_name: Option<String>,
303 pub thread_id: u64,
305 pub timings: Vec<SerializedTaskTiming>,
307}
308
309impl SerializedThreadTaskTimings {
310 pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings {
316 let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings);
317
318 let mut hasher = DefaultHasher::new();
319 timings.thread_id.hash(&mut hasher);
320 let thread_id = hasher.finish();
321
322 SerializedThreadTaskTimings {
323 thread_name: timings.thread_name,
324 thread_id,
325 timings: serialized_timings,
326 }
327 }
328}
329
330#[doc(hidden)]
331#[derive(Debug, Clone)]
332pub struct ThreadTimingsDelta {
333 pub thread_id: u64,
335 pub thread_name: Option<String>,
337 pub new_timings: Vec<SerializedTaskTiming>,
340}
341
342#[doc(hidden)]
344pub struct ProfilingCollector {
345 startup_time: Instant,
346 cursors: HashMap<ThreadId, u64>,
347}
348
349impl ProfilingCollector {
350 pub fn new(startup_time: Instant) -> Self {
351 Self {
352 startup_time,
353 cursors: HashMap::default(),
354 }
355 }
356
357 pub fn startup_time(&self) -> Instant {
358 self.startup_time
359 }
360
361 pub fn collect_unseen(
362 &mut self,
363 all_timings: Vec<ThreadTaskTimings>,
364 ) -> Vec<ThreadTimingsDelta> {
365 let mut deltas = Vec::with_capacity(all_timings.len());
366
367 for thread in all_timings {
368 let mut hasher = DefaultHasher::new();
369 thread.thread_id.hash(&mut hasher);
370 let hashed_id = hasher.finish();
371
372 let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0);
373 let buffer_len = thread.timings.len() as u64;
374 let buffer_start = thread.total_pushed.saturating_sub(buffer_len);
375
376 let mut slice = if prev_cursor < buffer_start {
377 thread.timings.as_slice()
380 } else {
381 let skip = (prev_cursor - buffer_start) as usize;
382 &thread.timings[skip.min(thread.timings.len())..]
383 };
384
385 let cursor_advance = thread.total_pushed;
386 self.cursors.insert(thread.thread_id, cursor_advance);
387
388 if slice.is_empty() {
389 continue;
390 }
391
392 let new_timings = SerializedTaskTiming::convert(self.startup_time, slice);
393
394 deltas.push(ThreadTimingsDelta {
395 thread_id: hashed_id,
396 thread_name: thread.thread_name,
397 new_timings,
398 });
399 }
400
401 deltas
402 }
403
404 pub fn reset(&mut self) {
405 self.cursors.clear();
406 }
407}
408
409#[cfg(feature = "profiler")]
413const MAX_TASK_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<TaskTiming>();
414
415#[doc(hidden)]
416pub(crate) type TaskTimings = VecDeque<TaskTiming>;
417
418#[doc(hidden)]
419pub type GuardedTaskTimings = spin::Mutex<ThreadTimings>;
420
421#[doc(hidden)]
422pub struct GlobalThreadTimings {
423 pub thread_id: ThreadId,
424 pub timings: std::sync::Weak<GuardedTaskTimings>,
425}
426
427#[doc(hidden)]
428#[derive(Debug, Clone)]
429pub struct TaskStatistics {
430 pub poll_time_to_beat: Duration,
431 pub runtime_to_beat: Duration,
432 pub longest_poll_times: [TaskTiming; 5],
433 pub longest_runtimes: [TaskTiming; 5],
434}
435
436impl std::fmt::Display for TaskStatistics {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 f.write_str("Tasks that blocked the longest before yielding\n")?;
439 for timing in self.longest_poll_times {
440 f.write_fmt(format_args!(
441 "{:<20} - {}:{}\n",
442 format!("{:?}", timing.poll_duration()),
443 timing.location.file(),
444 timing.location.column()
445 ))?;
446 }
447 f.write_str("Tasks that ran the longest\n")?;
448 for timing in self.longest_runtimes {
449 f.write_fmt(format_args!(
450 "{:<20} - {}:{}\n",
451 format!("{:?}", timing.since_spawn()),
452 timing.location.file(),
453 timing.location.column()
454 ))?;
455 }
456 Ok(())
457 }
458}
459
460impl Default for TaskStatistics {
461 fn default() -> Self {
462 Self {
463 poll_time_to_beat: Duration::from_micros(100),
466 runtime_to_beat: Duration::from_micros(100),
467 longest_poll_times: [TaskTiming::placeholder(); 5],
468 longest_runtimes: [TaskTiming::placeholder(); 5],
469 }
470 }
471}
472
473impl TaskStatistics {
474 #[inline(always)]
475 fn add_yield_timing(&mut self, task: TaskTiming) {
476 let yielded_after = task.poll_duration();
477 if yielded_after >= self.poll_time_to_beat {
478 cold_path(); let to_replace = self
480 .longest_poll_times
481 .iter()
482 .position_min_by_key(|task| task.since_spawn())
483 .expect("guarded by the comparison with nth_longest_yield_time");
484 self.longest_poll_times[to_replace] = task;
485
486 self.poll_time_to_beat = self
487 .longest_poll_times
488 .iter()
489 .map(|task| task.since_spawn())
490 .min()
491 .expect("never empty");
492 }
493 }
494
495 #[inline(always)]
496 fn add_runtime(&mut self, task: TaskTiming) {
497 let runtime = task.since_spawn();
498 if runtime >= self.runtime_to_beat {
499 cold_path(); let to_replace = self
501 .longest_runtimes
502 .iter()
503 .position_min_by_key(|task| task.since_spawn())
504 .expect("guarded by the comparison with nth_longest_yield_time");
505 self.longest_runtimes[to_replace] = task;
506
507 self.runtime_to_beat = self
508 .longest_runtimes
509 .iter()
510 .map(|task| task.since_spawn())
511 .min()
512 .expect("never empty");
513 }
514 }
515}
516
517#[doc(hidden)]
518pub static GLOBAL_THREAD_TIMINGS: spin::Mutex<Vec<GlobalThreadTimings>> =
519 spin::Mutex::new(Vec::new());
520
521fn upgraded_thread_timings() -> Vec<(ThreadId, Arc<GuardedTaskTimings>)> {
531 let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock();
532 global_thread_timings
533 .iter()
534 .filter_map(|t| Some((t.thread_id, t.timings.upgrade()?)))
535 .collect()
536}
537
538thread_local! {
539 #[doc(hidden)]
540 pub static THREAD_TIMINGS: LazyCell<Arc<GuardedTaskTimings>> = LazyCell::new(|| {
541 let current_thread = std::thread::current();
542 let thread_name = current_thread.name();
543 let thread_id = current_thread.id();
544 let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id);
545 let timings = Arc::new(spin::Mutex::new(timings));
546
547 {
548 let timings = Arc::downgrade(&timings);
549 let global_timings = GlobalThreadTimings {
550 thread_id: std::thread::current().id(),
551 timings,
552 };
553 GLOBAL_THREAD_TIMINGS.lock().push(global_timings);
554 }
555
556 timings
557 });
558}
559
560#[doc(hidden)]
561pub struct ThreadTimings {
562 pub thread_name: Option<String>,
563 pub thread_id: ThreadId,
564 pub timings: TaskTimings,
565 pub running: Option<ActiveTiming>,
566 pub stats: TaskStatistics,
567 pub total_pushed: u64,
568}
569
570impl ThreadTimings {
571 pub fn new(thread_name: Option<String>, thread_id: ThreadId) -> Self {
572 ThreadTimings {
573 thread_name,
574 thread_id,
575 timings: TaskTimings::new(),
576 stats: TaskStatistics::default(),
577 total_pushed: 0,
578 running: None,
579 }
580 }
581
582 #[cfg(feature = "profiler")]
583 pub fn update_running_task(
584 &mut self,
585 spawned: SpawnTime,
586 location: &'static std::panic::Location<'_>,
587 ) {
588 let start = Instant::now();
589 self.running = Some(ActiveTiming {
590 spawned,
591 location,
592 start,
593 });
594 }
595 #[cfg(not(feature = "profiler"))]
596 pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {}
597
598 #[cfg(feature = "profiler")]
599 pub fn save_task_timing(&mut self, ended: YieldTime) -> TaskTiming {
600 let ActiveTiming {
601 location,
602 start,
603 spawned,
604 } = self
605 .running
606 .take()
607 .expect("this function is only ever called after register_task_start");
608
609 let timing = TaskTiming {
610 location,
611 spawned,
612 start,
613 end: ended,
614 };
615 self.stats.add_yield_timing(timing);
616 self.stats.add_runtime(timing);
617
618 if trace_enabled() {
619 cold_path(); if self.timings.len() >= MAX_TASK_TIMINGS {
621 self.timings.pop_front();
622 }
623 self.timings.push_back(timing);
624 self.total_pushed += 1;
625 }
626 timing
627 }
628 #[cfg(not(feature = "profiler"))]
629 pub fn save_task_timing(&mut self, _: YieldTime) {}
630
631 pub fn get_thread_task_timings(&self, includes: TasksIncluded) -> ThreadTaskTimings {
634 ThreadTaskTimings {
635 thread_name: self.thread_name.clone(),
636 thread_id: self.thread_id,
637 timings: self
638 .timings
639 .iter()
640 .cloned()
641 .chain(
642 self.running
643 .filter(|_| matches!(includes, TasksIncluded::CompletedAndRunning))
644 .map(|running| TaskTiming {
645 spawned: running.spawned,
646 location: running.location,
647 start: running.start,
648 end: YieldTime(Instant::now()),
649 }),
650 )
651 .collect(),
652 stats: self.stats.clone(),
653 total_pushed: self.total_pushed,
654 }
655 }
656}
657
658impl Drop for ThreadTimings {
659 fn drop(&mut self) {
660 let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock();
661
662 let Some((index, _)) = thread_timings
663 .iter()
664 .enumerate()
665 .find(|(_, t)| t.thread_id == self.thread_id)
666 else {
667 return;
668 };
669 thread_timings.swap_remove(index);
670 }
671}
672
673#[doc(hidden)]
674pub fn update_running_task(spawned: SpawnTime, location: &'static std::panic::Location<'_>) {
675 #[cfg(feature = "profiler")]
676 journal::begin_foreground_turn();
677 THREAD_TIMINGS.with(|timings| {
678 timings.lock().update_running_task(spawned, location);
679 });
680}
681
682#[doc(hidden)]
683pub fn save_task_timing() {
684 let yielded_at = YieldTime(Instant::now());
685 #[cfg(feature = "profiler")]
686 {
687 let timing = THREAD_TIMINGS.with(|timings| timings.lock().save_task_timing(yielded_at));
688 journal::record_task_poll(timing);
689 }
690 #[cfg(not(feature = "profiler"))]
691 THREAD_TIMINGS.with(|timings| {
692 timings.lock().save_task_timing(yielded_at);
693 });
694}
695
696#[doc(hidden)]
697pub fn get_current_thread_task_timings(include_running: TasksIncluded) -> ThreadTaskTimings {
698 THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings(include_running))
699}
700
701const TRACE_SETTING_ENABLED: u64 = 1 << 63;
702const TRACE_SCOPE_COUNT_MASK: u64 = TRACE_SETTING_ENABLED - 1;
703static TRACE_STATE: AtomicU64 = AtomicU64::new(0);
704
705pub fn set_trace_enabled(enabled: bool) -> bool {
713 let mut state = TRACE_STATE.load(Ordering::Acquire);
714 loop {
715 let was_enabled = state & TRACE_SETTING_ENABLED != 0;
716 if was_enabled == enabled {
717 return false;
718 }
719
720 let next_state = if enabled {
721 state | TRACE_SETTING_ENABLED
722 } else {
723 state & TRACE_SCOPE_COUNT_MASK
724 };
725 match TRACE_STATE.compare_exchange_weak(
726 state,
727 next_state,
728 Ordering::AcqRel,
729 Ordering::Acquire,
730 ) {
731 Ok(_) => {
732 if next_state == 0 {
733 clear_trace_buffers();
734 }
735 return true;
736 }
737 Err(updated_state) => state = updated_state,
738 }
739 }
740}
741
742#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
743pub(crate) struct TraceGuard;
744
745#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
746pub(crate) fn trace_scope() -> TraceGuard {
747 let incremented = TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| {
748 (state & TRACE_SCOPE_COUNT_MASK < TRACE_SCOPE_COUNT_MASK).then_some(state + 1)
749 });
750 assert!(incremented.is_ok(), "too many active profiler trace scopes");
751 TraceGuard
752}
753
754#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
755impl Drop for TraceGuard {
756 fn drop(&mut self) {
757 let previous_state =
758 TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| {
759 (state & TRACE_SCOPE_COUNT_MASK > 0).then_some(state - 1)
760 });
761 match previous_state {
762 Ok(1) => clear_trace_buffers(),
763 Ok(_) => {}
764 Err(_) => debug_assert!(false, "profiler trace scope count underflowed"),
765 }
766 }
767}
768
769pub fn trace_enabled() -> bool {
771 TRACE_STATE.load(Ordering::Relaxed) != 0
772}
773
774fn clear_trace_buffers() {
775 for (_, timings) in upgraded_thread_timings() {
776 let mut timings = timings.lock();
777 timings.timings.clear();
778 timings.timings.shrink_to_fit();
779 timings.total_pushed = 0;
780 }
781 #[cfg(feature = "profiler")]
782 {
783 let mut frames = FRAME_TIMINGS.lock();
784 frames.timings.clear();
785 frames.timings.shrink_to_fit();
786 frames.total_pushed = 0;
787 }
788}
789
790#[cfg(feature = "profiler")]
792#[derive(Debug, Copy, Clone)]
793pub struct FrameTiming {
794 pub window_id: WindowId,
796 pub dirty_at: Option<Instant>,
799 pub invalidations: u64,
801 pub draw_start: Instant,
803 pub draw_end: Instant,
805}
806
807#[cfg(feature = "profiler")]
808impl FrameTiming {
809 pub fn draw_duration(&self) -> Duration {
811 self.draw_end.duration_since(self.draw_start)
812 }
813
814 pub fn dirty_to_draw_duration(&self) -> Option<Duration> {
817 self.dirty_at
818 .map(|dirty_at| self.draw_end.duration_since(dirty_at))
819 }
820}
821
822#[cfg(feature = "profiler")]
824#[derive(Debug, Copy, Clone)]
825pub struct PresentTiming {
826 pub window_id: WindowId,
828 pub present_start: Instant,
830 pub present_end: Instant,
832 pub animation_interval: Option<Duration>,
835}
836
837#[cfg(feature = "profiler")]
838impl PresentTiming {
839 pub fn present_duration(&self) -> Duration {
841 self.present_end.duration_since(self.present_start)
842 }
843}
844
845#[cfg(feature = "profiler")]
847#[derive(Debug, Copy, Clone)]
848pub enum FrameEvent {
849 Draw(FrameTiming),
851 Present(PresentTiming),
853}
854
855#[cfg(feature = "profiler")]
858#[derive(Clone)]
859pub struct FrameDurationSnapshot {
860 pub dirty_to_present_histogram: Histogram<u64>,
862 pub draw_duration_histogram: Histogram<u64>,
864 pub present_interval_histogram: Histogram<u64>,
867}
868
869#[cfg(feature = "profiler")]
872#[derive(Clone)]
873pub struct InputLatencySnapshot {
874 pub latency_histogram: Histogram<u64>,
876 pub events_per_frame_histogram: Histogram<u64>,
878 pub mid_draw_events_dropped: u64,
881}
882
883#[cfg(feature = "profiler")]
884enum WindowActivity {
885 Input {
886 started_at: Instant,
887 kind: &'static str,
888 },
889 Draw {
890 started_at: Instant,
891 },
892}
893
894#[cfg(feature = "profiler")]
900pub struct WindowProfiler {
901 window_id: WindowId,
902 active_activities: SmallVec<[WindowActivity; 4]>,
903 active_actions: SmallVec<[(&'static str, Instant); 2]>,
904 dirty_to_present_histogram: Histogram<u64>,
905 draw_duration_histogram: Histogram<u64>,
906 present_interval_histogram: Histogram<u64>,
907 first_input_at: Option<Instant>,
908 pending_input_count: u64,
909 input_latency_histogram: Histogram<u64>,
910 events_per_frame_histogram: Histogram<u64>,
911 mid_draw_events_dropped: u64,
912 last_present_at: Option<Instant>,
913 animating_at_last_present: bool,
914 pending_frame: Option<FrameTiming>,
915}
916
917#[cfg(feature = "profiler")]
918impl WindowProfiler {
919 pub fn new(window_id: WindowId) -> anyhow::Result<Self> {
921 let profiler = Self {
922 window_id,
923 active_activities: SmallVec::new(),
924 active_actions: SmallVec::new(),
925 dirty_to_present_histogram: Histogram::new(3).map_err(|error| {
926 anyhow::anyhow!("Failed to create dirty-to-present histogram: {error}")
927 })?,
928 draw_duration_histogram: Histogram::new(3).map_err(|error| {
929 anyhow::anyhow!("Failed to create draw duration histogram: {error}")
930 })?,
931 present_interval_histogram: Histogram::new(3).map_err(|error| {
932 anyhow::anyhow!("Failed to create present interval histogram: {error}")
933 })?,
934 first_input_at: None,
935 pending_input_count: 0,
936 input_latency_histogram: Histogram::new(3).map_err(|error| {
937 anyhow::anyhow!("Failed to create input latency histogram: {error}")
938 })?,
939 events_per_frame_histogram: Histogram::new(3).map_err(|error| {
940 anyhow::anyhow!("Failed to create events per frame histogram: {error}")
941 })?,
942 mid_draw_events_dropped: 0,
943 last_present_at: None,
944 animating_at_last_present: false,
945 pending_frame: None,
946 };
947 journal::record_frame_pending(window_id, Instant::now());
948 Ok(profiler)
949 }
950
951 pub fn begin_input(&mut self, kind: &'static str) {
954 journal::begin_foreground_turn();
955 self.active_activities.push(WindowActivity::Input {
956 started_at: Instant::now(),
957 kind,
958 });
959 }
960
961 pub fn end_input(&mut self, caused_invalidation: bool) {
963 let Some(WindowActivity::Input { started_at, kind }) = self.active_activities.pop() else {
964 debug_assert!(false, "input activity must be the current window activity");
965 journal::end_foreground_turn();
966 return;
967 };
968
969 if !journal::power_interrupted_since(started_at) {
970 journal::record_input(journal::InputTiming {
971 kind,
972 start: started_at,
973 end: Instant::now(),
974 caused_invalidation,
975 });
976 }
977 journal::end_foreground_turn();
978
979 if !caused_invalidation || !journal::frame_sample_is_valid(self.window_id, started_at) {
980 return;
981 }
982 if self
983 .first_input_at
984 .is_some_and(|at| !journal::frame_sample_is_valid(self.window_id, at))
985 {
986 self.first_input_at = None;
987 self.pending_input_count = 0;
988 }
989
990 let arrived_during_draw = self
991 .active_activities
992 .iter()
993 .any(|activity| matches!(activity, WindowActivity::Draw { .. }));
994 if arrived_during_draw {
995 self.mid_draw_events_dropped += 1;
996 } else {
997 self.first_input_at.get_or_insert(started_at);
998 self.pending_input_count += 1;
999 }
1000 }
1001
1002 pub fn begin_action_handler(&mut self, action: &(dyn Action + 'static), cx: &mut App) {
1004 journal::begin_foreground_turn();
1005 let name = actions::update_running_action(action, cx);
1006 self.active_actions.push((name, Instant::now()));
1007 }
1008
1009 pub fn end_action_handler(&mut self) {
1011 actions::save_action_timing();
1015 let Some((name, start)) = self.active_actions.pop() else {
1016 debug_assert!(false, "action handler must be begun before it ends");
1017 journal::end_foreground_turn();
1018 return;
1019 };
1020 if !journal::power_interrupted_since(start) {
1021 journal::record_action(ActionTiming {
1022 name,
1023 start,
1024 end: Instant::now(),
1025 });
1026 }
1027 journal::end_foreground_turn();
1028 }
1029
1030 pub fn begin_draw(&mut self) {
1032 journal::begin_foreground_turn();
1033 let started_at = Instant::now();
1034 journal::record_frame_pending(self.window_id, started_at);
1035 self.active_activities
1036 .push(WindowActivity::Draw { started_at });
1037 }
1038
1039 pub fn end_draw(&mut self, dirty_at: Option<Instant>, invalidations: u64) -> Duration {
1041 let Some(WindowActivity::Draw {
1042 started_at: draw_start,
1043 }) = self.active_activities.pop()
1044 else {
1045 debug_assert!(false, "draw activity must be the current window activity");
1046 journal::end_foreground_turn();
1047 return Duration::ZERO;
1048 };
1049
1050 let draw_end = Instant::now();
1051 let frame_timing = FrameTiming {
1052 window_id: self.window_id,
1053 dirty_at: dirty_at.filter(|at| journal::frame_sample_is_valid(self.window_id, *at)),
1054 invalidations,
1055 draw_start,
1056 draw_end,
1057 };
1058 let draw_duration = frame_timing.draw_duration();
1059 if !journal::power_interrupted_since(draw_start) {
1060 self.record_draw_timing(frame_timing);
1061 }
1062 journal::end_foreground_turn();
1063 draw_duration
1064 }
1065
1066 pub fn record_present(
1071 &mut self,
1072 present_start: Instant,
1073 present_end: Instant,
1074 window_active: bool,
1075 next_frame_scheduled: bool,
1076 ) {
1077 self.record_present_at(
1078 present_start,
1079 present_end,
1080 window_active,
1081 next_frame_scheduled,
1082 );
1083 }
1084
1085 pub fn input_latency_snapshot(&self) -> InputLatencySnapshot {
1087 InputLatencySnapshot {
1088 latency_histogram: self.input_latency_histogram.clone(),
1089 events_per_frame_histogram: self.events_per_frame_histogram.clone(),
1090 mid_draw_events_dropped: self.mid_draw_events_dropped,
1091 }
1092 }
1093
1094 pub fn frame_duration_snapshot(&self) -> FrameDurationSnapshot {
1096 FrameDurationSnapshot {
1097 dirty_to_present_histogram: self.dirty_to_present_histogram.clone(),
1098 draw_duration_histogram: self.draw_duration_histogram.clone(),
1099 present_interval_histogram: self.present_interval_histogram.clone(),
1100 }
1101 }
1102
1103 fn record_present_at(
1104 &mut self,
1105 present_start: Instant,
1106 present_end: Instant,
1107 window_active: bool,
1108 next_frame_scheduled: bool,
1109 ) {
1110 if let Some(first_input_at) = self.first_input_at.take()
1111 && journal::frame_sample_is_valid(self.window_id, first_input_at)
1112 {
1113 let latency_nanos = present_end.duration_since(first_input_at).as_nanos() as u64;
1114 self.input_latency_histogram.record(latency_nanos).ok();
1115 if self.pending_input_count > 0 {
1116 self.events_per_frame_histogram
1117 .record(self.pending_input_count)
1118 .ok();
1119 }
1120 }
1121 self.pending_input_count = 0;
1122
1123 let frame = self
1124 .pending_frame
1125 .take()
1126 .filter(|frame| journal::frame_sample_is_valid(self.window_id, frame.draw_start))
1127 .map(|mut frame| {
1128 frame.dirty_at = frame
1129 .dirty_at
1130 .filter(|at| journal::frame_sample_is_valid(self.window_id, *at));
1131 frame
1132 });
1133 let animation_interval =
1134 if frame.is_some() && self.animating_at_last_present && window_active {
1135 self.last_present_at
1136 .filter(|at| journal::frame_sample_is_valid(self.window_id, *at))
1137 .map(|last_present_at| present_end.duration_since(last_present_at))
1138 } else {
1139 None
1140 };
1141 let present_timing = PresentTiming {
1142 window_id: self.window_id,
1143 present_start,
1144 present_end,
1145 animation_interval,
1146 };
1147 journal::record_present(present_timing, frame);
1148
1149 let Some(frame) = frame else {
1150 return;
1151 };
1152
1153 if let Some(dirty_at) = frame.dirty_at
1154 && let Err(error) = self
1155 .dirty_to_present_histogram
1156 .record(present_end.duration_since(dirty_at).as_nanos() as u64)
1157 {
1158 log::error!("failed to record dirty-to-present frame timing: {error}");
1159 }
1160
1161 if let Some(animation_interval) = animation_interval {
1162 self.present_interval_histogram
1163 .record(animation_interval.as_nanos() as u64)
1164 .ok();
1165 }
1166 record_frame_event(FrameEvent::Present(present_timing));
1167
1168 self.last_present_at = Some(present_end);
1169 self.animating_at_last_present = next_frame_scheduled && window_active;
1170 }
1171
1172 fn record_draw_timing(&mut self, timing: FrameTiming) {
1173 self.record_draw_duration(timing.draw_duration());
1174 self.pending_frame = Some(timing);
1175 record_frame_event(FrameEvent::Draw(timing));
1176 journal::record_draw(timing);
1177 }
1178
1179 fn record_draw_duration(&mut self, duration: Duration) {
1180 self.draw_duration_histogram
1181 .record(duration.as_nanos() as u64)
1182 .ok();
1183 }
1184}
1185
1186#[cfg(feature = "profiler")]
1187impl Drop for WindowProfiler {
1188 fn drop(&mut self) {
1189 journal::record_window_closed(self.window_id);
1190 }
1191}
1192
1193#[cfg(feature = "profiler")]
1195const MAX_FRAME_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<FrameEvent>();
1196
1197#[cfg(feature = "profiler")]
1198struct FrameTimings {
1199 timings: VecDeque<FrameEvent>,
1200 total_pushed: u64,
1201}
1202
1203#[cfg(feature = "profiler")]
1204static FRAME_TIMINGS: spin::Mutex<FrameTimings> = spin::Mutex::new(FrameTimings {
1205 timings: VecDeque::new(),
1206 total_pushed: 0,
1207});
1208
1209#[cfg(feature = "profiler")]
1213pub fn record_frame_event(event: FrameEvent) {
1214 if !trace_enabled() {
1215 return;
1216 }
1217 cold_path(); let mut frames = FRAME_TIMINGS.lock();
1220 if frames.timings.len() >= MAX_FRAME_TIMINGS {
1221 frames.timings.pop_front();
1222 }
1223 frames.timings.push_back(event);
1224 frames.total_pushed += 1;
1225}
1226
1227#[cfg(feature = "profiler")]
1230pub struct FrameTimingCollector {
1231 cursor: u64,
1232}
1233
1234#[cfg(feature = "profiler")]
1235impl Default for FrameTimingCollector {
1236 fn default() -> Self {
1237 Self::new()
1238 }
1239}
1240
1241#[cfg(feature = "profiler")]
1242impl FrameTimingCollector {
1243 pub fn new() -> Self {
1245 Self {
1246 cursor: FRAME_TIMINGS.lock().total_pushed,
1247 }
1248 }
1249
1250 pub fn collect_unseen(&mut self) -> Vec<FrameEvent> {
1254 let frames = FRAME_TIMINGS.lock();
1255 let buffer_len = frames.timings.len() as u64;
1256 let buffer_start = frames.total_pushed.saturating_sub(buffer_len);
1257 let skip = self.cursor.saturating_sub(buffer_start) as usize;
1258 let unseen = frames
1259 .timings
1260 .iter()
1261 .skip(skip.min(frames.timings.len()))
1262 .copied()
1263 .collect();
1264 self.cursor = frames.total_pushed;
1265 unseen
1266 }
1267}
1268
1269#[cfg(all(test, feature = "profiler"))]
1270mod tests {
1271 use super::*;
1272 use std::sync::{Mutex, MutexGuard};
1273
1274 #[test]
1275 fn interruptions_drop_latency_samples_but_hiding_keeps_draw_work() {
1276 for sleep in [false, true] {
1277 let (_journal, _guard) = journal::install_test_foreground_journal(64, 4);
1278 let id = WindowId::from(1);
1279 let mut profiler = WindowProfiler::new(id).expect("valid histograms");
1280 let old = Instant::now() - Duration::from_secs(1);
1281 record_test_draw(&mut profiler, old);
1282 profiler.record_present_at(old, old, true, true);
1283 profiler.first_input_at = Some(old);
1284 profiler.pending_input_count = 1;
1285 profiler.begin_draw();
1286 if sleep {
1287 journal::record_power_transition(journal::PowerState::Suspended);
1288 journal::record_power_transition(journal::PowerState::Awake);
1289 } else {
1290 journal::record_window_visibility(id, crate::WindowVisibility::Hidden);
1291 journal::record_window_visibility(id, crate::WindowVisibility::Visible);
1292 }
1293 profiler.end_draw(Some(old), 1);
1294 let now = Instant::now();
1295 profiler.record_present_at(now, now, true, true);
1296 assert_eq!(
1297 profiler.draw_duration_histogram.len(),
1298 if sleep { 1 } else { 2 }
1299 );
1300 assert_eq!(profiler.input_latency_histogram.len(), 0);
1301 assert_eq!(profiler.dirty_to_present_histogram.len(), 1);
1302 assert_eq!(profiler.present_interval_histogram.len(), 0);
1303 profiler.begin_draw();
1304 profiler.end_draw(Some(Instant::now()), 1);
1305 let now = Instant::now();
1306 profiler.record_present_at(now, now, true, true);
1307 assert_eq!(profiler.dirty_to_present_histogram.len(), 2);
1308 }
1309 }
1310
1311 #[test]
1312 fn records_draw_events_only_while_tracing() {
1313 let _trace_test_guard = TraceTestGuard::new();
1314 let window_id = WindowId::from(0xD0A0);
1315 let mut window_profiler =
1316 WindowProfiler::new(window_id).expect("window profiler should initialize");
1317 let dirty_at = Instant::now();
1318 let mut collector = FrameTimingCollector::new();
1319
1320 window_profiler.begin_draw();
1321 window_profiler.end_draw(Some(dirty_at), 3);
1322 assert!(
1323 collector
1324 .collect_unseen()
1325 .iter()
1326 .all(|event| !event_matches_window(*event, window_id))
1327 );
1328
1329 set_trace_enabled(true);
1330 let mut collector = FrameTimingCollector::new();
1331 window_profiler.begin_draw();
1332 window_profiler.end_draw(Some(dirty_at), 3);
1333
1334 let timing = collector
1335 .collect_unseen()
1336 .into_iter()
1337 .find_map(|event| match event {
1338 FrameEvent::Draw(timing) if timing.window_id == window_id => Some(timing),
1339 _ => None,
1340 })
1341 .expect("draw event should be recorded while tracing");
1342 assert_eq!(timing.dirty_at, Some(dirty_at));
1343 assert_eq!(timing.invalidations, 3);
1344 assert!(timing.draw_start >= dirty_at);
1345 }
1346
1347 #[test]
1348 fn records_present_events_for_newly_drawn_frames() {
1349 let _trace_test_guard = TraceTestGuard::new();
1350 set_trace_enabled(true);
1351 let window_id = WindowId::from(0xA11E);
1352 let mut window_profiler =
1353 WindowProfiler::new(window_id).expect("window profiler should initialize");
1354 let start = Instant::now();
1355 let mut collector = FrameTimingCollector::new();
1356
1357 record_test_draw(&mut window_profiler, start);
1358 window_profiler.record_present_at(start, start, true, true);
1359 record_test_draw(&mut window_profiler, start + FRAME);
1360 window_profiler.record_present_at(start + FRAME, start + FRAME, true, true);
1361 window_profiler.record_present_at(
1362 start + FRAME + FRAME / 2,
1363 start + FRAME + FRAME / 2,
1364 true,
1365 true,
1366 );
1367
1368 let present_timings = collector
1369 .collect_unseen()
1370 .into_iter()
1371 .filter_map(|event| match event {
1372 FrameEvent::Present(timing) if timing.window_id == window_id => Some(timing),
1373 _ => None,
1374 })
1375 .collect::<Vec<_>>();
1376 let [first_present, second_present] = present_timings.as_slice() else {
1377 panic!("expected exactly two present events, got {present_timings:?}");
1378 };
1379 assert_eq!(first_present.animation_interval, None);
1380 assert_eq!(second_present.animation_interval, Some(FRAME));
1381
1382 #[cfg(feature = "profiler")]
1383 {
1384 assert_eq!(window_profiler.present_interval_histogram.len(), 1);
1385 assert!(
1386 window_profiler.present_interval_histogram.max()
1387 >= second_present
1388 .animation_interval
1389 .expect("second present should have an animation interval")
1390 .as_nanos() as u64
1391 );
1392 }
1393 }
1394
1395 #[test]
1396 fn disabling_tracing_clears_frame_events() {
1397 let _trace_test_guard = TraceTestGuard::new();
1398 set_trace_enabled(true);
1399 let window_id = WindowId::from(0xC1EA);
1400 let mut window_profiler =
1401 WindowProfiler::new(window_id).expect("window profiler should initialize");
1402 let mut collector = FrameTimingCollector::new();
1403
1404 window_profiler.begin_draw();
1405 window_profiler.end_draw(None, 0);
1406 assert!(
1407 FRAME_TIMINGS
1408 .lock()
1409 .timings
1410 .iter()
1411 .copied()
1412 .any(|event| event_matches_window(event, window_id))
1413 );
1414
1415 set_trace_enabled(false);
1416 assert!(
1417 collector
1418 .collect_unseen()
1419 .iter()
1420 .all(|event| !event_matches_window(*event, window_id))
1421 );
1422 }
1423
1424 #[cfg(feature = "profiler")]
1425 #[test]
1426 fn records_intervals_only_between_animation_frames() {
1427 let mut window_profiler =
1428 WindowProfiler::new(WindowId::from(1)).expect("window profiler should initialize");
1429 let start = Instant::now();
1430
1431 draw_and_present(&mut window_profiler, start, true, true);
1432 assert_eq!(window_profiler.present_interval_histogram.len(), 0);
1433
1434 draw_and_present(&mut window_profiler, start + FRAME, true, true);
1435 assert_eq!(window_profiler.present_interval_histogram.len(), 1);
1436
1437 draw_and_present(&mut window_profiler, start + FRAME * 2, true, false);
1438 assert_eq!(window_profiler.present_interval_histogram.len(), 2);
1439
1440 draw_and_present(&mut window_profiler, start + FRAME * 100, true, true);
1441 assert_eq!(window_profiler.present_interval_histogram.len(), 2);
1442 }
1443
1444 #[cfg(feature = "profiler")]
1445 #[test]
1446 fn missed_frames_stretch_the_recorded_interval() {
1447 let mut window_profiler =
1448 WindowProfiler::new(WindowId::from(2)).expect("window profiler should initialize");
1449 let start = Instant::now();
1450
1451 draw_and_present(&mut window_profiler, start, true, true);
1452 draw_and_present(&mut window_profiler, start + FRAME * 5, true, true);
1453
1454 let recorded = window_profiler.present_interval_histogram.max();
1455 assert!(recorded >= (FRAME * 4).as_nanos() as u64);
1456 }
1457
1458 #[cfg(feature = "profiler")]
1459 #[test]
1460 fn ignores_re_presents_of_unchanged_frames() {
1461 let mut window_profiler =
1462 WindowProfiler::new(WindowId::from(3)).expect("window profiler should initialize");
1463 let start = Instant::now();
1464
1465 draw_and_present(&mut window_profiler, start, true, true);
1466 window_profiler.record_present_at(start + FRAME / 2, start + FRAME / 2, true, true);
1467 draw_and_present(&mut window_profiler, start + FRAME, true, true);
1468
1469 assert_eq!(window_profiler.present_interval_histogram.len(), 1);
1470 assert!(
1471 window_profiler.present_interval_histogram.max() >= (FRAME * 3 / 4).as_nanos() as u64
1472 );
1473 }
1474
1475 #[cfg(feature = "profiler")]
1476 #[test]
1477 fn skips_intervals_for_inactive_windows() {
1478 let mut window_profiler =
1479 WindowProfiler::new(WindowId::from(4)).expect("window profiler should initialize");
1480 let start = Instant::now();
1481
1482 draw_and_present(&mut window_profiler, start, false, true);
1483 draw_and_present(&mut window_profiler, start + FRAME, false, true);
1484 assert_eq!(window_profiler.present_interval_histogram.len(), 0);
1485
1486 draw_and_present(&mut window_profiler, start + FRAME * 2, true, true);
1487 assert_eq!(window_profiler.present_interval_histogram.len(), 0);
1488
1489 draw_and_present(&mut window_profiler, start + FRAME * 3, true, true);
1490 assert_eq!(window_profiler.present_interval_histogram.len(), 1);
1491 }
1492
1493 #[test]
1494 fn records_dirty_to_present_durations() {
1495 let mut window_profiler =
1496 WindowProfiler::new(WindowId::from(8)).expect("window profiler should initialize");
1497 let draw_end = Instant::now();
1498 let present_end = draw_end + Duration::from_millis(6);
1499
1500 record_test_draw(&mut window_profiler, draw_end);
1501 window_profiler.record_present_at(present_end, present_end, true, false);
1502
1503 let snapshot = window_profiler.frame_duration_snapshot();
1504 let histogram = snapshot.dirty_to_present_histogram;
1505 assert_eq!(histogram.len(), 1);
1506 assert!(histogram.max() >= Duration::from_millis(10).as_nanos() as u64);
1507 }
1508
1509 #[cfg(feature = "profiler")]
1510 #[test]
1511 fn records_every_draw_duration() {
1512 let mut window_profiler =
1513 WindowProfiler::new(WindowId::from(5)).expect("window profiler should initialize");
1514
1515 window_profiler.record_draw_duration(Duration::from_millis(2));
1516 window_profiler.record_draw_duration(Duration::from_millis(40));
1517
1518 let snapshot = window_profiler.frame_duration_snapshot();
1519 assert_eq!(snapshot.draw_duration_histogram.len(), 2);
1520 assert!(snapshot.draw_duration_histogram.max() >= 39_000_000);
1521 }
1522
1523 #[test]
1524 fn records_input_latency_at_the_frame_presentation_timestamp() {
1525 let mut window_profiler =
1526 WindowProfiler::new(WindowId::from(6)).expect("window profiler should initialize");
1527 let first_input_at = Instant::now();
1528 let presented_at = first_input_at + Duration::from_millis(12);
1529
1530 begin_input_at(&mut window_profiler, first_input_at);
1531 window_profiler.end_input(true);
1532 begin_input_at(
1533 &mut window_profiler,
1534 first_input_at + Duration::from_millis(2),
1535 );
1536 window_profiler.end_input(true);
1537 record_test_draw(&mut window_profiler, presented_at);
1538 window_profiler.record_present_at(presented_at, presented_at, true, false);
1539
1540 let snapshot = window_profiler.input_latency_snapshot();
1541 assert_eq!(snapshot.latency_histogram.len(), 1);
1542 assert!(snapshot.latency_histogram.max() >= Duration::from_millis(12).as_nanos() as u64);
1543 assert_eq!(snapshot.events_per_frame_histogram.len(), 1);
1544 assert_eq!(snapshot.events_per_frame_histogram.max(), 2);
1545 assert_eq!(snapshot.mid_draw_events_dropped, 0);
1546 }
1547
1548 #[test]
1549 fn excludes_input_that_arrives_during_a_draw() {
1550 let mut window_profiler =
1551 WindowProfiler::new(WindowId::from(7)).expect("window profiler should initialize");
1552
1553 window_profiler.begin_draw();
1554 begin_input_at(&mut window_profiler, Instant::now());
1555 window_profiler.end_input(true);
1556 window_profiler.end_draw(None, 0);
1557
1558 let snapshot = window_profiler.input_latency_snapshot();
1559 assert!(snapshot.latency_histogram.is_empty());
1560 assert!(snapshot.events_per_frame_histogram.is_empty());
1561 assert_eq!(snapshot.mid_draw_events_dropped, 1);
1562 }
1563
1564 #[test]
1565 fn overlapping_trace_scopes_keep_tracing_enabled() {
1566 let _trace_test_guard = TraceTestGuard::new();
1567 let first_scope = trace_scope();
1568 let second_scope = trace_scope();
1569
1570 assert!(trace_enabled());
1571 drop(first_scope);
1572 assert!(trace_enabled());
1573 drop(second_scope);
1574 assert!(!trace_enabled());
1575 }
1576
1577 const FRAME: Duration = Duration::from_millis(16);
1578 static TRACE_TEST_LOCK: Mutex<()> = Mutex::new(());
1579
1580 struct TraceTestGuard {
1581 was_enabled: bool,
1582 _lock: MutexGuard<'static, ()>,
1583 }
1584
1585 impl TraceTestGuard {
1586 fn new() -> Self {
1587 let lock = TRACE_TEST_LOCK
1588 .lock()
1589 .unwrap_or_else(|poisoned| poisoned.into_inner());
1590 let was_enabled = trace_enabled();
1591 set_trace_enabled(false);
1592 Self {
1593 was_enabled,
1594 _lock: lock,
1595 }
1596 }
1597 }
1598
1599 impl Drop for TraceTestGuard {
1600 fn drop(&mut self) {
1601 set_trace_enabled(false);
1602 if self.was_enabled {
1603 set_trace_enabled(true);
1604 }
1605 }
1606 }
1607
1608 fn event_matches_window(event: FrameEvent, window_id: WindowId) -> bool {
1609 match event {
1610 FrameEvent::Draw(timing) => timing.window_id == window_id,
1611 FrameEvent::Present(timing) => timing.window_id == window_id,
1612 }
1613 }
1614
1615 fn begin_input_at(window_profiler: &mut WindowProfiler, started_at: Instant) {
1616 window_profiler
1617 .active_activities
1618 .push(WindowActivity::Input {
1619 started_at,
1620 kind: "test",
1621 });
1622 }
1623
1624 #[cfg(feature = "profiler")]
1625 fn draw_and_present(
1626 window_profiler: &mut WindowProfiler,
1627 presented_at: Instant,
1628 window_active: bool,
1629 next_frame_scheduled: bool,
1630 ) {
1631 record_test_draw(window_profiler, presented_at);
1632 window_profiler.record_present_at(
1633 presented_at,
1634 presented_at,
1635 window_active,
1636 next_frame_scheduled,
1637 );
1638 }
1639
1640 fn record_test_draw(window_profiler: &mut WindowProfiler, draw_end: Instant) {
1641 window_profiler.record_draw_timing(FrameTiming {
1642 window_id: window_profiler.window_id,
1643 dirty_at: Some(draw_end - Duration::from_millis(4)),
1644 invalidations: 1,
1645 draw_start: draw_end - Duration::from_millis(2),
1646 draw_end,
1647 });
1648 }
1649}