Skip to main content

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