Skip to main content

gpui_x/
profiler.rs

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/// No-op stand-in for `std::hint::cold_path`, which was reverted from stable
20/// to unstable in current Rust releases (tracking issue rust-lang/rust#136873).
21#[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    /// A task timing with a duration of zero. Any task will replace this in history.
111    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    /// Convert upgraded per-thread timings into their structured format.
144    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); // +1 for running task
157                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/// Serializable variant of [`core::panic::Location`]
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SerializedLocation {
234    /// Name of the source file
235    pub file: SharedString,
236    /// Line in the source file
237    pub line: u32,
238    /// Column in the source file
239    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/// Serializable variant of [`TaskTiming`]
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct SerializedTaskTiming {
255    /// Location of the timing
256    pub location: SerializedLocation,
257    /// Time at which the measurement was reported in nanoseconds
258    pub start: u128,
259    /// Duration of the measurement in nanoseconds
260    pub duration: u128,
261}
262
263impl SerializedTaskTiming {
264    /// Convert an array of [`TaskTiming`] into their serializable format
265    ///
266    /// # Params
267    ///
268    /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
269    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    /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
287    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/// Serializable variant of [`ThreadTaskTimings`]
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct SerializedThreadTaskTimings {
301    /// Thread name
302    pub thread_name: Option<String>,
303    /// Hash of the thread id
304    pub thread_id: u64,
305    /// Timing records for this thread
306    pub timings: Vec<SerializedTaskTiming>,
307}
308
309impl SerializedThreadTaskTimings {
310    /// Convert [`ThreadTaskTimings`] into their serializable format
311    ///
312    /// # Params
313    ///
314    /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
315    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    /// Hashed thread id
334    pub thread_id: u64,
335    /// Thread name, if known
336    pub thread_name: Option<String>,
337    /// New timings since the last call. If the circular buffer wrapped around
338    /// since the previous poll, some entries may have been lost.
339    pub new_timings: Vec<SerializedTaskTiming>,
340}
341
342/// Tracks which timing events have already been seen so that callers can request only unseen events.
343#[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                // Cursor fell behind the buffer — some entries were evicted.
378                // Return everything still in the buffer.
379                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// Allow 16MiB of task timing entries.
410// VecDeque grows by doubling its capacity when full, so keep this a power of 2 to avoid wasting
411// memory.
412#[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            // Do not track polls that are not problematic
464            // this keeps more calls on the fast path
465            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(); // most tasks are not the worst, optimize for that
479            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(); // most tasks are not the worst, optimize for that
500            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
521/// Upgrades all live per-thread timing handles, holding the global registry
522/// lock only for the duration of the upgrades.
523///
524/// The upgraded `Arc`s must never be dropped while `GLOBAL_THREAD_TIMINGS` is
525/// locked: dropping the last strong reference runs [`ThreadTimings::drop`],
526/// which locks `GLOBAL_THREAD_TIMINGS` again and would deadlock the
527/// non-reentrant spinlock. A thread exiting concurrently can hand off its last
528/// reference to us at any time, so callers of this function process (lock,
529/// read, drop) the returned handles only after the global lock is released.
530fn 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(); // optimize for when the profiling is off
620            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    // Running tasks are included in the reliability trace, which is written
632    // whenever the foreground executor makes no progress for > n seconds
633    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
705/// Enables or disables profiler trace collection at runtime.
706///
707/// When transitioning from enabled to disabled, `add_task_timing` becomes
708/// cheaper since only cheap statistics are gathered. The existing per-thread
709/// task buffers and the frame-event buffer are cleared so stale data isn't
710/// reported after a later re-enable. Active trace scopes keep collection enabled
711/// until the last scope ends. Calls with the current setting are a no-op.
712pub 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
769/// Returns whether profiler trace collection is enabled.
770pub 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/// Timing for a single drawn window frame.
791#[cfg(feature = "profiler")]
792#[derive(Debug, Copy, Clone)]
793pub struct FrameTiming {
794    /// The window that was drawn.
795    pub window_id: WindowId,
796    /// When the frame first became dirty (its first invalidation). `None` if
797    /// profiler tracing was not yet enabled when the invalidation occurred.
798    pub dirty_at: Option<Instant>,
799    /// Number of invalidations coalesced into this frame.
800    pub invalidations: u64,
801    /// When `Window::draw` started.
802    pub draw_start: Instant,
803    /// When `Window::draw` finished.
804    pub draw_end: Instant,
805}
806
807#[cfg(feature = "profiler")]
808impl FrameTiming {
809    /// Time spent inside `Window::draw`.
810    pub fn draw_duration(&self) -> Duration {
811        self.draw_end.duration_since(self.draw_start)
812    }
813
814    /// Time from the frame's first invalidation to the end of its draw, if the
815    /// first invalidation was observed.
816    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/// Work spent submitting a window frame to the platform.
823#[cfg(feature = "profiler")]
824#[derive(Debug, Copy, Clone)]
825pub struct PresentTiming {
826    /// The window whose frame was submitted.
827    pub window_id: WindowId,
828    /// When the platform submission began.
829    pub present_start: Instant,
830    /// When the platform submission completed.
831    pub present_end: Instant,
832    /// The interval since the previous newly drawn frame was submitted, when
833    /// both frames belong to an active animation.
834    pub animation_interval: Option<Duration>,
835}
836
837#[cfg(feature = "profiler")]
838impl PresentTiming {
839    /// Time spent submitting the frame to the platform.
840    pub fn present_duration(&self) -> Duration {
841        self.present_end.duration_since(self.present_start)
842    }
843}
844
845/// A frame event observed by the profiler.
846#[cfg(feature = "profiler")]
847#[derive(Debug, Copy, Clone)]
848pub enum FrameEvent {
849    /// A window frame was drawn.
850    Draw(FrameTiming),
851    /// A newly drawn window frame was presented.
852    Present(PresentTiming),
853}
854
855/// A point-in-time snapshot of the frame-duration histograms for a window,
856/// suitable for external formatting.
857#[cfg(feature = "profiler")]
858#[derive(Clone)]
859pub struct FrameDurationSnapshot {
860    /// Histogram of durations from the first invalidation through presentation, in nanoseconds.
861    pub dirty_to_present_histogram: Histogram<u64>,
862    /// Histogram of `Window::draw` durations, in nanoseconds.
863    pub draw_duration_histogram: Histogram<u64>,
864    /// Histogram of intervals between consecutively presented frames while the
865    /// window was animating, in nanoseconds.
866    pub present_interval_histogram: Histogram<u64>,
867}
868
869/// A point-in-time snapshot of the input-latency histograms for a window,
870/// suitable for external formatting.
871#[cfg(feature = "profiler")]
872#[derive(Clone)]
873pub struct InputLatencySnapshot {
874    /// Histogram of input-to-frame latency samples, in nanoseconds.
875    pub latency_histogram: Histogram<u64>,
876    /// Histogram of input events coalesced per rendered frame.
877    pub events_per_frame_histogram: Histogram<u64>,
878    /// Count of input events that arrived mid-draw and were excluded from
879    /// latency recording.
880    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/// Collects profiling information for one window.
895///
896/// Aggregate histograms are always populated when the `profiler` feature is
897/// compiled in. Individual draw and present events are added to the global
898/// profiler buffer only while tracing is enabled.
899#[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    /// Creates a profiler for a window.
920    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    /// Records the beginning of an input dispatch. `kind` names the platform
952    /// input variant being dispatched (see [`crate::PlatformInput::kind_name`]).
953    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    /// Records the end of an input dispatch.
962    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    /// Records the beginning of an action handler.
1003    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    /// Records the end of the current action handler.
1010    pub fn end_action_handler(&mut self) {
1011        // Dual-write to the legacy aggregate store; its single global running
1012        // slot misbehaves when tests run actions concurrently, which is why
1013        // the journal entry is tracked here on the window instead.
1014        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    /// Records the beginning of a window draw.
1031    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    /// Records the end of a window draw and returns the draw duration.
1040    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    /// Records that a frame was presented.
1067    ///
1068    /// `next_frame_scheduled` marks the animation state for the interval ending
1069    /// at the next newly drawn frame's presentation.
1070    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    /// Returns a snapshot of the current input-latency histograms.
1086    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    /// Returns a snapshot of the current frame-duration histograms.
1095    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// Allow 16MiB of frame event entries.
1194#[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/// Records a frame event.
1210///
1211/// No-op unless profiler tracing is enabled via [`set_trace_enabled`].
1212#[cfg(feature = "profiler")]
1213pub fn record_frame_event(event: FrameEvent) {
1214    if !trace_enabled() {
1215        return;
1216    }
1217    cold_path(); // optimize for when profiling is off
1218
1219    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/// Drains frame events recorded after this collector was created, tracking a
1228/// cursor so each call to [`Self::collect_unseen`] returns only new entries.
1229#[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    /// Creates a collector that only sees frame events recorded from this point on.
1244    pub fn new() -> Self {
1245        Self {
1246            cursor: FRAME_TIMINGS.lock().total_pushed,
1247        }
1248    }
1249
1250    /// Returns frame events recorded since the previous call (or since the
1251    /// collector was created). If the ring buffer wrapped around since the
1252    /// previous poll, the evicted entries are lost.
1253    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}