Skip to main content

gpui/
profiler.rs

1//! Timing for tasks, actions and window frames.
2//!
3//! Recording is compiled in with the `profiler` feature. Without it, the functions
4//! that return timings return empty results, and the recording functions do nothing.
5//! Even with the feature, task and frame events in the trace buffers are kept only
6//! while tracing is on; [`set_trace_enabled`] controls that state. The foreground journal,
7//! which `App::foreground_journal` reads, records them regardless. With the feature, each
8//! window also keeps its histograms, whether or not tracing is on.
9//!
10//! To see frame times on screen, set the debug frame overlay mode with
11//! `Window::set_debug_frame_overlay_mode`. To read the numbers in code, call
12//! `Window::frame_duration_snapshot` and `Window::input_latency_snapshot`. Those
13//! three window methods exist only with the `profiler` feature. The
14//! [performance guide](crate::_documentation::guides::performance) walks through them.
15
16#[cfg(feature = "profiler")]
17use hdrhistogram::Histogram;
18use itertools::Itertools;
19use scheduler::{Instant, SpawnTime};
20#[cfg(feature = "profiler")]
21use smallvec::SmallVec;
22use std::{
23    cell::LazyCell,
24    collections::{HashMap, VecDeque},
25    hash::{DefaultHasher, Hash, Hasher},
26    sync::{
27        Arc,
28        atomic::{AtomicU64, Ordering},
29    },
30    thread::ThreadId,
31    time::Duration,
32};
33
34mod actions;
35#[cfg(feature = "profiler")]
36pub mod hang;
37#[cfg(feature = "profiler")]
38pub mod journal;
39pub use actions::{ActionStatistics, ActionTiming, take_action_stats};
40
41/// Hints that the calling branch is unlikely to be taken.
42///
43/// Stands in for `std::hint::cold_path`, which is still unstable on our MSRV
44/// (rust-lang/rust#136873); calling a `#[cold]` function is the equivalent hint.
45#[cold]
46#[inline(always)]
47pub(super) fn cold_path() {}
48
49use serde::{Deserialize, Serialize};
50
51#[cfg(feature = "profiler")]
52use crate::{Action, App, WindowId};
53use crate::{SharedString, TasksIncluded};
54
55/// Returns the task timings of every live thread.
56#[cfg(feature = "profiler")]
57#[doc(hidden)]
58pub fn get_all_timings(included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
59    ThreadTaskTimings::collect(upgraded_thread_timings(), included)
60}
61
62/// Returns the task timings of the calling thread.
63#[cfg(feature = "profiler")]
64#[doc(hidden)]
65pub fn get_current_thread_timings(included: TasksIncluded) -> gpui::ThreadTaskTimings {
66    gpui::profiler::get_current_thread_task_timings(included)
67}
68
69/// Returns and resets the task statistics of every live thread.
70#[cfg(feature = "profiler")]
71#[doc(hidden)]
72pub fn take_all_stats(included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
73    ThreadTaskStatistics::collect_and_reset(upgraded_thread_timings(), included)
74}
75
76/// Returns no timings, because the `profiler` feature is off.
77#[cfg(not(feature = "profiler"))]
78#[doc(hidden)]
79pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
80    Vec::new()
81}
82/// Returns an empty timing for the calling thread, because the `profiler` feature is off.
83#[cfg(not(feature = "profiler"))]
84#[doc(hidden)]
85pub fn get_current_thread_timings(_included: TasksIncluded) -> gpui::ThreadTaskTimings {
86    gpui::ThreadTaskTimings {
87        thread_name: None,
88        thread_id: std::thread::current().id(),
89        timings: Vec::new(),
90        stats: TaskStatistics::default(),
91        total_pushed: 0,
92    }
93}
94/// Returns no statistics, because the `profiler` feature is off.
95#[cfg(not(feature = "profiler"))]
96#[doc(hidden)]
97pub fn take_all_stats(_included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
98    Vec::new()
99}
100
101/// The instant a task poll ended, either by yielding or by finishing.
102#[doc(hidden)]
103#[derive(Debug, Copy, Clone)]
104pub struct YieldTime(pub Instant);
105
106/// One completed poll of a task.
107#[doc(hidden)]
108#[derive(Copy, Clone)]
109pub struct TaskTiming {
110    /// Where the task was spawned.
111    pub location: &'static core::panic::Location<'static>,
112    /// When the task was spawned.
113    pub spawned: SpawnTime,
114    /// When this poll started.
115    pub start: Instant,
116    /// When this poll yielded or finished.
117    pub end: YieldTime,
118}
119
120impl std::fmt::Debug for TaskTiming {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.debug_struct("TaskTiming")
123            .field("location", &self.location)
124            .field("since_spawned", &self.spawned.0.elapsed())
125            .field("last_poll_duration", &self.poll_duration())
126            .field("total_runtime", &self.since_spawn())
127            .finish()
128    }
129}
130
131/// A task poll that is still running on its thread.
132#[doc(hidden)]
133#[derive(Debug, Copy, Clone)]
134pub struct ActiveTiming {
135    /// Where the task was spawned.
136    pub location: &'static core::panic::Location<'static>,
137    /// When the task was spawned.
138    pub spawned: SpawnTime,
139    /// When this poll started.
140    pub start: Instant,
141}
142
143impl TaskTiming {
144    /// Returns a timing with zero duration, used to fill the statistics before any task has run.
145    pub fn placeholder() -> Self {
146        let now = Instant::now();
147        Self {
148            location: std::panic::Location::caller(),
149            spawned: SpawnTime(now),
150            start: now,
151            end: YieldTime(now),
152        }
153    }
154
155    /// Returns how long this poll ran, from start until it yielded or finished.
156    #[inline(always)]
157    pub fn poll_duration(&self) -> Duration {
158        self.end.0 - self.start
159    }
160
161    #[inline(always)]
162    fn since_spawn(&self) -> Duration {
163        self.end.0 - self.spawned.0
164    }
165}
166
167/// The recorded task polls of one thread.
168#[doc(hidden)]
169#[derive(Debug, Clone)]
170pub struct ThreadTaskTimings {
171    /// The thread's name, if it has one.
172    pub thread_name: Option<String>,
173    /// The thread's ID.
174    pub thread_id: ThreadId,
175    /// Completed polls, oldest first, and the running poll if it was requested.
176    pub timings: Vec<TaskTiming>,
177    /// The longest polls and runtimes seen on this thread.
178    pub stats: TaskStatistics,
179    /// The number of polls recorded since the thread started. This can be larger than
180    /// `timings.len()`, because the buffer keeps only the most recent polls.
181    pub total_pushed: u64,
182}
183
184impl ThreadTaskTimings {
185    /// Copies the given threads' recorded polls into [`ThreadTaskTimings`] values.
186    ///
187    /// The running poll is included only when `included` is [`TasksIncluded::CompletedAndRunning`].
188    pub fn collect(
189        timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
190        included: TasksIncluded,
191    ) -> Vec<Self> {
192        timings
193            .into_iter()
194            .map(|(thread_id, timings)| {
195                let timings = timings.lock();
196                let thread_name = timings.thread_name.clone();
197                let total_pushed = timings.total_pushed;
198                let completed = &timings.timings;
199
200                let mut vec = Vec::with_capacity(completed.len() + 1); // +1 for running task
201                let (s1, s2) = completed.as_slices();
202                vec.extend_from_slice(s1);
203                vec.extend_from_slice(s2);
204                if let TasksIncluded::CompletedAndRunning = included
205                    && let Some(running) = timings.running
206                {
207                    vec.push(TaskTiming {
208                        location: running.location,
209                        spawned: running.spawned,
210                        start: running.start,
211                        end: YieldTime(Instant::now()),
212                    })
213                }
214
215                ThreadTaskTimings {
216                    thread_name,
217                    thread_id,
218                    timings: vec,
219                    stats: timings.stats.clone(),
220                    total_pushed,
221                }
222            })
223            .collect()
224    }
225}
226
227/// The task statistics of one thread, taken from the thread's recorder.
228#[doc(hidden)]
229#[derive(Debug)]
230pub struct ThreadTaskStatistics {
231    /// The thread's name, if it has one.
232    pub thread_name: Option<String>,
233    /// The thread's ID.
234    pub thread_id: ThreadId,
235    /// The statistics that were taken.
236    pub stats: TaskStatistics,
237}
238
239impl ThreadTaskStatistics {
240    /// Takes each thread's statistics and resets them to [`TaskStatistics::default`].
241    ///
242    /// The running poll is folded into the statistics when `include_running` is
243    /// [`TasksIncluded::CompletedAndRunning`].
244    pub fn collect_and_reset(
245        timings: Vec<(ThreadId, Arc<GuardedTaskTimings>)>,
246        include_running: TasksIncluded,
247    ) -> Vec<Self> {
248        timings
249            .into_iter()
250            .map(|(thread_id, timings)| {
251                let mut timings = timings.lock();
252                let thread_name = timings.thread_name.clone();
253
254                let mut stats = std::mem::take(&mut timings.stats);
255                if let TasksIncluded::CompletedAndRunning = include_running
256                    && let Some(ActiveTiming {
257                        location,
258                        spawned,
259                        start,
260                    }) = timings.running
261                {
262                    let end = YieldTime(Instant::now());
263                    let timing = TaskTiming {
264                        location,
265                        spawned,
266                        start,
267                        end,
268                    };
269                    stats.add_runtime(timing);
270                    stats.add_yield_timing(timing);
271                }
272
273                Self {
274                    thread_name,
275                    thread_id,
276                    stats,
277                }
278            })
279            .collect()
280    }
281}
282
283/// Serializable variant of [`core::panic::Location`]
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct SerializedLocation {
286    /// Name of the source file
287    pub file: SharedString,
288    /// Line in the source file
289    pub line: u32,
290    /// Column in the source file
291    pub column: u32,
292}
293
294impl From<&core::panic::Location<'static>> for SerializedLocation {
295    fn from(value: &core::panic::Location<'static>) -> Self {
296        SerializedLocation {
297            file: value.file().into(),
298            line: value.line(),
299            column: value.column(),
300        }
301    }
302}
303
304/// Serializable variant of [`TaskTiming`]
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct SerializedTaskTiming {
307    /// Where the task was spawned
308    pub location: SerializedLocation,
309    /// Start of the poll, in nanoseconds after the anchor passed to [`Self::convert`]
310    pub start: u128,
311    /// How long the poll ran, in nanoseconds
312    pub duration: u128,
313}
314
315impl SerializedTaskTiming {
316    /// Converts a slice of [`TaskTiming`] values into their serializable format.
317    ///
318    /// # Params
319    ///
320    /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
321    pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec<SerializedTaskTiming> {
322        let serialized = timings
323            .iter()
324            .map(|timing| {
325                let start = timing.start.duration_since(anchor).as_nanos();
326                let duration = timing.end.0.duration_since(timing.start).as_nanos();
327                SerializedTaskTiming {
328                    location: timing.location.into(),
329                    start,
330                    duration,
331                }
332            })
333            .collect::<Vec<_>>();
334
335        serialized
336    }
337
338    /// Converts one [`TaskTiming`] into its serializable format.
339    ///
340    /// `anchor` is the [`Instant`] that `start` is measured from. It should be no later than
341    /// the timing's start.
342    pub fn from(anchor: Instant, timing: TaskTiming) -> SerializedTaskTiming {
343        let start = timing.start.duration_since(anchor).as_nanos();
344        let duration = timing.end.0.duration_since(timing.start).as_nanos();
345        SerializedTaskTiming {
346            location: timing.location.into(),
347            start,
348            duration,
349        }
350    }
351}
352
353/// Serializable variant of [`ThreadTaskTimings`]
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct SerializedThreadTaskTimings {
356    /// Thread name
357    pub thread_name: Option<String>,
358    /// Hash of the thread id
359    pub thread_id: u64,
360    /// Timing records for this thread
361    pub timings: Vec<SerializedTaskTiming>,
362}
363
364impl SerializedThreadTaskTimings {
365    /// Convert [`ThreadTaskTimings`] into their serializable format
366    ///
367    /// # Params
368    ///
369    /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
370    pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings {
371        let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings);
372
373        let mut hasher = DefaultHasher::new();
374        timings.thread_id.hash(&mut hasher);
375        let thread_id = hasher.finish();
376
377        SerializedThreadTaskTimings {
378            thread_name: timings.thread_name,
379            thread_id,
380            timings: serialized_timings,
381        }
382    }
383}
384
385/// The polls one thread recorded since the previous [`ProfilingCollector::collect_unseen`] call.
386#[doc(hidden)]
387#[derive(Debug, Clone)]
388pub struct ThreadTimingsDelta {
389    /// Hashed thread id
390    pub thread_id: u64,
391    /// Thread name, if known
392    pub thread_name: Option<String>,
393    /// New timings since the last call. If the circular buffer wrapped around
394    /// since the previous poll, some entries may have been lost.
395    pub new_timings: Vec<SerializedTaskTiming>,
396}
397
398/// Tracks which timing events have already been seen so that callers can request only unseen events.
399#[doc(hidden)]
400pub struct ProfilingCollector {
401    startup_time: Instant,
402    cursors: HashMap<ThreadId, u64>,
403}
404
405impl ProfilingCollector {
406    /// Creates a collector that reports timings relative to `startup_time`.
407    pub fn new(startup_time: Instant) -> Self {
408        Self {
409            startup_time,
410            cursors: HashMap::default(),
411        }
412    }
413
414    /// Returns the instant that serialized timings are measured from.
415    pub fn startup_time(&self) -> Instant {
416        self.startup_time
417    }
418
419    /// Returns the polls recorded since the previous call, one entry per thread that has new polls.
420    ///
421    /// Polls that were evicted from a thread's buffer before this call are skipped.
422    pub fn collect_unseen(
423        &mut self,
424        all_timings: Vec<ThreadTaskTimings>,
425    ) -> Vec<ThreadTimingsDelta> {
426        let mut deltas = Vec::with_capacity(all_timings.len());
427
428        for thread in all_timings {
429            let mut hasher = DefaultHasher::new();
430            thread.thread_id.hash(&mut hasher);
431            let hashed_id = hasher.finish();
432
433            let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0);
434            let buffer_len = thread.timings.len() as u64;
435            let buffer_start = thread.total_pushed.saturating_sub(buffer_len);
436
437            let mut slice = if prev_cursor < buffer_start {
438                // Cursor fell behind the buffer — some entries were evicted.
439                // Return everything still in the buffer.
440                thread.timings.as_slice()
441            } else {
442                let skip = (prev_cursor - buffer_start) as usize;
443                &thread.timings[skip.min(thread.timings.len())..]
444            };
445
446            let cursor_advance = thread.total_pushed;
447            self.cursors.insert(thread.thread_id, cursor_advance);
448
449            if slice.is_empty() {
450                continue;
451            }
452
453            let new_timings = SerializedTaskTiming::convert(self.startup_time, slice);
454
455            deltas.push(ThreadTimingsDelta {
456                thread_id: hashed_id,
457                thread_name: thread.thread_name,
458                new_timings,
459            });
460        }
461
462        deltas
463    }
464
465    /// Forgets every cursor, so the next [`Self::collect_unseen`] returns all buffered polls.
466    pub fn reset(&mut self) {
467        self.cursors.clear();
468    }
469}
470
471// Allow 16MiB of task timing entries.
472// VecDeque grows by doubling its capacity when full, so keep this a power of 2 to avoid wasting
473// memory.
474#[cfg(feature = "profiler")]
475const MAX_TASK_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<TaskTiming>();
476
477#[doc(hidden)]
478pub(crate) type TaskTimings = VecDeque<TaskTiming>;
479
480/// A thread's timings behind a spinlock, shared between the thread and the profiler.
481#[doc(hidden)]
482pub type GuardedTaskTimings = spin::Mutex<ThreadTimings>;
483
484/// A weak handle to one thread's timings, kept in [`GLOBAL_THREAD_TIMINGS`].
485#[doc(hidden)]
486pub struct GlobalThreadTimings {
487    /// The thread's ID.
488    pub thread_id: ThreadId,
489    /// A weak handle, so the registry doesn't keep a finished thread's timings alive.
490    pub timings: std::sync::Weak<GuardedTaskTimings>,
491}
492
493/// The five longest polls and the five longest runtimes seen on one thread.
494#[doc(hidden)]
495#[derive(Debug, Clone)]
496pub struct TaskStatistics {
497    /// The poll duration a poll must reach to be recorded in `longest_poll_times`.
498    pub poll_time_to_beat: Duration,
499    /// The runtime a task must reach to be recorded in `longest_runtimes`.
500    pub runtime_to_beat: Duration,
501    /// The five polls that ran the longest before yielding.
502    pub longest_poll_times: [TaskTiming; 5],
503    /// The five tasks that ran the longest from spawn to their last poll.
504    pub longest_runtimes: [TaskTiming; 5],
505}
506
507impl std::fmt::Display for TaskStatistics {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        f.write_str("Tasks that blocked the longest before yielding\n")?;
510        for timing in self.longest_poll_times {
511            f.write_fmt(format_args!(
512                "{:<20} - {}:{}\n",
513                format!("{:?}", timing.poll_duration()),
514                timing.location.file(),
515                timing.location.column()
516            ))?;
517        }
518        f.write_str("Tasks that ran the longest\n")?;
519        for timing in self.longest_runtimes {
520            f.write_fmt(format_args!(
521                "{:<20} - {}:{}\n",
522                format!("{:?}", timing.since_spawn()),
523                timing.location.file(),
524                timing.location.column()
525            ))?;
526        }
527        Ok(())
528    }
529}
530
531impl Default for TaskStatistics {
532    fn default() -> Self {
533        Self {
534            // Do not track polls that are not problematic
535            // this keeps more calls on the fast path
536            poll_time_to_beat: Duration::from_micros(100),
537            runtime_to_beat: Duration::from_micros(100),
538            longest_poll_times: [TaskTiming::placeholder(); 5],
539            longest_runtimes: [TaskTiming::placeholder(); 5],
540        }
541    }
542}
543
544impl TaskStatistics {
545    #[inline(always)]
546    fn add_yield_timing(&mut self, task: TaskTiming) {
547        let yielded_after = task.poll_duration();
548        if yielded_after >= self.poll_time_to_beat {
549            cold_path(); // most tasks are not the worst, optimize for that
550            let to_replace = self
551                .longest_poll_times
552                .iter()
553                .position_min_by_key(|task| task.poll_duration())
554                .expect("guarded by the comparison with nth_longest_yield_time");
555            self.longest_poll_times[to_replace] = task;
556
557            self.poll_time_to_beat = self
558                .longest_poll_times
559                .iter()
560                .map(|task| task.poll_duration())
561                .min()
562                .expect("never empty")
563                .max(Duration::from_micros(100));
564        }
565    }
566
567    #[inline(always)]
568    fn add_runtime(&mut self, task: TaskTiming) {
569        let runtime = task.since_spawn();
570        if runtime >= self.runtime_to_beat {
571            cold_path(); // most tasks are not the worst, optimize for that
572            let to_replace = self
573                .longest_runtimes
574                .iter()
575                .position_min_by_key(|task| task.since_spawn())
576                .expect("guarded by the comparison with nth_longest_yield_time");
577            self.longest_runtimes[to_replace] = task;
578
579            self.runtime_to_beat = self
580                .longest_runtimes
581                .iter()
582                .map(|task| task.since_spawn())
583                .min()
584                .expect("never empty")
585                .max(Duration::from_micros(100));
586        }
587    }
588}
589
590/// Every thread that has recorded timings, so the profiler can read them from any thread.
591#[doc(hidden)]
592pub static GLOBAL_THREAD_TIMINGS: spin::Mutex<Vec<GlobalThreadTimings>> =
593    spin::Mutex::new(Vec::new());
594
595/// Upgrades all live per-thread timing handles, holding the global registry
596/// lock only for the duration of the upgrades.
597///
598/// The upgraded `Arc`s must never be dropped while `GLOBAL_THREAD_TIMINGS` is
599/// locked: dropping the last strong reference runs [`ThreadTimings::drop`],
600/// which locks `GLOBAL_THREAD_TIMINGS` again and would deadlock the
601/// non-reentrant spinlock. A thread exiting concurrently can hand off its last
602/// reference to us at any time, so callers of this function process (lock,
603/// read, drop) the returned handles only after the global lock is released.
604fn upgraded_thread_timings() -> Vec<(ThreadId, Arc<GuardedTaskTimings>)> {
605    let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock();
606    global_thread_timings
607        .iter()
608        .filter_map(|t| Some((t.thread_id, t.timings.upgrade()?)))
609        .collect()
610}
611
612thread_local! {
613    /// This thread's timings. Created on first use and registered in [`GLOBAL_THREAD_TIMINGS`].
614    #[doc(hidden)]
615    pub static THREAD_TIMINGS: LazyCell<Arc<GuardedTaskTimings>> = LazyCell::new(|| {
616        let current_thread = std::thread::current();
617        let thread_name = current_thread.name();
618        let thread_id = current_thread.id();
619        let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id);
620        let timings = Arc::new(spin::Mutex::new(timings));
621
622        {
623            let timings = Arc::downgrade(&timings);
624            let global_timings = GlobalThreadTimings {
625                thread_id: std::thread::current().id(),
626                timings,
627            };
628            GLOBAL_THREAD_TIMINGS.lock().push(global_timings);
629        }
630
631        timings
632    });
633}
634
635/// The timing state of one thread: its completed polls, the running poll and its statistics.
636#[doc(hidden)]
637pub struct ThreadTimings {
638    /// The thread's name, if it has one.
639    pub thread_name: Option<String>,
640    /// The thread's ID.
641    pub thread_id: ThreadId,
642    /// Completed polls, oldest first. Holds about 16 MiB of entries and drops the oldest first.
643    pub timings: TaskTimings,
644    /// The poll that is running now, if any.
645    pub running: Option<ActiveTiming>,
646    /// The longest polls and runtimes seen on this thread.
647    pub stats: TaskStatistics,
648    /// The number of polls pushed to `timings` since the thread started, including evicted ones.
649    /// Counts only while tracing is on.
650    pub total_pushed: u64,
651}
652
653impl ThreadTimings {
654    /// Creates empty timing state for a thread.
655    pub fn new(thread_name: Option<String>, thread_id: ThreadId) -> Self {
656        ThreadTimings {
657            thread_name,
658            thread_id,
659            timings: TaskTimings::new(),
660            stats: TaskStatistics::default(),
661            total_pushed: 0,
662            running: None,
663        }
664    }
665
666    /// Marks a poll as started now. Called before each task poll.
667    #[cfg(feature = "profiler")]
668    pub fn update_running_task(
669        &mut self,
670        spawned: SpawnTime,
671        location: &'static std::panic::Location<'_>,
672    ) {
673        let start = Instant::now();
674        self.running = Some(ActiveTiming {
675            spawned,
676            location,
677            start,
678        });
679    }
680    /// Does nothing, because the `profiler` feature is off.
681    #[cfg(not(feature = "profiler"))]
682    pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {}
683
684    /// Ends the running poll at `ended`, records it in the statistics and, while tracing is on, in the buffer.
685    ///
686    /// # Panics
687    ///
688    /// Panics if no poll is running. It must follow a call to [`Self::update_running_task`].
689    #[cfg(feature = "profiler")]
690    pub fn save_task_timing(&mut self, ended: YieldTime) -> TaskTiming {
691        let ActiveTiming {
692            location,
693            start,
694            spawned,
695        } = self
696            .running
697            .take()
698            .expect("this function is only ever called after register_task_start");
699
700        let timing = TaskTiming {
701            location,
702            spawned,
703            start,
704            end: ended,
705        };
706        self.stats.add_yield_timing(timing);
707        self.stats.add_runtime(timing);
708
709        if trace_enabled() {
710            cold_path(); // optimize for when the profiling is off
711            if self.timings.len() >= MAX_TASK_TIMINGS {
712                self.timings.pop_front();
713            }
714            self.timings.push_back(timing);
715            self.total_pushed += 1;
716        }
717        timing
718    }
719    /// Does nothing, because the `profiler` feature is off.
720    #[cfg(not(feature = "profiler"))]
721    pub fn save_task_timing(&mut self, _: YieldTime) {}
722
723    /// Returns a copy of this thread's recorded polls and statistics.
724    ///
725    /// The running poll is included when `includes` is [`TasksIncluded::CompletedAndRunning`],
726    /// with its end set to the time of this call.
727    pub fn get_thread_task_timings(&self, includes: TasksIncluded) -> ThreadTaskTimings {
728        ThreadTaskTimings {
729            thread_name: self.thread_name.clone(),
730            thread_id: self.thread_id,
731            timings: self
732                .timings
733                .iter()
734                .cloned()
735                .chain(
736                    self.running
737                        .filter(|_| matches!(includes, TasksIncluded::CompletedAndRunning))
738                        .map(|running| TaskTiming {
739                            spawned: running.spawned,
740                            location: running.location,
741                            start: running.start,
742                            end: YieldTime(Instant::now()),
743                        }),
744                )
745                .collect(),
746            stats: self.stats.clone(),
747            total_pushed: self.total_pushed,
748        }
749    }
750}
751
752impl Drop for ThreadTimings {
753    fn drop(&mut self) {
754        let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock();
755
756        let Some((index, _)) = thread_timings
757            .iter()
758            .enumerate()
759            .find(|(_, t)| t.thread_id == self.thread_id)
760        else {
761            return;
762        };
763        thread_timings.swap_remove(index);
764    }
765}
766
767/// Marks a task poll as started on the calling thread. Called by the scheduler before each poll.
768#[doc(hidden)]
769pub fn update_running_task(spawned: SpawnTime, location: &'static std::panic::Location<'_>) {
770    #[cfg(feature = "profiler")]
771    journal::begin_foreground_turn();
772    THREAD_TIMINGS.with(|timings| {
773        timings.lock().update_running_task(spawned, location);
774    });
775}
776
777/// Ends the task poll that [`update_running_task`] started on the calling thread.
778#[doc(hidden)]
779pub fn save_task_timing() {
780    let yielded_at = YieldTime(Instant::now());
781    #[cfg(feature = "profiler")]
782    {
783        let timing = THREAD_TIMINGS.with(|timings| timings.lock().save_task_timing(yielded_at));
784        journal::record_task_poll(timing);
785    }
786    #[cfg(not(feature = "profiler"))]
787    THREAD_TIMINGS.with(|timings| {
788        timings.lock().save_task_timing(yielded_at);
789    });
790}
791
792/// Returns the calling thread's recorded polls and statistics.
793#[doc(hidden)]
794pub fn get_current_thread_task_timings(include_running: TasksIncluded) -> ThreadTaskTimings {
795    THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings(include_running))
796}
797
798const TRACE_SETTING_ENABLED: u64 = 1 << 63;
799const TRACE_SCOPE_COUNT_MASK: u64 = TRACE_SETTING_ENABLED - 1;
800static TRACE_STATE: AtomicU64 = AtomicU64::new(0);
801
802/// Turns trace collection on or off, and returns `true` if the setting changed.
803///
804/// While tracing is off, task polls and frame events update only the cheap
805/// statistics, and nothing is added to the trace buffers. Turning tracing off
806/// clears the per-thread task buffers and the frame-event buffer, unless a trace
807/// scope is still active, so a later re-enable doesn't report stale data. Active
808/// trace scopes keep collection on until the last scope ends, whatever this
809/// setting is. Passing the current setting changes nothing and returns `false`.
810///
811/// # Examples
812///
813/// ```
814/// use gpui::profiler::{set_trace_enabled, trace_enabled};
815///
816/// assert!(set_trace_enabled(true));
817/// assert!(trace_enabled());
818/// assert!(!set_trace_enabled(true), "already on, so nothing changed");
819/// set_trace_enabled(false);
820/// ```
821pub fn set_trace_enabled(enabled: bool) -> bool {
822    let mut state = TRACE_STATE.load(Ordering::Acquire);
823    loop {
824        let was_enabled = state & TRACE_SETTING_ENABLED != 0;
825        if was_enabled == enabled {
826            return false;
827        }
828
829        let next_state = if enabled {
830            state | TRACE_SETTING_ENABLED
831        } else {
832            state & TRACE_SCOPE_COUNT_MASK
833        };
834        match TRACE_STATE.compare_exchange_weak(
835            state,
836            next_state,
837            Ordering::AcqRel,
838            Ordering::Acquire,
839        ) {
840            Ok(_) => {
841                if next_state == 0 {
842                    clear_trace_buffers();
843                }
844                return true;
845            }
846            Err(updated_state) => state = updated_state,
847        }
848    }
849}
850
851/// Keeps trace collection on while it lives. Created by [`trace_scope`].
852#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
853pub(crate) struct TraceGuard;
854
855/// Starts a trace scope, so tracing stays on until the returned guard is dropped.
856#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
857pub(crate) fn trace_scope() -> TraceGuard {
858    let incremented = TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| {
859        (state & TRACE_SCOPE_COUNT_MASK < TRACE_SCOPE_COUNT_MASK).then_some(state + 1)
860    });
861    assert!(incremented.is_ok(), "too many active profiler trace scopes");
862    TraceGuard
863}
864
865#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
866impl Drop for TraceGuard {
867    fn drop(&mut self) {
868        let previous_state =
869            TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| {
870                (state & TRACE_SCOPE_COUNT_MASK > 0).then_some(state - 1)
871            });
872        match previous_state {
873            Ok(1) => clear_trace_buffers(),
874            Ok(_) => {}
875            Err(_) => debug_assert!(false, "profiler trace scope count underflowed"),
876        }
877    }
878}
879
880/// Returns `true` while trace collection is on, either from [`set_trace_enabled`] or from an active trace scope.
881pub fn trace_enabled() -> bool {
882    TRACE_STATE.load(Ordering::Relaxed) != 0
883}
884
885fn clear_trace_buffers() {
886    for (_, timings) in upgraded_thread_timings() {
887        let mut timings = timings.lock();
888        timings.timings.clear();
889        timings.timings.shrink_to_fit();
890        timings.total_pushed = 0;
891    }
892    #[cfg(feature = "profiler")]
893    {
894        let mut frames = FRAME_TIMINGS.lock();
895        frames.timings.clear();
896        frames.timings.shrink_to_fit();
897        frames.total_pushed = 0;
898    }
899}
900
901/// The timing of one drawn window frame. Available with the `profiler` feature.
902#[cfg(feature = "profiler")]
903#[derive(Debug, Copy, Clone)]
904pub struct FrameTiming {
905    /// The window that was drawn.
906    pub window_id: WindowId,
907    /// When the frame first became dirty (its first invalidation). `None` if
908    /// nothing invalidated the window since its previous draw.
909    pub dirty_at: Option<Instant>,
910    /// Number of invalidations coalesced into this frame.
911    pub invalidations: u64,
912    /// When `Window::draw` started.
913    pub draw_start: Instant,
914    /// When `Window::draw` finished.
915    pub draw_end: Instant,
916    /// The refresh interval of the window's display when the frame was drawn,
917    /// or `None` when the platform doesn't report it.
918    pub refresh_interval: Option<Duration>,
919    /// When the platform's frame signal for this frame fired, or `None` when
920    /// the platform doesn't report it. Coalesced signals report the first.
921    pub signal_at: Option<Instant>,
922    /// How long each phase of the draw took.
923    pub phases: DrawPhases,
924}
925
926/// How long each phase of a window draw took.
927#[cfg(feature = "profiler")]
928#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
929pub struct DrawPhases {
930    /// Rendering views into elements and requesting their layout.
931    pub request_layout: Duration,
932    /// Computing layout and prepainting elements, including deferred draws,
933    /// prompts, drags, and tooltips.
934    pub prepaint: Duration,
935    /// Painting elements into the scene.
936    pub paint: Duration,
937}
938
939/// A phase of drawing a view.
940#[cfg(feature = "profiler")]
941#[derive(Debug, Copy, Clone, PartialEq, Eq)]
942pub enum ViewPhase {
943    /// Rendering the view and requesting its layout.
944    RequestLayout,
945    /// Prepainting the view's elements. Views whose cached elements are
946    /// stale also render and lay out here.
947    Prepaint,
948    /// Painting the view's elements.
949    Paint,
950}
951
952#[cfg(feature = "profiler")]
953impl ViewPhase {
954    /// The phase's name, e.g. `"prepaint"`.
955    pub fn as_str(self) -> &'static str {
956        match self {
957            Self::RequestLayout => "request_layout",
958            Self::Prepaint => "prepaint",
959            Self::Paint => "paint",
960        }
961    }
962}
963
964/// Views whose own work in one draw phase takes less than this aren't
965/// recorded, keeping the journal's per-frame cost bounded while still naming
966/// the views that make a frame late.
967#[cfg(feature = "profiler")]
968pub const VIEW_TIMING_FLOOR: Duration = Duration::from_micros(500);
969
970/// One phase of drawing one view, recorded when its own work took at least
971/// [`VIEW_TIMING_FLOOR`].
972#[cfg(feature = "profiler")]
973#[derive(Debug, Copy, Clone)]
974pub struct ViewTiming {
975    /// The view's type name.
976    pub name: &'static str,
977    /// The draw phase.
978    pub phase: ViewPhase,
979    /// When the view's phase began.
980    pub start: Instant,
981    /// When the view's phase ended.
982    pub end: Instant,
983    /// The phase's duration excluding views nested inside it, so a parent
984    /// view isn't blamed for its children's work.
985    pub self_duration: Duration,
986}
987
988/// The least work a frame can take before it counts as late: one refresh
989/// at 120 Hz. Frames rarely fit a single refresh on faster displays yet, so
990/// judging them by it would count nearly every frame as late and hide
991/// regressions; missed refreshes still measure against the real interval.
992#[cfg(feature = "profiler")]
993pub const FRAME_BUDGET_FLOOR: Duration = Duration::from_nanos(8_333_333);
994
995/// The work a frame can take before it counts as late when its display's
996/// refresh interval is unknown: one refresh at 60 Hz, the most common rate,
997/// so frames on faster displays may be judged leniently but none too strictly.
998#[cfg(feature = "profiler")]
999pub const UNKNOWN_REFRESH_FRAME_BUDGET: Duration = Duration::from_nanos(16_666_667);
1000
1001#[cfg(feature = "profiler")]
1002impl FrameTiming {
1003    /// When the frame's work began: the later of its signal and its first
1004    /// invalidation, since waiting for the platform's frame signal isn't work,
1005    /// however late it fires. `None` without a frame signal.
1006    pub fn work_start(&self) -> Option<Instant> {
1007        let signal_at = self.signal_at?;
1008        // A frame signal can fire while the main thread is busy and only be
1009        // handled after the window became dirty; time before then isn't the
1010        // frame's.
1011        Some(
1012            self.dirty_at
1013                .map_or(signal_at, |dirty_at| dirty_at.max(signal_at)),
1014        )
1015    }
1016
1017    /// The main-thread time from [`FrameTiming::work_start`] until the frame
1018    /// was submitted at `submitted_at`: the start of its submission, which may
1019    /// consist mostly of waiting for the display to free a buffer.
1020    pub fn work(&self, submitted_at: Instant) -> Option<Duration> {
1021        Some(submitted_at.saturating_duration_since(self.work_start()?))
1022    }
1023
1024    /// Whether the frame's work, submitted at `submitted_at`, exceeded
1025    /// [`FrameTiming::budget`], when its work is known.
1026    pub fn is_late(&self, submitted_at: Instant) -> Option<bool> {
1027        Some(self.work(submitted_at)? > self.budget())
1028    }
1029
1030    /// The work a frame can take before it counts as late: one refresh
1031    /// interval, but no less than [`FRAME_BUDGET_FLOOR`], or
1032    /// [`UNKNOWN_REFRESH_FRAME_BUDGET`] when the interval is unknown.
1033    pub fn budget(&self) -> Duration {
1034        self.refresh_interval
1035            .map_or(UNKNOWN_REFRESH_FRAME_BUDGET, |interval| {
1036                interval.max(FRAME_BUDGET_FLOOR)
1037            })
1038    }
1039
1040    /// How many refreshes the frame missed when submitted at `submitted_at`:
1041    /// zero when its work fit within one refresh interval.
1042    pub fn missed_refreshes(&self, submitted_at: Instant) -> Option<u64> {
1043        let refresh_interval = self.refresh_interval?;
1044        let work = self.work(submitted_at)?;
1045        let refreshes = work.as_nanos().div_ceil(refresh_interval.as_nanos()).max(1);
1046        Some(u64::try_from(refreshes - 1).unwrap_or(u64::MAX))
1047    }
1048
1049    /// Time spent inside `Window::draw`.
1050    pub fn draw_duration(&self) -> Duration {
1051        self.draw_end.duration_since(self.draw_start)
1052    }
1053
1054    /// Time from the frame's first invalidation to the end of its draw, if the
1055    /// first invalidation was observed.
1056    pub fn dirty_to_draw_duration(&self) -> Option<Duration> {
1057        self.dirty_at
1058            .map(|dirty_at| self.draw_end.duration_since(dirty_at))
1059    }
1060}
1061
1062/// The work spent submitting a window frame to the platform. Available with the `profiler` feature.
1063#[cfg(feature = "profiler")]
1064#[derive(Debug, Copy, Clone)]
1065pub struct PresentTiming {
1066    /// The window whose frame was submitted.
1067    pub window_id: WindowId,
1068    /// When the platform submission began.
1069    pub present_start: Instant,
1070    /// When the platform submission completed.
1071    pub present_end: Instant,
1072    /// The interval since the previous newly drawn frame was submitted, when
1073    /// both frames belong to an active animation.
1074    pub animation_interval: Option<Duration>,
1075}
1076
1077#[cfg(feature = "profiler")]
1078impl PresentTiming {
1079    /// Time spent submitting the frame to the platform.
1080    pub fn present_duration(&self) -> Duration {
1081        self.present_end.duration_since(self.present_start)
1082    }
1083}
1084
1085/// A frame event observed by the profiler. Available with the `profiler` feature.
1086#[cfg(feature = "profiler")]
1087#[derive(Debug, Copy, Clone)]
1088pub enum FrameEvent {
1089    /// A window frame was drawn.
1090    Draw(FrameTiming),
1091    /// A newly drawn window frame was presented.
1092    Present(PresentTiming),
1093}
1094
1095/// A point-in-time copy of a window's frame-duration histograms, for formatting or logging.
1096///
1097/// Get one with [`Window::frame_duration_snapshot`]. Available with the `profiler` feature.
1098#[cfg(feature = "profiler")]
1099#[derive(Clone)]
1100pub struct FrameDurationSnapshot {
1101    /// Histogram of durations from the first invalidation through presentation, in nanoseconds.
1102    pub dirty_to_present_histogram: Histogram<u64>,
1103    /// Histogram of `Window::draw` durations, in nanoseconds.
1104    pub draw_duration_histogram: Histogram<u64>,
1105    /// Histogram of intervals between consecutively presented frames while the
1106    /// window was animating, in nanoseconds.
1107    pub present_interval_histogram: Histogram<u64>,
1108    /// The same frames, split by the refresh interval of the window's display
1109    /// when they were drawn.
1110    pub by_refresh_interval: Vec<RefreshIntervalFrames>,
1111}
1112
1113/// Upper bounds, in milliseconds, of the duration buckets in
1114/// [`RefreshIntervalFrames`]. Each bucket counts values up to and including its
1115/// bound; a final bucket counts everything longer.
1116#[cfg(feature = "profiler")]
1117pub const FRAME_DURATION_BUCKETS_MS: [u64; 9] = [4, 8, 16, 33, 50, 100, 250, 500, 1000];
1118
1119/// Upper bounds, in percent of the display's real refresh interval, of the
1120/// work-per-frame buckets in [`RefreshIntervalFrames`]. Each bucket counts
1121/// values up to and including its bound; a final bucket counts everything
1122/// more. Whether a frame was on time is judged by [`FrameTiming::budget`]
1123/// instead, which never drops below [`FRAME_BUDGET_FLOOR`], so on displays
1124/// faster than 120 Hz frames above 100% can still be on time.
1125#[cfg(feature = "profiler")]
1126pub const FRAME_WORK_BUCKETS_PERCENT: [u64; 7] = [25, 50, 75, 100, 150, 200, 400];
1127
1128/// Counts `histogram`'s values above the previous bound and up to and
1129/// including each of `upper_bounds`, plus a final count of the values above
1130/// the last bound.
1131#[cfg(feature = "profiler")]
1132pub fn histogram_bucket_counts(
1133    histogram: &Histogram<u64>,
1134    upper_bounds: impl IntoIterator<Item = u64>,
1135) -> Vec<u64> {
1136    let mut counted = 0;
1137    let mut counts: Vec<u64> = upper_bounds
1138        .into_iter()
1139        .map(|bound| {
1140            // Cumulative counts, since adjacent bounds can share a histogram
1141            // bucket.
1142            let cumulative = histogram.count_between(0, bound);
1143            let count = cumulative.saturating_sub(counted);
1144            counted = counted.max(cumulative);
1145            count
1146        })
1147        .collect();
1148    counts.push(histogram.len().saturating_sub(counted));
1149    counts
1150}
1151
1152/// Statistics for the frames a window drew and presented while its display had
1153/// one refresh interval.
1154///
1155/// A frame is on time when its [`FrameTiming::work`] fit within
1156/// [`FrameTiming::budget`].
1157#[cfg(feature = "profiler")]
1158#[derive(Clone, Debug)]
1159pub struct RefreshIntervalFrames {
1160    /// The display's refresh interval, or `None` when the platform doesn't
1161    /// report it. Frame pacing is only measured for known intervals.
1162    pub refresh_interval: Option<Duration>,
1163    /// Frames drawn, whether or not they were presented.
1164    pub frames_drawn: u64,
1165    /// Draw durations, in nanoseconds.
1166    pub draw_duration: Histogram<u64>,
1167    /// Presented frames whose work fit within [`FrameTiming::budget`].
1168    pub frames_on_time: u64,
1169    /// Presented frames whose work exceeded [`FrameTiming::budget`].
1170    pub frames_late: u64,
1171    /// Refreshes that on-time and late frames missed in total, when the
1172    /// refresh interval is known.
1173    pub missed_refreshes: u64,
1174    /// Presented frames without a frame signal to measure their work from.
1175    pub frames_unmeasured: u64,
1176    /// Work of on-time and late frames, in nanoseconds.
1177    pub work: Histogram<u64>,
1178    /// Of `frames_on_time`, the frames that responded to input.
1179    pub input_frames_on_time: u64,
1180    /// Of `frames_late`, the frames that responded to input.
1181    pub input_frames_late: u64,
1182    /// Work of on-time and late frames that responded to input, in
1183    /// nanoseconds.
1184    pub input_work: Histogram<u64>,
1185}
1186
1187#[cfg(feature = "profiler")]
1188impl RefreshIntervalFrames {
1189    fn new(refresh_interval: Option<Duration>, empty_histogram: &Histogram<u64>) -> Self {
1190        Self {
1191            refresh_interval,
1192            frames_drawn: 0,
1193            draw_duration: empty_histogram.clone(),
1194            frames_on_time: 0,
1195            frames_late: 0,
1196            missed_refreshes: 0,
1197            frames_unmeasured: 0,
1198            work: empty_histogram.clone(),
1199            input_frames_on_time: 0,
1200            input_frames_late: 0,
1201            input_work: empty_histogram.clone(),
1202        }
1203    }
1204
1205    /// The frames recorded since `earlier`, a previous snapshot of the same
1206    /// statistics.
1207    pub fn since(&self, earlier: &Self) -> Self {
1208        let subtract = |current: &Histogram<u64>, earlier: &Histogram<u64>| {
1209            let mut delta = current.clone();
1210            match delta.subtract(earlier) {
1211                Ok(()) => delta,
1212                Err(error) => {
1213                    log::error!("failed to subtract frame histograms: {error}");
1214                    current.clone()
1215                }
1216            }
1217        };
1218        Self {
1219            refresh_interval: self.refresh_interval,
1220            frames_drawn: self.frames_drawn.saturating_sub(earlier.frames_drawn),
1221            draw_duration: subtract(&self.draw_duration, &earlier.draw_duration),
1222            frames_on_time: self.frames_on_time.saturating_sub(earlier.frames_on_time),
1223            frames_late: self.frames_late.saturating_sub(earlier.frames_late),
1224            missed_refreshes: self
1225                .missed_refreshes
1226                .saturating_sub(earlier.missed_refreshes),
1227            frames_unmeasured: self
1228                .frames_unmeasured
1229                .saturating_sub(earlier.frames_unmeasured),
1230            work: subtract(&self.work, &earlier.work),
1231            input_frames_on_time: self
1232                .input_frames_on_time
1233                .saturating_sub(earlier.input_frames_on_time),
1234            input_frames_late: self
1235                .input_frames_late
1236                .saturating_sub(earlier.input_frames_late),
1237            input_work: subtract(&self.input_work, &earlier.input_work),
1238        }
1239    }
1240
1241    fn record_presented_frame(
1242        &mut self,
1243        frame: &FrameTiming,
1244        presentation: &PresentTiming,
1245        responded_to_input: bool,
1246    ) {
1247        let submitted_at = presentation.present_start;
1248        let (Some(work), Some(is_late)) = (frame.work(submitted_at), frame.is_late(submitted_at))
1249        else {
1250            self.frames_unmeasured += 1;
1251            return;
1252        };
1253        record_duration(&mut self.work, work);
1254        if responded_to_input {
1255            record_duration(&mut self.input_work, work);
1256        }
1257        self.missed_refreshes += frame.missed_refreshes(submitted_at).unwrap_or(0);
1258        if is_late {
1259            self.frames_late += 1;
1260            self.input_frames_late += u64::from(responded_to_input);
1261        } else {
1262            self.frames_on_time += 1;
1263            self.input_frames_on_time += u64::from(responded_to_input);
1264        }
1265    }
1266}
1267
1268/// Records `duration` in nanoseconds, growing the histogram as needed.
1269#[cfg(feature = "profiler")]
1270fn record_duration(histogram: &mut Histogram<u64>, duration: Duration) {
1271    let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
1272    if let Err(error) = histogram.record(nanos) {
1273        log::error!("failed to record frame duration: {error}");
1274    }
1275}
1276
1277/// A point-in-time copy of a window's input-latency histograms, for formatting or logging.
1278///
1279/// Get one with [`Window::input_latency_snapshot`]. Available with the `profiler` feature.
1280#[cfg(feature = "profiler")]
1281#[derive(Clone)]
1282pub struct InputLatencySnapshot {
1283    /// Histogram of input-to-frame latency samples, in nanoseconds.
1284    pub latency_histogram: Histogram<u64>,
1285    /// Histogram of input events coalesced per rendered frame.
1286    pub events_per_frame_histogram: Histogram<u64>,
1287    /// Count of input events that arrived mid-draw and were excluded from
1288    /// latency recording.
1289    pub mid_draw_events_dropped: u64,
1290}
1291
1292#[cfg(feature = "profiler")]
1293enum WindowActivity {
1294    Input {
1295        started_at: Instant,
1296        kind: &'static str,
1297    },
1298    Draw {
1299        started_at: Instant,
1300    },
1301}
1302
1303/// Collects profiling information for one window.
1304///
1305/// Aggregate histograms are always populated when the `profiler` feature is
1306/// compiled in. Individual draw and present events are added to the global
1307/// profiler buffer only while tracing is enabled. Each window owns one. Read its
1308/// results with [`Window::frame_duration_snapshot`] and [`Window::input_latency_snapshot`].
1309#[cfg(feature = "profiler")]
1310pub struct WindowProfiler {
1311    window_id: WindowId,
1312    active_activities: SmallVec<[WindowActivity; 4]>,
1313    active_actions: SmallVec<[(&'static str, Instant); 2]>,
1314    dirty_to_present_histogram: Histogram<u64>,
1315    draw_duration_histogram: Histogram<u64>,
1316    present_interval_histogram: Histogram<u64>,
1317    first_input_at: Option<Instant>,
1318    pending_input_count: u64,
1319    input_latency_histogram: Histogram<u64>,
1320    events_per_frame_histogram: Histogram<u64>,
1321    mid_draw_events_dropped: u64,
1322    last_present_at: Option<Instant>,
1323    animating_at_last_present: bool,
1324    pending_frame: Option<FrameTiming>,
1325    refresh_interval: Option<Duration>,
1326    frame_signal_at: Option<Instant>,
1327    by_refresh_interval: Vec<RefreshIntervalFrames>,
1328    /// Cloned for each new refresh interval's histograms, since creating one
1329    /// can fail.
1330    empty_histogram: Histogram<u64>,
1331    draw_phases: DrawPhases,
1332    /// For each view phase in progress, the time spent in views nested
1333    /// inside it so far.
1334    nested_view_time: SmallVec<[Duration; 16]>,
1335}
1336
1337#[cfg(feature = "profiler")]
1338impl WindowProfiler {
1339    /// Creates a profiler for the window `window_id`.
1340    ///
1341    /// # Errors
1342    ///
1343    /// Returns an error if one of the histograms can't be allocated.
1344    pub fn new(window_id: WindowId) -> anyhow::Result<Self> {
1345        let profiler = Self {
1346            window_id,
1347            active_activities: SmallVec::new(),
1348            active_actions: SmallVec::new(),
1349            dirty_to_present_histogram: Histogram::new(3).map_err(|error| {
1350                anyhow::anyhow!("Failed to create dirty-to-present histogram: {error}")
1351            })?,
1352            draw_duration_histogram: Histogram::new(3).map_err(|error| {
1353                anyhow::anyhow!("Failed to create draw duration histogram: {error}")
1354            })?,
1355            present_interval_histogram: Histogram::new(3).map_err(|error| {
1356                anyhow::anyhow!("Failed to create present interval histogram: {error}")
1357            })?,
1358            first_input_at: None,
1359            pending_input_count: 0,
1360            input_latency_histogram: Histogram::new(3).map_err(|error| {
1361                anyhow::anyhow!("Failed to create input latency histogram: {error}")
1362            })?,
1363            events_per_frame_histogram: Histogram::new(3).map_err(|error| {
1364                anyhow::anyhow!("Failed to create events per frame histogram: {error}")
1365            })?,
1366            mid_draw_events_dropped: 0,
1367            last_present_at: None,
1368            animating_at_last_present: false,
1369            pending_frame: None,
1370            refresh_interval: None,
1371            frame_signal_at: None,
1372            by_refresh_interval: Vec::new(),
1373            empty_histogram: Histogram::new(3)
1374                .map_err(|error| anyhow::anyhow!("Failed to create frame histogram: {error}"))?,
1375            draw_phases: DrawPhases::default(),
1376            nested_view_time: SmallVec::new(),
1377        };
1378        journal::record_frame_pending(window_id, Instant::now());
1379        Ok(profiler)
1380    }
1381
1382    /// Records the start of an input dispatch. `kind` names the platform
1383    /// input variant being dispatched (see [`crate::PlatformInput::kind_name`]).
1384    ///
1385    /// Each call needs a matching [`Self::end_input`].
1386    pub fn begin_input(&mut self, kind: &'static str) {
1387        journal::begin_foreground_turn();
1388        self.active_activities.push(WindowActivity::Input {
1389            started_at: Instant::now(),
1390            kind,
1391        });
1392    }
1393
1394    /// Records the end of the input dispatch that [`Self::begin_input`] started.
1395    ///
1396    /// If the input caused an invalidation and didn't arrive during a draw, it counts
1397    /// toward the input-latency histograms at the next present. Input that arrives
1398    /// during a draw is counted in `mid_draw_events_dropped` instead.
1399    pub fn end_input(&mut self, caused_invalidation: bool) {
1400        let Some(WindowActivity::Input { started_at, kind }) = self.active_activities.pop() else {
1401            debug_assert!(false, "input activity must be the current window activity");
1402            journal::end_foreground_turn();
1403            return;
1404        };
1405
1406        if !journal::power_interrupted_since(started_at) {
1407            journal::record_input(journal::InputTiming {
1408                kind,
1409                start: started_at,
1410                end: Instant::now(),
1411                caused_invalidation,
1412            });
1413        }
1414        journal::end_foreground_turn();
1415
1416        if !caused_invalidation || !journal::frame_sample_is_valid(self.window_id, started_at) {
1417            return;
1418        }
1419        if self
1420            .first_input_at
1421            .is_some_and(|at| !journal::frame_sample_is_valid(self.window_id, at))
1422        {
1423            self.first_input_at = None;
1424            self.pending_input_count = 0;
1425        }
1426
1427        let arrived_during_draw = self
1428            .active_activities
1429            .iter()
1430            .any(|activity| matches!(activity, WindowActivity::Draw { .. }));
1431        if arrived_during_draw {
1432            self.mid_draw_events_dropped += 1;
1433        } else {
1434            self.first_input_at.get_or_insert(started_at);
1435            self.pending_input_count += 1;
1436        }
1437    }
1438
1439    /// Records the start of the handler for `action`. Pair it with [`Self::end_action_handler`].
1440    pub fn begin_action_handler(&mut self, action: &(dyn Action + 'static), cx: &mut App) {
1441        journal::begin_foreground_turn();
1442        let name = actions::update_running_action(action, cx);
1443        self.active_actions.push((name, Instant::now()));
1444    }
1445
1446    /// Records the end of the current action handler.
1447    pub fn end_action_handler(&mut self) {
1448        // Dual-write to the legacy aggregate store; its single global running
1449        // slot misbehaves when tests run actions concurrently, which is why
1450        // the journal entry is tracked here on the window instead.
1451        actions::save_action_timing();
1452        let Some((name, start)) = self.active_actions.pop() else {
1453            debug_assert!(false, "action handler must be begun before it ends");
1454            journal::end_foreground_turn();
1455            return;
1456        };
1457        if !journal::power_interrupted_since(start) {
1458            journal::record_action(ActionTiming {
1459                name,
1460                start,
1461                end: Instant::now(),
1462            });
1463        }
1464        journal::end_foreground_turn();
1465    }
1466
1467    /// Records when the frame signal for the frame the next draw will produce
1468    /// fired. Coalesced signals report the first.
1469    pub fn record_frame_signal(&mut self, signal_at: Option<Instant>) {
1470        self.frame_signal_at = signal_at;
1471    }
1472
1473    /// Sets the refresh interval of the window's display, which frames drawn
1474    /// from now on are attributed to.
1475    pub fn set_refresh_interval(&mut self, refresh_interval: Option<Duration>) {
1476        self.refresh_interval = refresh_interval;
1477    }
1478
1479    /// Records the beginning of a window draw.
1480    pub fn begin_draw(&mut self) {
1481        journal::begin_foreground_turn();
1482        let started_at = Instant::now();
1483        journal::record_frame_pending(self.window_id, started_at);
1484        self.active_activities
1485            .push(WindowActivity::Draw { started_at });
1486    }
1487
1488    /// Records the phases of the window draw in progress.
1489    pub fn record_draw_phases(&mut self, phases: DrawPhases) {
1490        self.draw_phases = phases;
1491    }
1492
1493    /// Records the start of one phase of drawing a view, returning its start
1494    /// time to pass to [`WindowProfiler::end_view`].
1495    pub fn begin_view(&mut self) -> Instant {
1496        self.nested_view_time.push(Duration::ZERO);
1497        Instant::now()
1498    }
1499
1500    /// Records the end of a view phase begun at `start`, journaling it when
1501    /// its own work reached [`VIEW_TIMING_FLOOR`].
1502    pub fn end_view(&mut self, start: Instant, name: &'static str, phase: ViewPhase) {
1503        let end = Instant::now();
1504        let duration = end.saturating_duration_since(start);
1505        let nested = self.nested_view_time.pop().unwrap_or_default();
1506        if let Some(parent_nested) = self.nested_view_time.last_mut() {
1507            *parent_nested += duration;
1508        }
1509        let self_duration = duration.saturating_sub(nested);
1510        if self_duration >= VIEW_TIMING_FLOOR {
1511            journal::record_view(ViewTiming {
1512                name,
1513                phase,
1514                start,
1515                end,
1516                self_duration,
1517            });
1518        }
1519    }
1520
1521    /// Records the end of the draw that [`Self::begin_draw`] started, and returns how long it took.
1522    ///
1523    /// `dirty_at` is when the frame was first invalidated, and `invalidations` is how many
1524    /// invalidations this frame covers. Returns [`Duration::ZERO`] if no draw was begun.
1525    pub fn end_draw(&mut self, dirty_at: Option<Instant>, invalidations: u64) -> Duration {
1526        let Some(WindowActivity::Draw {
1527            started_at: draw_start,
1528        }) = self.active_activities.pop()
1529        else {
1530            debug_assert!(false, "draw activity must be the current window activity");
1531            journal::end_foreground_turn();
1532            return Duration::ZERO;
1533        };
1534
1535        let draw_end = Instant::now();
1536        // Taken even when the draw isn't recorded, so a later draw can't claim it.
1537        let signal_at = self.frame_signal_at.take();
1538        let dirty_at = dirty_at.filter(|at| journal::frame_sample_is_valid(self.window_id, *at));
1539        let frame_timing = FrameTiming {
1540            window_id: self.window_id,
1541            dirty_at,
1542            invalidations,
1543            draw_start,
1544            draw_end,
1545            refresh_interval: self.refresh_interval,
1546            signal_at,
1547            phases: std::mem::take(&mut self.draw_phases),
1548        };
1549        let draw_duration = frame_timing.draw_duration();
1550        if !journal::power_interrupted_since(draw_start) {
1551            self.record_draw_timing(frame_timing);
1552        }
1553        journal::end_foreground_turn();
1554        draw_duration
1555    }
1556
1557    /// Records that the window's latest frame was presented between `present_start`
1558    /// and `present_end`.
1559    ///
1560    /// `window_active` says whether the window is active, and `next_frame_scheduled`
1561    /// says whether another frame is already requested. Together they decide whether
1562    /// the gap until the next newly drawn frame counts as an animation interval.
1563    pub fn record_present(
1564        &mut self,
1565        present_start: Instant,
1566        present_end: Instant,
1567        window_active: bool,
1568        next_frame_scheduled: bool,
1569    ) {
1570        self.record_present_at(
1571            present_start,
1572            present_end,
1573            window_active,
1574            next_frame_scheduled,
1575        );
1576    }
1577
1578    /// Returns a copy of the input-latency histograms. Latency runs from the first input
1579    /// that caused an invalidation to the present of the frame it caused.
1580    pub fn input_latency_snapshot(&self) -> InputLatencySnapshot {
1581        InputLatencySnapshot {
1582            latency_histogram: self.input_latency_histogram.clone(),
1583            events_per_frame_histogram: self.events_per_frame_histogram.clone(),
1584            mid_draw_events_dropped: self.mid_draw_events_dropped,
1585        }
1586    }
1587
1588    /// Returns a copy of the frame-duration histograms, in nanoseconds.
1589    pub fn frame_duration_snapshot(&self) -> FrameDurationSnapshot {
1590        FrameDurationSnapshot {
1591            dirty_to_present_histogram: self.dirty_to_present_histogram.clone(),
1592            draw_duration_histogram: self.draw_duration_histogram.clone(),
1593            present_interval_histogram: self.present_interval_histogram.clone(),
1594            by_refresh_interval: self.by_refresh_interval.clone(),
1595        }
1596    }
1597
1598    fn frames_for(&mut self, refresh_interval: Option<Duration>) -> &mut RefreshIntervalFrames {
1599        let index = match self
1600            .by_refresh_interval
1601            .iter()
1602            .position(|frames| frames.refresh_interval == refresh_interval)
1603        {
1604            Some(index) => index,
1605            None => {
1606                self.by_refresh_interval.push(RefreshIntervalFrames::new(
1607                    refresh_interval,
1608                    &self.empty_histogram,
1609                ));
1610                self.by_refresh_interval.len() - 1
1611            }
1612        };
1613        &mut self.by_refresh_interval[index]
1614    }
1615
1616    fn record_present_at(
1617        &mut self,
1618        present_start: Instant,
1619        present_end: Instant,
1620        window_active: bool,
1621        next_frame_scheduled: bool,
1622    ) {
1623        let mut responded_to_input = false;
1624        if let Some(first_input_at) = self.first_input_at.take()
1625            && journal::frame_sample_is_valid(self.window_id, first_input_at)
1626        {
1627            responded_to_input = true;
1628            let latency_nanos = present_end.duration_since(first_input_at).as_nanos() as u64;
1629            self.input_latency_histogram.record(latency_nanos).ok();
1630            if self.pending_input_count > 0 {
1631                self.events_per_frame_histogram
1632                    .record(self.pending_input_count)
1633                    .ok();
1634            }
1635        }
1636        self.pending_input_count = 0;
1637
1638        let frame = self
1639            .pending_frame
1640            .take()
1641            .filter(|frame| journal::frame_sample_is_valid(self.window_id, frame.draw_start))
1642            .map(|mut frame| {
1643                frame.dirty_at = frame
1644                    .dirty_at
1645                    .filter(|at| journal::frame_sample_is_valid(self.window_id, *at));
1646                frame
1647            });
1648        let animation_interval =
1649            if frame.is_some() && self.animating_at_last_present && window_active {
1650                self.last_present_at
1651                    .filter(|at| journal::frame_sample_is_valid(self.window_id, *at))
1652                    .map(|last_present_at| present_end.duration_since(last_present_at))
1653            } else {
1654                None
1655            };
1656        let present_timing = PresentTiming {
1657            window_id: self.window_id,
1658            present_start,
1659            present_end,
1660            animation_interval,
1661        };
1662        journal::record_present(present_timing, frame);
1663
1664        let Some(frame) = frame else {
1665            return;
1666        };
1667        self.frames_for(frame.refresh_interval)
1668            .record_presented_frame(&frame, &present_timing, responded_to_input);
1669
1670        if let Some(dirty_at) = frame.dirty_at
1671            && let Err(error) = self
1672                .dirty_to_present_histogram
1673                .record(present_end.duration_since(dirty_at).as_nanos() as u64)
1674        {
1675            log::error!("failed to record dirty-to-present frame timing: {error}");
1676        }
1677
1678        if let Some(animation_interval) = animation_interval {
1679            self.present_interval_histogram
1680                .record(animation_interval.as_nanos() as u64)
1681                .ok();
1682        }
1683        record_frame_event(FrameEvent::Present(present_timing));
1684
1685        self.last_present_at = Some(present_end);
1686        self.animating_at_last_present = next_frame_scheduled && window_active;
1687    }
1688
1689    fn record_draw_timing(&mut self, timing: FrameTiming) {
1690        self.record_draw_duration(timing.draw_duration());
1691        let frames = self.frames_for(timing.refresh_interval);
1692        frames.frames_drawn += 1;
1693        record_duration(&mut frames.draw_duration, timing.draw_duration());
1694        self.pending_frame = Some(timing);
1695        record_frame_event(FrameEvent::Draw(timing));
1696        journal::record_draw(timing);
1697    }
1698
1699    fn record_draw_duration(&mut self, duration: Duration) {
1700        self.draw_duration_histogram
1701            .record(duration.as_nanos() as u64)
1702            .ok();
1703    }
1704}
1705
1706#[cfg(feature = "profiler")]
1707impl Drop for WindowProfiler {
1708    fn drop(&mut self) {
1709        journal::record_window_closed(self.window_id);
1710    }
1711}
1712
1713// Allow 16MiB of frame event entries.
1714#[cfg(feature = "profiler")]
1715const MAX_FRAME_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::<FrameEvent>();
1716
1717#[cfg(feature = "profiler")]
1718struct FrameTimings {
1719    timings: VecDeque<FrameEvent>,
1720    total_pushed: u64,
1721}
1722
1723#[cfg(feature = "profiler")]
1724static FRAME_TIMINGS: spin::Mutex<FrameTimings> = spin::Mutex::new(FrameTimings {
1725    timings: VecDeque::new(),
1726    total_pushed: 0,
1727});
1728
1729/// Adds a frame event to the global trace buffer.
1730///
1731/// Does nothing unless tracing is enabled with [`set_trace_enabled`]. Available with the
1732/// `profiler` feature.
1733#[cfg(feature = "profiler")]
1734pub fn record_frame_event(event: FrameEvent) {
1735    if !trace_enabled() {
1736        return;
1737    }
1738    cold_path(); // optimize for when profiling is off
1739
1740    let mut frames = FRAME_TIMINGS.lock();
1741    if frames.timings.len() >= MAX_FRAME_TIMINGS {
1742        frames.timings.pop_front();
1743    }
1744    frames.timings.push_back(event);
1745    frames.total_pushed += 1;
1746}
1747
1748/// Reads the frame events recorded after this collector was created.
1749///
1750/// Each call to [`Self::collect_unseen`] returns only the events recorded since the
1751/// previous call. Available with the `profiler` feature.
1752#[cfg(feature = "profiler")]
1753pub struct FrameTimingCollector {
1754    cursor: u64,
1755}
1756
1757#[cfg(feature = "profiler")]
1758impl Default for FrameTimingCollector {
1759    fn default() -> Self {
1760        Self::new()
1761    }
1762}
1763
1764#[cfg(feature = "profiler")]
1765impl FrameTimingCollector {
1766    /// Creates a collector that only sees frame events recorded from this point on.
1767    pub fn new() -> Self {
1768        Self {
1769            cursor: FRAME_TIMINGS.lock().total_pushed,
1770        }
1771    }
1772
1773    /// Returns frame events recorded since the previous call (or since the
1774    /// collector was created). If the ring buffer wrapped around since the
1775    /// previous poll, the evicted entries are lost.
1776    pub fn collect_unseen(&mut self) -> Vec<FrameEvent> {
1777        let frames = FRAME_TIMINGS.lock();
1778        let buffer_len = frames.timings.len() as u64;
1779        let buffer_start = frames.total_pushed.saturating_sub(buffer_len);
1780        let skip = self.cursor.saturating_sub(buffer_start) as usize;
1781        let unseen = frames
1782            .timings
1783            .iter()
1784            .skip(skip.min(frames.timings.len()))
1785            .copied()
1786            .collect();
1787        self.cursor = frames.total_pushed;
1788        unseen
1789    }
1790}
1791
1792#[cfg(all(test, feature = "profiler"))]
1793mod tests {
1794    use super::*;
1795    use std::sync::{Mutex, MutexGuard};
1796
1797    #[test]
1798    fn long_poll_sample_is_ranked_by_poll_time_not_wait_time() {
1799        let spawned = Instant::now();
1800        let mut stats = TaskStatistics::default();
1801        for microseconds in [500, 100, 400, 200, 300, 50, 350, 150, 600] {
1802            // Every task waited 10 seconds to start, which must not affect its ranking.
1803            let start = spawned + Duration::from_secs(10);
1804            stats.add_yield_timing(TaskTiming {
1805                location: std::panic::Location::caller(),
1806                spawned: SpawnTime(spawned),
1807                start,
1808                end: YieldTime(start + Duration::from_micros(microseconds)),
1809            });
1810        }
1811
1812        let mut retained = stats.longest_poll_times.map(|task| task.poll_duration());
1813        retained.sort();
1814        assert_eq!(
1815            retained,
1816            [300, 350, 400, 500, 600].map(Duration::from_micros)
1817        );
1818        assert_eq!(stats.poll_time_to_beat, Duration::from_micros(300));
1819    }
1820
1821    #[test]
1822    fn short_polls_do_not_fill_unused_long_poll_slots() {
1823        let spawned = Instant::now();
1824        let mut stats = TaskStatistics::default();
1825        for microseconds in [200, 50, 100, 20] {
1826            stats.add_yield_timing(TaskTiming {
1827                location: std::panic::Location::caller(),
1828                spawned: SpawnTime(spawned),
1829                start: spawned,
1830                end: YieldTime(spawned + Duration::from_micros(microseconds)),
1831            });
1832        }
1833
1834        assert_eq!(stats.poll_time_to_beat, Duration::from_micros(100));
1835        let retained = stats
1836            .longest_poll_times
1837            .iter()
1838            .filter(|task| !task.poll_duration().is_zero())
1839            .count();
1840        assert_eq!(retained, 2, "only the 200 and 100 microsecond polls count");
1841    }
1842
1843    #[test]
1844    fn interruptions_drop_latency_samples_but_hiding_keeps_draw_work() {
1845        for sleep in [false, true] {
1846            let (_journal, _guard) = journal::install_test_foreground_journal(64, 4);
1847            let id = WindowId::from(1);
1848            let mut profiler = WindowProfiler::new(id).expect("valid histograms");
1849            let old = Instant::now() - Duration::from_secs(1);
1850            record_test_draw(&mut profiler, old);
1851            profiler.record_present_at(old, old, true, true);
1852            profiler.first_input_at = Some(old);
1853            profiler.pending_input_count = 1;
1854            profiler.begin_draw();
1855            if sleep {
1856                journal::record_power_transition(journal::PowerState::Suspended);
1857                journal::record_power_transition(journal::PowerState::Awake);
1858            } else {
1859                journal::record_window_visibility(id, crate::WindowVisibility::Hidden);
1860                journal::record_window_visibility(id, crate::WindowVisibility::Visible);
1861            }
1862            profiler.end_draw(Some(old), 1);
1863            let now = Instant::now();
1864            profiler.record_present_at(now, now, true, true);
1865            assert_eq!(
1866                profiler.draw_duration_histogram.len(),
1867                if sleep { 1 } else { 2 }
1868            );
1869            assert_eq!(profiler.input_latency_histogram.len(), 0);
1870            assert_eq!(profiler.dirty_to_present_histogram.len(), 1);
1871            assert_eq!(profiler.present_interval_histogram.len(), 0);
1872            profiler.begin_draw();
1873            profiler.end_draw(Some(Instant::now()), 1);
1874            let now = Instant::now();
1875            profiler.record_present_at(now, now, true, true);
1876            assert_eq!(profiler.dirty_to_present_histogram.len(), 2);
1877        }
1878    }
1879
1880    #[test]
1881    fn frame_work_starts_at_the_later_of_signal_and_invalidation() {
1882        let refresh = Duration::from_millis(10);
1883        let start = Instant::now();
1884        let at = |ms| start + Duration::from_millis(ms);
1885        let frame_with = |dirty: Option<u64>, signal: Option<u64>| FrameTiming {
1886            window_id: WindowId::from(1),
1887            dirty_at: dirty.map(at),
1888            invalidations: 1,
1889            draw_start: at(1),
1890            draw_end: at(2),
1891            refresh_interval: Some(refresh),
1892            signal_at: signal.map(at),
1893            phases: Default::default(),
1894        };
1895        let work = |dirty, signal| frame_with(dirty, signal).work(at(50));
1896
1897        // Waiting for the frame signal isn't work, however late it fires.
1898        assert_eq!(work(Some(0), Some(8)), Some(Duration::from_millis(42)));
1899        assert_eq!(work(Some(0), Some(40)), Some(Duration::from_millis(10)));
1900        // A frame needed after its signal fired is measured from when it was
1901        // needed.
1902        assert_eq!(work(Some(5), Some(0)), Some(Duration::from_millis(45)));
1903        // Animation frames have no invalidation to wait for.
1904        assert_eq!(work(None, Some(0)), Some(Duration::from_millis(50)));
1905        assert_eq!(work(Some(0), None), None);
1906
1907        let frame = frame_with(Some(0), Some(0));
1908        for (submitted_ms, missed) in [(7, 0), (10, 0), (20, 1), (25, 2)] {
1909            assert_eq!(frame.missed_refreshes(at(submitted_ms)), Some(missed));
1910        }
1911    }
1912
1913    #[test]
1914    fn frames_are_split_by_refresh_interval_and_classified() {
1915        let (_journal, _guard) = journal::install_test_foreground_journal(64, 4);
1916        let mut profiler = WindowProfiler::new(WindowId::from(1)).expect("valid histograms");
1917        let sixty_hertz = Duration::from_secs(1) / 60;
1918        let one_hundred_twenty_hertz = Duration::from_secs(1) / 120;
1919        let three_hundred_sixty_hertz = Duration::from_secs(1) / 360;
1920        let base = Instant::now();
1921        let at = |ms| base + Duration::from_millis(ms);
1922
1923        struct Frame {
1924            refresh_interval: Option<Duration>,
1925            signal_ms: Option<u64>,
1926            present_start_ms: u64,
1927            responded_to_input: bool,
1928        }
1929        let frames = [
1930            // On time, responding to input.
1931            Frame {
1932                refresh_interval: Some(sixty_hertz),
1933                signal_ms: Some(1),
1934                present_start_ms: 5,
1935                responded_to_input: true,
1936            },
1937            Frame {
1938                refresh_interval: Some(sixty_hertz),
1939                signal_ms: Some(0),
1940                present_start_ms: 40,
1941                responded_to_input: true,
1942            },
1943            Frame {
1944                refresh_interval: Some(sixty_hertz),
1945                signal_ms: None,
1946                present_start_ms: 5,
1947                responded_to_input: false,
1948            },
1949            // A late frame signal: only the 2 ms after it count.
1950            Frame {
1951                refresh_interval: Some(sixty_hertz),
1952                signal_ms: Some(40),
1953                present_start_ms: 42,
1954                responded_to_input: false,
1955            },
1956            Frame {
1957                refresh_interval: Some(one_hundred_twenty_hertz),
1958                signal_ms: Some(0),
1959                present_start_ms: 12,
1960                responded_to_input: false,
1961            },
1962            // Misses refreshes but fits the budget floor.
1963            Frame {
1964                refresh_interval: Some(three_hundred_sixty_hertz),
1965                signal_ms: Some(0),
1966                present_start_ms: 6,
1967                responded_to_input: false,
1968            },
1969            Frame {
1970                refresh_interval: Some(three_hundred_sixty_hertz),
1971                signal_ms: Some(0),
1972                present_start_ms: 9,
1973                responded_to_input: false,
1974            },
1975            Frame {
1976                refresh_interval: None,
1977                signal_ms: Some(0),
1978                present_start_ms: 2,
1979                responded_to_input: false,
1980            },
1981        ];
1982        for frame in frames {
1983            profiler.set_refresh_interval(frame.refresh_interval);
1984            profiler.record_draw_timing(FrameTiming {
1985                window_id: profiler.window_id,
1986                dirty_at: Some(at(0)),
1987                invalidations: 1,
1988                draw_start: at(0),
1989                draw_end: at(1),
1990                refresh_interval: frame.refresh_interval,
1991                signal_at: frame.signal_ms.map(at),
1992                phases: Default::default(),
1993            });
1994            if frame.responded_to_input {
1995                profiler.first_input_at = Some(at(0));
1996            }
1997            profiler.record_present_at(
1998                at(frame.present_start_ms),
1999                at(frame.present_start_ms + 1),
2000                true,
2001                false,
2002            );
2003        }
2004
2005        let snapshot = profiler.frame_duration_snapshot();
2006        let frames_for = |refresh_interval| {
2007            snapshot
2008                .by_refresh_interval
2009                .iter()
2010                .find(|frames| frames.refresh_interval == refresh_interval)
2011                .cloned()
2012                .expect("frames for the refresh interval")
2013        };
2014        let work_buckets = |work: &Histogram<u64>, refresh_interval: Duration| {
2015            histogram_bucket_counts(
2016                work,
2017                FRAME_WORK_BUCKETS_PERCENT
2018                    .map(|percent| (refresh_interval * percent as u32 / 100).as_nanos() as u64),
2019            )
2020        };
2021        let sixty = frames_for(Some(sixty_hertz));
2022        assert_eq!(sixty.frames_drawn, 4);
2023        assert_eq!(sixty.frames_on_time, 2);
2024        assert_eq!(sixty.frames_late, 1);
2025        // 40 ms of work at 16.7 ms per refresh.
2026        assert_eq!(sixty.missed_refreshes, 2);
2027        assert_eq!(sixty.frames_unmeasured, 1);
2028        assert_eq!(
2029            work_buckets(&sixty.work, sixty_hertz),
2030            [2, 0, 0, 0, 0, 0, 1, 0]
2031        );
2032        assert_eq!(sixty.input_frames_on_time, 1);
2033        assert_eq!(sixty.input_frames_late, 1);
2034        assert_eq!(
2035            work_buckets(&sixty.input_work, sixty_hertz),
2036            [1, 0, 0, 0, 0, 0, 1, 0]
2037        );
2038
2039        let one_hundred_twenty = frames_for(Some(one_hundred_twenty_hertz));
2040        assert_eq!(one_hundred_twenty.frames_late, 1);
2041        assert_eq!(one_hundred_twenty.missed_refreshes, 1);
2042        assert_eq!(one_hundred_twenty.input_frames_late, 0);
2043
2044        let three_hundred_sixty = frames_for(Some(three_hundred_sixty_hertz));
2045        assert_eq!(three_hundred_sixty.frames_on_time, 1);
2046        assert_eq!(three_hundred_sixty.frames_late, 1);
2047        // 6 ms of work misses 2 refreshes at 2.78 ms each, and 9 ms misses 3.
2048        assert_eq!(three_hundred_sixty.missed_refreshes, 5);
2049
2050        // Judged against the 60 Hz budget, without missed refreshes.
2051        let unknown = frames_for(None);
2052        assert_eq!(unknown.frames_drawn, 1);
2053        assert_eq!(unknown.frames_on_time, 1);
2054        assert_eq!(unknown.missed_refreshes, 0);
2055
2056        let later = profiler.frame_duration_snapshot();
2057        let delta = later.by_refresh_interval[0].since(&snapshot.by_refresh_interval[0]);
2058        assert_eq!(
2059            delta.frames_drawn + delta.frames_on_time + delta.frames_late,
2060            0
2061        );
2062        assert_eq!(
2063            delta.draw_duration.len() + delta.work.len() + delta.input_work.len(),
2064            0
2065        );
2066    }
2067
2068    #[test]
2069    fn records_draw_events_only_while_tracing() {
2070        let _trace_test_guard = TraceTestGuard::new();
2071        let window_id = WindowId::from(0xD0A0);
2072        let mut window_profiler =
2073            WindowProfiler::new(window_id).expect("window profiler should initialize");
2074        let dirty_at = Instant::now();
2075        let mut collector = FrameTimingCollector::new();
2076
2077        window_profiler.begin_draw();
2078        window_profiler.end_draw(Some(dirty_at), 3);
2079        assert!(
2080            collector
2081                .collect_unseen()
2082                .iter()
2083                .all(|event| !event_matches_window(*event, window_id))
2084        );
2085
2086        set_trace_enabled(true);
2087        let mut collector = FrameTimingCollector::new();
2088        window_profiler.begin_draw();
2089        window_profiler.end_draw(Some(dirty_at), 3);
2090
2091        let timing = collector
2092            .collect_unseen()
2093            .into_iter()
2094            .find_map(|event| match event {
2095                FrameEvent::Draw(timing) if timing.window_id == window_id => Some(timing),
2096                _ => None,
2097            })
2098            .expect("draw event should be recorded while tracing");
2099        assert_eq!(timing.dirty_at, Some(dirty_at));
2100        assert_eq!(timing.invalidations, 3);
2101        assert!(timing.draw_start >= dirty_at);
2102    }
2103
2104    #[test]
2105    fn records_present_events_for_newly_drawn_frames() {
2106        let _trace_test_guard = TraceTestGuard::new();
2107        set_trace_enabled(true);
2108        let window_id = WindowId::from(0xA11E);
2109        let mut window_profiler =
2110            WindowProfiler::new(window_id).expect("window profiler should initialize");
2111        let start = Instant::now();
2112        let mut collector = FrameTimingCollector::new();
2113
2114        record_test_draw(&mut window_profiler, start);
2115        window_profiler.record_present_at(start, start, true, true);
2116        record_test_draw(&mut window_profiler, start + FRAME);
2117        window_profiler.record_present_at(start + FRAME, start + FRAME, true, true);
2118        window_profiler.record_present_at(
2119            start + FRAME + FRAME / 2,
2120            start + FRAME + FRAME / 2,
2121            true,
2122            true,
2123        );
2124
2125        let present_timings = collector
2126            .collect_unseen()
2127            .into_iter()
2128            .filter_map(|event| match event {
2129                FrameEvent::Present(timing) if timing.window_id == window_id => Some(timing),
2130                _ => None,
2131            })
2132            .collect::<Vec<_>>();
2133        let [first_present, second_present] = present_timings.as_slice() else {
2134            panic!("expected exactly two present events, got {present_timings:?}");
2135        };
2136        assert_eq!(first_present.animation_interval, None);
2137        assert_eq!(second_present.animation_interval, Some(FRAME));
2138
2139        #[cfg(feature = "profiler")]
2140        {
2141            assert_eq!(window_profiler.present_interval_histogram.len(), 1);
2142            assert!(
2143                window_profiler.present_interval_histogram.max()
2144                    >= second_present
2145                        .animation_interval
2146                        .expect("second present should have an animation interval")
2147                        .as_nanos() as u64
2148            );
2149        }
2150    }
2151
2152    #[test]
2153    fn disabling_tracing_clears_frame_events() {
2154        let _trace_test_guard = TraceTestGuard::new();
2155        set_trace_enabled(true);
2156        let window_id = WindowId::from(0xC1EA);
2157        let mut window_profiler =
2158            WindowProfiler::new(window_id).expect("window profiler should initialize");
2159        let mut collector = FrameTimingCollector::new();
2160
2161        window_profiler.begin_draw();
2162        window_profiler.end_draw(None, 0);
2163        assert!(
2164            FRAME_TIMINGS
2165                .lock()
2166                .timings
2167                .iter()
2168                .copied()
2169                .any(|event| event_matches_window(event, window_id))
2170        );
2171
2172        set_trace_enabled(false);
2173        assert!(
2174            collector
2175                .collect_unseen()
2176                .iter()
2177                .all(|event| !event_matches_window(*event, window_id))
2178        );
2179    }
2180
2181    #[cfg(feature = "profiler")]
2182    #[test]
2183    fn records_intervals_only_between_animation_frames() {
2184        let mut window_profiler =
2185            WindowProfiler::new(WindowId::from(1)).expect("window profiler should initialize");
2186        let start = Instant::now();
2187
2188        draw_and_present(&mut window_profiler, start, true, true);
2189        assert_eq!(window_profiler.present_interval_histogram.len(), 0);
2190
2191        draw_and_present(&mut window_profiler, start + FRAME, true, true);
2192        assert_eq!(window_profiler.present_interval_histogram.len(), 1);
2193
2194        draw_and_present(&mut window_profiler, start + FRAME * 2, true, false);
2195        assert_eq!(window_profiler.present_interval_histogram.len(), 2);
2196
2197        draw_and_present(&mut window_profiler, start + FRAME * 100, true, true);
2198        assert_eq!(window_profiler.present_interval_histogram.len(), 2);
2199    }
2200
2201    #[cfg(feature = "profiler")]
2202    #[test]
2203    fn missed_frames_stretch_the_recorded_interval() {
2204        let mut window_profiler =
2205            WindowProfiler::new(WindowId::from(2)).expect("window profiler should initialize");
2206        let start = Instant::now();
2207
2208        draw_and_present(&mut window_profiler, start, true, true);
2209        draw_and_present(&mut window_profiler, start + FRAME * 5, true, true);
2210
2211        let recorded = window_profiler.present_interval_histogram.max();
2212        assert!(recorded >= (FRAME * 4).as_nanos() as u64);
2213    }
2214
2215    #[cfg(feature = "profiler")]
2216    #[test]
2217    fn ignores_re_presents_of_unchanged_frames() {
2218        let mut window_profiler =
2219            WindowProfiler::new(WindowId::from(3)).expect("window profiler should initialize");
2220        let start = Instant::now();
2221
2222        draw_and_present(&mut window_profiler, start, true, true);
2223        window_profiler.record_present_at(start + FRAME / 2, start + FRAME / 2, true, true);
2224        draw_and_present(&mut window_profiler, start + FRAME, true, true);
2225
2226        assert_eq!(window_profiler.present_interval_histogram.len(), 1);
2227        assert!(
2228            window_profiler.present_interval_histogram.max() >= (FRAME * 3 / 4).as_nanos() as u64
2229        );
2230    }
2231
2232    #[cfg(feature = "profiler")]
2233    #[test]
2234    fn skips_intervals_for_inactive_windows() {
2235        let mut window_profiler =
2236            WindowProfiler::new(WindowId::from(4)).expect("window profiler should initialize");
2237        let start = Instant::now();
2238
2239        draw_and_present(&mut window_profiler, start, false, true);
2240        draw_and_present(&mut window_profiler, start + FRAME, false, true);
2241        assert_eq!(window_profiler.present_interval_histogram.len(), 0);
2242
2243        draw_and_present(&mut window_profiler, start + FRAME * 2, true, true);
2244        assert_eq!(window_profiler.present_interval_histogram.len(), 0);
2245
2246        draw_and_present(&mut window_profiler, start + FRAME * 3, true, true);
2247        assert_eq!(window_profiler.present_interval_histogram.len(), 1);
2248    }
2249
2250    #[test]
2251    fn records_dirty_to_present_durations() {
2252        let mut window_profiler =
2253            WindowProfiler::new(WindowId::from(8)).expect("window profiler should initialize");
2254        let draw_end = Instant::now();
2255        let present_end = draw_end + Duration::from_millis(6);
2256
2257        record_test_draw(&mut window_profiler, draw_end);
2258        window_profiler.record_present_at(present_end, present_end, true, false);
2259
2260        let snapshot = window_profiler.frame_duration_snapshot();
2261        let histogram = snapshot.dirty_to_present_histogram;
2262        assert_eq!(histogram.len(), 1);
2263        assert!(histogram.max() >= Duration::from_millis(10).as_nanos() as u64);
2264    }
2265
2266    #[cfg(feature = "profiler")]
2267    #[test]
2268    fn records_every_draw_duration() {
2269        let mut window_profiler =
2270            WindowProfiler::new(WindowId::from(5)).expect("window profiler should initialize");
2271
2272        window_profiler.record_draw_duration(Duration::from_millis(2));
2273        window_profiler.record_draw_duration(Duration::from_millis(40));
2274
2275        let snapshot = window_profiler.frame_duration_snapshot();
2276        assert_eq!(snapshot.draw_duration_histogram.len(), 2);
2277        assert!(snapshot.draw_duration_histogram.max() >= 39_000_000);
2278    }
2279
2280    #[test]
2281    fn records_input_latency_at_the_frame_presentation_timestamp() {
2282        let mut window_profiler =
2283            WindowProfiler::new(WindowId::from(6)).expect("window profiler should initialize");
2284        let first_input_at = Instant::now();
2285        let presented_at = first_input_at + Duration::from_millis(12);
2286
2287        begin_input_at(&mut window_profiler, first_input_at);
2288        window_profiler.end_input(true);
2289        begin_input_at(
2290            &mut window_profiler,
2291            first_input_at + Duration::from_millis(2),
2292        );
2293        window_profiler.end_input(true);
2294        record_test_draw(&mut window_profiler, presented_at);
2295        window_profiler.record_present_at(presented_at, presented_at, true, false);
2296
2297        let snapshot = window_profiler.input_latency_snapshot();
2298        assert_eq!(snapshot.latency_histogram.len(), 1);
2299        assert!(snapshot.latency_histogram.max() >= Duration::from_millis(12).as_nanos() as u64);
2300        assert_eq!(snapshot.events_per_frame_histogram.len(), 1);
2301        assert_eq!(snapshot.events_per_frame_histogram.max(), 2);
2302        assert_eq!(snapshot.mid_draw_events_dropped, 0);
2303    }
2304
2305    #[test]
2306    fn excludes_input_that_arrives_during_a_draw() {
2307        let mut window_profiler =
2308            WindowProfiler::new(WindowId::from(7)).expect("window profiler should initialize");
2309
2310        window_profiler.begin_draw();
2311        begin_input_at(&mut window_profiler, Instant::now());
2312        window_profiler.end_input(true);
2313        window_profiler.end_draw(None, 0);
2314
2315        let snapshot = window_profiler.input_latency_snapshot();
2316        assert!(snapshot.latency_histogram.is_empty());
2317        assert!(snapshot.events_per_frame_histogram.is_empty());
2318        assert_eq!(snapshot.mid_draw_events_dropped, 1);
2319    }
2320
2321    #[test]
2322    fn overlapping_trace_scopes_keep_tracing_enabled() {
2323        let _trace_test_guard = TraceTestGuard::new();
2324        let first_scope = trace_scope();
2325        let second_scope = trace_scope();
2326
2327        assert!(trace_enabled());
2328        drop(first_scope);
2329        assert!(trace_enabled());
2330        drop(second_scope);
2331        assert!(!trace_enabled());
2332    }
2333
2334    const FRAME: Duration = Duration::from_millis(16);
2335    static TRACE_TEST_LOCK: Mutex<()> = Mutex::new(());
2336
2337    struct TraceTestGuard {
2338        was_enabled: bool,
2339        _lock: MutexGuard<'static, ()>,
2340    }
2341
2342    impl TraceTestGuard {
2343        fn new() -> Self {
2344            let lock = TRACE_TEST_LOCK
2345                .lock()
2346                .unwrap_or_else(|poisoned| poisoned.into_inner());
2347            let was_enabled = trace_enabled();
2348            set_trace_enabled(false);
2349            Self {
2350                was_enabled,
2351                _lock: lock,
2352            }
2353        }
2354    }
2355
2356    impl Drop for TraceTestGuard {
2357        fn drop(&mut self) {
2358            set_trace_enabled(false);
2359            if self.was_enabled {
2360                set_trace_enabled(true);
2361            }
2362        }
2363    }
2364
2365    fn event_matches_window(event: FrameEvent, window_id: WindowId) -> bool {
2366        match event {
2367            FrameEvent::Draw(timing) => timing.window_id == window_id,
2368            FrameEvent::Present(timing) => timing.window_id == window_id,
2369        }
2370    }
2371
2372    fn begin_input_at(window_profiler: &mut WindowProfiler, started_at: Instant) {
2373        window_profiler
2374            .active_activities
2375            .push(WindowActivity::Input {
2376                started_at,
2377                kind: "test",
2378            });
2379    }
2380
2381    #[cfg(feature = "profiler")]
2382    fn draw_and_present(
2383        window_profiler: &mut WindowProfiler,
2384        presented_at: Instant,
2385        window_active: bool,
2386        next_frame_scheduled: bool,
2387    ) {
2388        record_test_draw(window_profiler, presented_at);
2389        window_profiler.record_present_at(
2390            presented_at,
2391            presented_at,
2392            window_active,
2393            next_frame_scheduled,
2394        );
2395    }
2396
2397    fn record_test_draw(window_profiler: &mut WindowProfiler, draw_end: Instant) {
2398        window_profiler.record_draw_timing(FrameTiming {
2399            window_id: window_profiler.window_id,
2400            dirty_at: Some(draw_end - Duration::from_millis(4)),
2401            invalidations: 1,
2402            draw_start: draw_end - Duration::from_millis(2),
2403            draw_end,
2404            refresh_interval: None,
2405            signal_at: None,
2406            phases: Default::default(),
2407        });
2408    }
2409}