1#[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#[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#[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#[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#[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#[cfg(not(feature = "profiler"))]
78#[doc(hidden)]
79pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec<gpui::ThreadTaskTimings> {
80 Vec::new()
81}
82#[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#[cfg(not(feature = "profiler"))]
96#[doc(hidden)]
97pub fn take_all_stats(_included: TasksIncluded) -> Vec<gpui::ThreadTaskStatistics> {
98 Vec::new()
99}
100
101#[doc(hidden)]
103#[derive(Debug, Copy, Clone)]
104pub struct YieldTime(pub Instant);
105
106#[doc(hidden)]
108#[derive(Copy, Clone)]
109pub struct TaskTiming {
110 pub location: &'static core::panic::Location<'static>,
112 pub spawned: SpawnTime,
114 pub start: Instant,
116 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#[doc(hidden)]
133#[derive(Debug, Copy, Clone)]
134pub struct ActiveTiming {
135 pub location: &'static core::panic::Location<'static>,
137 pub spawned: SpawnTime,
139 pub start: Instant,
141}
142
143impl TaskTiming {
144 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 #[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#[doc(hidden)]
169#[derive(Debug, Clone)]
170pub struct ThreadTaskTimings {
171 pub thread_name: Option<String>,
173 pub thread_id: ThreadId,
175 pub timings: Vec<TaskTiming>,
177 pub stats: TaskStatistics,
179 pub total_pushed: u64,
182}
183
184impl ThreadTaskTimings {
185 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); 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#[doc(hidden)]
229#[derive(Debug)]
230pub struct ThreadTaskStatistics {
231 pub thread_name: Option<String>,
233 pub thread_id: ThreadId,
235 pub stats: TaskStatistics,
237}
238
239impl ThreadTaskStatistics {
240 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#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct SerializedLocation {
286 pub file: SharedString,
288 pub line: u32,
290 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#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct SerializedTaskTiming {
307 pub location: SerializedLocation,
309 pub start: u128,
311 pub duration: u128,
313}
314
315impl SerializedTaskTiming {
316 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct SerializedThreadTaskTimings {
356 pub thread_name: Option<String>,
358 pub thread_id: u64,
360 pub timings: Vec<SerializedTaskTiming>,
362}
363
364impl SerializedThreadTaskTimings {
365 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#[doc(hidden)]
387#[derive(Debug, Clone)]
388pub struct ThreadTimingsDelta {
389 pub thread_id: u64,
391 pub thread_name: Option<String>,
393 pub new_timings: Vec<SerializedTaskTiming>,
396}
397
398#[doc(hidden)]
400pub struct ProfilingCollector {
401 startup_time: Instant,
402 cursors: HashMap<ThreadId, u64>,
403}
404
405impl ProfilingCollector {
406 pub fn new(startup_time: Instant) -> Self {
408 Self {
409 startup_time,
410 cursors: HashMap::default(),
411 }
412 }
413
414 pub fn startup_time(&self) -> Instant {
416 self.startup_time
417 }
418
419 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 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 pub fn reset(&mut self) {
467 self.cursors.clear();
468 }
469}
470
471#[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#[doc(hidden)]
482pub type GuardedTaskTimings = spin::Mutex<ThreadTimings>;
483
484#[doc(hidden)]
486pub struct GlobalThreadTimings {
487 pub thread_id: ThreadId,
489 pub timings: std::sync::Weak<GuardedTaskTimings>,
491}
492
493#[doc(hidden)]
495#[derive(Debug, Clone)]
496pub struct TaskStatistics {
497 pub poll_time_to_beat: Duration,
499 pub runtime_to_beat: Duration,
501 pub longest_poll_times: [TaskTiming; 5],
503 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 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(); 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(); 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#[doc(hidden)]
592pub static GLOBAL_THREAD_TIMINGS: spin::Mutex<Vec<GlobalThreadTimings>> =
593 spin::Mutex::new(Vec::new());
594
595fn 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 #[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#[doc(hidden)]
637pub struct ThreadTimings {
638 pub thread_name: Option<String>,
640 pub thread_id: ThreadId,
642 pub timings: TaskTimings,
644 pub running: Option<ActiveTiming>,
646 pub stats: TaskStatistics,
648 pub total_pushed: u64,
651}
652
653impl ThreadTimings {
654 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 #[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 #[cfg(not(feature = "profiler"))]
682 pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {}
683
684 #[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(); 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 #[cfg(not(feature = "profiler"))]
721 pub fn save_task_timing(&mut self, _: YieldTime) {}
722
723 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#[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#[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#[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
802pub 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#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))]
853pub(crate) struct TraceGuard;
854
855#[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
880pub 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#[cfg(feature = "profiler")]
903#[derive(Debug, Copy, Clone)]
904pub struct FrameTiming {
905 pub window_id: WindowId,
907 pub dirty_at: Option<Instant>,
910 pub invalidations: u64,
912 pub draw_start: Instant,
914 pub draw_end: Instant,
916 pub refresh_interval: Option<Duration>,
919 pub signal_at: Option<Instant>,
922 pub phases: DrawPhases,
924}
925
926#[cfg(feature = "profiler")]
928#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
929pub struct DrawPhases {
930 pub request_layout: Duration,
932 pub prepaint: Duration,
935 pub paint: Duration,
937}
938
939#[cfg(feature = "profiler")]
941#[derive(Debug, Copy, Clone, PartialEq, Eq)]
942pub enum ViewPhase {
943 RequestLayout,
945 Prepaint,
948 Paint,
950}
951
952#[cfg(feature = "profiler")]
953impl ViewPhase {
954 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#[cfg(feature = "profiler")]
968pub const VIEW_TIMING_FLOOR: Duration = Duration::from_micros(500);
969
970#[cfg(feature = "profiler")]
973#[derive(Debug, Copy, Clone)]
974pub struct ViewTiming {
975 pub name: &'static str,
977 pub phase: ViewPhase,
979 pub start: Instant,
981 pub end: Instant,
983 pub self_duration: Duration,
986}
987
988#[cfg(feature = "profiler")]
993pub const FRAME_BUDGET_FLOOR: Duration = Duration::from_nanos(8_333_333);
994
995#[cfg(feature = "profiler")]
999pub const UNKNOWN_REFRESH_FRAME_BUDGET: Duration = Duration::from_nanos(16_666_667);
1000
1001#[cfg(feature = "profiler")]
1002impl FrameTiming {
1003 pub fn work_start(&self) -> Option<Instant> {
1007 let signal_at = self.signal_at?;
1008 Some(
1012 self.dirty_at
1013 .map_or(signal_at, |dirty_at| dirty_at.max(signal_at)),
1014 )
1015 }
1016
1017 pub fn work(&self, submitted_at: Instant) -> Option<Duration> {
1021 Some(submitted_at.saturating_duration_since(self.work_start()?))
1022 }
1023
1024 pub fn is_late(&self, submitted_at: Instant) -> Option<bool> {
1027 Some(self.work(submitted_at)? > self.budget())
1028 }
1029
1030 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 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 pub fn draw_duration(&self) -> Duration {
1051 self.draw_end.duration_since(self.draw_start)
1052 }
1053
1054 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#[cfg(feature = "profiler")]
1064#[derive(Debug, Copy, Clone)]
1065pub struct PresentTiming {
1066 pub window_id: WindowId,
1068 pub present_start: Instant,
1070 pub present_end: Instant,
1072 pub animation_interval: Option<Duration>,
1075}
1076
1077#[cfg(feature = "profiler")]
1078impl PresentTiming {
1079 pub fn present_duration(&self) -> Duration {
1081 self.present_end.duration_since(self.present_start)
1082 }
1083}
1084
1085#[cfg(feature = "profiler")]
1087#[derive(Debug, Copy, Clone)]
1088pub enum FrameEvent {
1089 Draw(FrameTiming),
1091 Present(PresentTiming),
1093}
1094
1095#[cfg(feature = "profiler")]
1099#[derive(Clone)]
1100pub struct FrameDurationSnapshot {
1101 pub dirty_to_present_histogram: Histogram<u64>,
1103 pub draw_duration_histogram: Histogram<u64>,
1105 pub present_interval_histogram: Histogram<u64>,
1108 pub by_refresh_interval: Vec<RefreshIntervalFrames>,
1111}
1112
1113#[cfg(feature = "profiler")]
1117pub const FRAME_DURATION_BUCKETS_MS: [u64; 9] = [4, 8, 16, 33, 50, 100, 250, 500, 1000];
1118
1119#[cfg(feature = "profiler")]
1126pub const FRAME_WORK_BUCKETS_PERCENT: [u64; 7] = [25, 50, 75, 100, 150, 200, 400];
1127
1128#[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 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#[cfg(feature = "profiler")]
1158#[derive(Clone, Debug)]
1159pub struct RefreshIntervalFrames {
1160 pub refresh_interval: Option<Duration>,
1163 pub frames_drawn: u64,
1165 pub draw_duration: Histogram<u64>,
1167 pub frames_on_time: u64,
1169 pub frames_late: u64,
1171 pub missed_refreshes: u64,
1174 pub frames_unmeasured: u64,
1176 pub work: Histogram<u64>,
1178 pub input_frames_on_time: u64,
1180 pub input_frames_late: u64,
1182 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 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#[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#[cfg(feature = "profiler")]
1281#[derive(Clone)]
1282pub struct InputLatencySnapshot {
1283 pub latency_histogram: Histogram<u64>,
1285 pub events_per_frame_histogram: Histogram<u64>,
1287 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#[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 empty_histogram: Histogram<u64>,
1331 draw_phases: DrawPhases,
1332 nested_view_time: SmallVec<[Duration; 16]>,
1335}
1336
1337#[cfg(feature = "profiler")]
1338impl WindowProfiler {
1339 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 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 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 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 pub fn end_action_handler(&mut self) {
1448 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 pub fn record_frame_signal(&mut self, signal_at: Option<Instant>) {
1470 self.frame_signal_at = signal_at;
1471 }
1472
1473 pub fn set_refresh_interval(&mut self, refresh_interval: Option<Duration>) {
1476 self.refresh_interval = refresh_interval;
1477 }
1478
1479 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 pub fn record_draw_phases(&mut self, phases: DrawPhases) {
1490 self.draw_phases = phases;
1491 }
1492
1493 pub fn begin_view(&mut self) -> Instant {
1496 self.nested_view_time.push(Duration::ZERO);
1497 Instant::now()
1498 }
1499
1500 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 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 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 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 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 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#[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#[cfg(feature = "profiler")]
1734pub fn record_frame_event(event: FrameEvent) {
1735 if !trace_enabled() {
1736 return;
1737 }
1738 cold_path(); 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#[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 pub fn new() -> Self {
1768 Self {
1769 cursor: FRAME_TIMINGS.lock().total_pushed,
1770 }
1771 }
1772
1773 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 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 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 assert_eq!(work(Some(5), Some(0)), Some(Duration::from_millis(45)));
1903 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 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 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 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 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 assert_eq!(three_hundred_sixty.missed_refreshes, 5);
2049
2050 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}