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