1use serde::Serialize;
8use std::cell::{Cell, RefCell};
9use std::time::{Duration, Instant};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
18pub enum ProfilerMode {
19 #[default]
21 Normal,
22 Diagnostic,
24 Profiling,
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
33pub struct FrameTimings {
34 pub event_processing: u32,
36 pub runtime_messages: u32,
38 pub build: u32,
40 pub layout: u32,
42 pub composite: u32,
44 pub semantics: u32,
46 pub paint: u32,
48 pub cpu_total: u32,
50}
51impl FrameTimings {
52 #[must_use]
53 pub fn total(&self) -> Duration {
54 Duration::from_micros(u64::from(self.cpu_total))
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
60pub struct FrameWork {
61 pub updated_elements: u64,
62 pub rebuilt_elements: u64,
63 pub laid_out_render_objects: u64,
64 pub repainted_render_objects: u64,
65 pub composited_layers: u64,
66 pub active_animations: u64,
67 pub display_list_commands: usize,
68 pub requested_another_frame: bool,
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
73pub struct FrameRecord {
74 pub frame: u64,
75 pub timings: FrameTimings,
76 pub work: FrameWork,
77 pub over_budget: bool,
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
83pub struct FrameStatistics {
84 pub samples: u64,
85 pub latest_us: f64,
86 pub mean_us: f64,
87 pub min_us: f64,
88 pub max_us: f64,
89 pub p50_us: f64,
90 pub p95_us: f64,
91 pub p99_us: f64,
92}
93
94#[derive(Debug)]
97pub struct FrameHistory {
98 records: std::collections::VecDeque<FrameRecord>,
99 capacity: usize,
100}
101impl FrameHistory {
102 #[must_use]
103 pub fn new(capacity: usize) -> Self {
104 Self {
105 records: std::collections::VecDeque::with_capacity(capacity.clamp(300, 1000)),
106 capacity: capacity.clamp(300, 1000),
107 }
108 }
109 pub fn push(&mut self, record: FrameRecord) {
110 if self.records.len() == self.capacity {
111 self.records.pop_front();
112 }
113 self.records.push_back(record);
114 }
115 #[must_use]
117 pub const fn len(&self) -> usize {
118 self.capacity
119 }
120 #[must_use]
121 pub const fn is_empty(&self) -> bool {
122 false
123 }
124 #[must_use]
125 pub fn recorded(&self) -> usize {
126 self.records.len()
127 }
128 #[must_use]
129 pub fn latest(&self) -> Option<FrameRecord> {
130 self.records.back().copied()
131 }
132 pub fn iter(&self) -> impl Iterator<Item = FrameRecord> + '_ {
133 self.records.iter().copied()
134 }
135 #[must_use]
138 pub fn statistics(&self) -> FrameStatistics {
139 let mut samples: Vec<u32> = self.records.iter().map(|f| f.timings.cpu_total).collect();
140 let count = samples.len() as u64;
141 if count == 0 {
142 return FrameStatistics::default();
143 }
144 samples.sort_unstable();
145 let pick = |percentile: f64| -> f64 {
146 let index = ((count as f64 - 1.) * percentile).round() as usize;
147 f64::from(samples[index.min(samples.len() - 1)])
148 };
149 let sum: u64 = samples.iter().map(|s| u64::from(*s)).sum();
150 FrameStatistics {
151 samples: count,
152 latest_us: f64::from(*samples.last().expect("non-empty")),
153 mean_us: sum as f64 / count as f64,
154 min_us: f64::from(samples[0]),
155 max_us: f64::from(samples[samples.len() - 1]),
156 p50_us: pick(0.50),
157 p95_us: pick(0.95),
158 p99_us: pick(0.99),
159 }
160 }
161}
162
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
165pub struct SchedulerCounters {
166 pub signal_reads: u64,
168 pub signal_writes: u64,
170 pub dependents_enqueued: u64,
172 pub runtime_wakes: u64,
173 pub redraw_requests: u64,
174 pub frames_started: u64,
175 pub frames_presented: u64,
176 pub frames_skipped: u64,
177}
178
179#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
180pub struct BudgetStatistics {
181 pub refresh_rate_hz: Option<f32>,
183 pub budget_us: Option<f64>,
185 pub frames_over_budget: u64,
186 pub longest_frame_us: f64,
187}
188
189#[derive(Debug)]
192pub struct PerformanceProfiler {
193 mode: ProfilerMode,
194 history: FrameHistory,
195 budget: BudgetStatistics,
196 next_frame: u64,
197 present_times: std::collections::VecDeque<Instant>,
199}
200impl PerformanceProfiler {
201 const FPS_WINDOW: usize = 120;
202
203 #[must_use]
206 pub fn frames_per_second(&self) -> Option<f32> {
207 let first = *self.present_times.front()?;
208 let last = *self.present_times.back()?;
209 let elapsed = last.duration_since(first).as_secs_f32();
210 (elapsed > 0.).then(|| (self.present_times.len().saturating_sub(1)) as f32 / elapsed)
211 }
212 pub(crate) fn note_present(&mut self, presented: bool) {
213 if !presented {
214 return;
215 }
216 if self.present_times.len() == Self::FPS_WINDOW {
217 self.present_times.pop_front();
218 }
219 self.present_times.push_back(Instant::now());
220 }
221}
222impl PerformanceProfiler {
223 #[must_use]
224 pub fn new(mode: ProfilerMode) -> Self {
225 Self {
226 mode,
227 history: FrameHistory::new(512),
228 budget: BudgetStatistics::default(),
229 next_frame: 1,
230 present_times: std::collections::VecDeque::with_capacity(Self::FPS_WINDOW),
231 }
232 }
233 #[must_use]
234 pub const fn mode(&self) -> ProfilerMode {
235 self.mode
236 }
237 pub fn set_mode(&mut self, mode: ProfilerMode) {
238 self.mode = mode;
239 }
240 #[must_use]
241 pub const fn history(&self) -> &FrameHistory {
242 &self.history
243 }
244 #[must_use]
245 pub const fn budget(&self) -> &BudgetStatistics {
246 &self.budget
247 }
248 pub fn set_refresh_rate_hz(&mut self, hz: Option<f32>) {
250 self.budget.refresh_rate_hz = hz.filter(|rate| *rate > 0.);
251 self.budget.budget_us = self
252 .budget
253 .refresh_rate_hz
254 .map(|rate| 1_000_000. / f64::from(rate));
255 }
256 #[must_use]
257 pub fn next_frame_id(&mut self) -> u64 {
258 let id = self.next_frame;
259 self.next_frame += 1;
260 id
261 }
262 pub fn record(&mut self, mut record: FrameRecord) {
265 if let Some(budget_us) = self.budget.budget_us {
266 record.over_budget = f64::from(record.timings.cpu_total) > budget_us;
267 if record.over_budget {
268 self.budget.frames_over_budget += 1;
269 }
270 }
271 let total = f64::from(record.timings.cpu_total);
272 if total > self.budget.longest_frame_us {
273 self.budget.longest_frame_us = total;
274 }
275 if self.mode != ProfilerMode::Normal {
276 self.history.push(record);
277 }
278 }
279}
280
281#[derive(Debug)]
283pub struct PhaseSpan {
284 started: Instant,
285}
286impl PhaseSpan {
287 #[must_use]
288 pub fn start() -> Self {
289 Self {
290 started: Instant::now(),
291 }
292 }
293 #[must_use]
294 pub fn elapsed_us(&self) -> u32 {
295 self.started
296 .elapsed()
297 .as_micros()
298 .try_into()
299 .unwrap_or(u32::MAX)
300 }
301}
302
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
307pub struct RenderFrameMetrics {
308 pub frame: u64,
309 pub draw_calls: u32,
310 pub instances: u32,
311 pub render_passes: u32,
312 pub path_triangles: u32,
314 pub upload_bytes: u64,
316 pub texture_upload_bytes: u64,
318 pub pipelines_created: u32,
321 pub queue_submissions: u32,
323 pub prepare_us: u32,
324 pub encode_us: u32,
325 pub submit_us: u32,
326}
327
328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
337pub struct GpuResourceSummary {
338 pub allocator_live_bytes: Option<u64>,
340 pub allocator_reserved_bytes: Option<u64>,
342 pub window_stencil_creations: u64,
345 pub window_stencil_recreations: u64,
347 pub shared_image_entries: u64,
349 pub shared_image_evictions: u64,
351 pub shared_gradient_entries: u64,
353 pub shared_gradient_evictions: u64,
355 pub glyph_live_pages: u64,
357 pub glyph_page_evictions: u64,
359 pub glyphs_rasterized: u64,
361 pub local_image_entries: u64,
363 pub local_gradient_entries: u64,
365 pub local_glyph_pages: u64,
367}
368
369#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
371pub struct GpuSample {
372 pub supported: bool,
373 pub frame: u64,
374 pub main_pass_us: f64,
375}
376
377#[derive(Clone, Debug, Default, PartialEq, Serialize)]
379pub struct WindowPerformance {
380 pub requested_frames: u64,
381 pub presented_frames: u64,
382 pub skipped_frames: u64,
383 pub latest: Option<FrameRecord>,
385 pub render: RenderFrameMetrics,
387 pub gpu: Option<GpuSample>,
389}
390
391#[derive(Clone, Debug, Default, PartialEq, Serialize)]
394pub struct PerformanceSnapshot {
395 pub scheduler: SchedulerCounters,
396 pub budget: BudgetStatistics,
397 pub fps: Option<f32>,
399 pub windows: Vec<WindowPerformance>,
400 pub frame_statistics: FrameStatistics,
402 pub widgets: WidgetWorkSnapshot,
404 pub text: TextCacheSnapshot,
406 pub accessibility: AccessibilitySnapshot,
408}
409
410#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
412pub struct WidgetWorkSnapshot {
413 pub mounts: u64,
414 pub unmounts: u64,
415 pub rebuilds: u64,
416 pub layouts: u64,
417 pub paints: u64,
418 pub composites: u64,
419 pub animation_ticks: u64,
420 pub scroll_offset_updates: u64,
421 pub reconciliation_fast_paths: u64,
422 pub layout_cache_hits: u64,
423 pub display_lists_reused: u64,
424 pub compositor_only_updates: u64,
425 pub lazy_layouts: u64,
426 pub items_built: u64,
427 pub items_reused: u64,
428 pub child_list_scans: u64,
430 pub identical_child_bailouts: u64,
431 pub elements_created: u64,
432 pub elements_removed: u64,
433 pub elements_moved: u64,
434 pub dirty_requests: u64,
435 pub dirty_queue_deduplicated: u64,
436 pub elements_total: usize,
437 pub render_objects_total: usize,
438 pub layers_total: usize,
439}
440
441#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
442pub struct TextCacheSnapshot {
443 pub layouts_requested: u64,
444 pub cache_hits: u64,
445 pub cache_misses: u64,
446 pub paragraphs_reshaped: u64,
448 pub parley_layouts_reused: u64,
450 pub documents_composed: u64,
452}
453
454#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
455pub struct AccessibilitySnapshot {
456 pub nodes_published: u64,
457 pub updates_skipped_unchanged: u64,
458}
459
460#[derive(Clone)]
469pub struct PerformanceHub {
470 inner: std::rc::Rc<HubInner>,
471}
472struct HubInner {
473 snapshot: RefCell<PerformanceSnapshot>,
474 version: super::Signal<u64>,
477 observed: Cell<bool>,
480 last_publish: Cell<Option<Instant>>,
481}
482impl Default for PerformanceHub {
483 fn default() -> Self {
484 Self {
485 inner: std::rc::Rc::new(HubInner {
486 snapshot: RefCell::new(PerformanceSnapshot::default()),
487 version: super::Signal::new(0),
488 observed: Cell::new(false),
489 last_publish: Cell::new(None),
490 }),
491 }
492 }
493}
494impl PerformanceHub {
495 #[must_use]
496 pub fn new() -> Self {
497 Self::default()
498 }
499 #[must_use]
502 pub fn version(&self) -> u64 {
503 self.inner.version.get()
504 }
505 #[must_use]
506 pub fn observed(&self) -> bool {
507 self.inner.observed.get()
508 }
509 pub fn set_observed(&self, observed: bool) {
510 self.inner.observed.set(observed);
511 }
512 #[must_use]
513 pub fn snapshot(&self) -> PerformanceSnapshot {
514 self.inner.snapshot.borrow().clone()
515 }
516 #[must_use]
518 pub fn publish_due(&self, min_interval: Duration) -> bool {
519 match self.inner.last_publish.get() {
520 Some(previous) => previous.elapsed() >= min_interval,
521 None => true,
522 }
523 }
524 pub fn publish(&self, snapshot: PerformanceSnapshot) {
525 *self.inner.snapshot.borrow_mut() = snapshot;
526 let next = self.inner.version.get().wrapping_add(1);
527 self.inner.version.set(next);
528 self.inner.last_publish.set(Some(Instant::now()));
529 }
530}
531
532impl PerformanceSnapshot {
533 #[must_use]
535 pub fn overlay_lines(&self) -> Vec<String> {
536 let mut lines = Vec::with_capacity(16);
537 lines.push(format!(
538 "FPS {}",
539 self.fps
540 .map(|fps| format!("{fps:.0}"))
541 .unwrap_or_else(|| "idle".into())
542 ));
543 let latest = self.windows.iter().find_map(|window| window.latest);
544 if let Some(record) = latest {
545 let timings = &record.timings;
546 let ms = |micros: u32| f64::from(micros) / 1000.;
547 lines.push(format!("CPU {:>6.2} ms", ms(timings.cpu_total)));
548 match self
549 .windows
550 .iter()
551 .filter_map(|window| window.gpu)
552 .find(|gpu| gpu.supported && gpu.main_pass_us.is_finite())
553 {
554 Some(gpu) => lines.push(format!("GPU {:>6.2} ms", gpu.main_pass_us / 1000.)),
555 None => lines.push("GPU timing unavailable".into()),
556 }
557 lines.push(format!("Build {:>7.2}", ms(timings.build)));
558 lines.push(format!("Layout {:>7.2}", ms(timings.layout)));
559 lines.push(format!("Paint {:>7.2}", ms(timings.paint)));
560 lines.push(format!("Composite {:>7.2}", ms(timings.composite)));
561 let render = self
562 .windows
563 .iter()
564 .map(|window| window.render)
565 .max_by_key(|render| render.draw_calls)
566 .unwrap_or_default();
567 lines.push(format!(
568 "Render {:>7.2}",
569 (f64::from(render.prepare_us) + f64::from(render.encode_us + render.submit_us))
570 / 1000.
571 ));
572 lines.push(format!("Draws {:>7}", render.draw_calls));
573 lines.push(format!("Instances {:>7}", render.instances));
574 lines.push(format!("Passes {:>7}", render.render_passes));
575 if render.pipelines_created > 0 {
576 lines.push(format!(
577 "Pipelines {:>7} (unexpected after init)",
578 render.pipelines_created
579 ));
580 }
581 if let Some(budget) = self.budget.budget_us {
582 lines.push(format!(
583 "Budget {:>6.2} ms ({} over)",
584 budget / 1000.,
585 self.budget.frames_over_budget
586 ));
587 }
588 }
589 let widgets = &self.widgets;
590 lines.push(format!("Elements {:>7}", widgets.elements_total));
591 lines.push(format!(
592 "Built {:>7}",
593 latest.map_or(0, |record| record.work.updated_elements)
594 ));
595 lines.push(format!(
596 "Layout {:>7}",
597 latest.map_or(0, |record| record.work.laid_out_render_objects)
598 ));
599 lines.push(format!(
600 "Paint {:>7}",
601 latest.map_or(0, |record| record.work.repainted_render_objects)
602 ));
603 let requests = self.text.cache_hits + self.text.cache_misses;
604 if requests > 0 {
605 let hit_rate = self.text.cache_hits as f64 / requests as f64 * 100.;
606 lines.push(format!("Text hit {:>6.1}%", hit_rate));
607 }
608 if self.widgets.child_list_scans > 0 {
609 lines.push(format!(
610 "Scan/Bail {:>4}/{}",
611 self.widgets.child_list_scans, self.widgets.identical_child_bailouts
612 ));
613 }
614 if self.text.documents_composed > 0 {
615 lines.push(format!(
616 "Para resh {:>4} ({} docs)",
617 self.text.paragraphs_reshaped, self.text.documents_composed
618 ));
619 }
620 if self.accessibility.updates_skipped_unchanged > 0 {
621 lines.push(format!(
622 "A11y skips {:>7}",
623 self.accessibility.updates_skipped_unchanged
624 ));
625 }
626 lines
627 }
628}