//! Test-only headless performance harness for craft-ui hot paths. //! //! Run through the server task workflow, e.g. `cargo run -p craft-ui --example ui_hotpaths --locked`. //! Set `CRAFT_UI_PERF_OUT` to write the raw JSON report (otherwise stdout); `CRAFT_UI_PERF_LABEL` //! is copied into the report. Frames are `Context::run_ui` plus `Context::tessellate`; texture //! deltas are counted but never uploaded, and no GPU, window, compositor or native input path is //! exercised. Fonts are egui's bundled `default_fonts` unless `CRAFT_UI_PERF_FONT` points at a //! pinned fixture file (recorded by the parent with its hash). Bodies, geometry, interactions and //! the mesh fingerprint are collected on the final measured frame; the hover probe is a separate //! unmeasured frame and never contributes to timings or counters. use std::{ collections::{BTreeMap, BTreeSet}, env, error::Error, hint::black_box, sync::Arc, time::{Duration, Instant}, }; use craft_ui::{ docking::{DockArea, DockError, DockStyle, Floating, Layout, Node, Permissions}, layout::{SplitAxis, SplitSize, fit_toolbar, split_rect}, tabs, }; use egui::{Context, Id, Rect, pos2, vec2}; use serde_json::{Value, json}; const WARMUPS: usize = 50; const MIN_WARMUP: Duration = Duration::from_secs(3); const MICRO_SAMPLES: usize = 50; const MICRO_BATCH: usize = 16; const FRAME_SAMPLES: usize = 1000; const EPS: f32 = 0.1; #[derive(Default)] struct Frames { times_ns: Vec, warmups: usize, warmup_elapsed_ns: u64, next_frame: usize, mesh_hash: u64, bodies: BTreeMap, shapes: usize, vertices: usize, indices: usize, set_images: usize, set_bytes: usize, free_images: usize, repaint_frames: usize, passes: Vec, galleys_after_warmup: usize, } /// Run all micro and dock/integration cases at 1× and 2×, gate each case, and emit the schema-3 /// JSON report to `CRAFT_UI_PERF_OUT` (or stdout). Fails when no bundled or fixture font is /// available. fn main() -> Result<(), Box> { let (fonts, font_names) = font_definitions()?; if font_names.is_empty() { return Err("the example needs egui's bundled default_fonts or CRAFT_UI_PERF_FONT".into()); } let mut micro = Vec::new(); for scale in [1.0f32, 2.0] { for count in [1usize, 8, 64, 4096] { let natural: Vec<_> = tab_widths(count).into_iter().map(|width| width * scale).collect(); let selected = count / 2; for available in [96.0f32, 640.0, 1920.0] { let (available, minimum, overflow) = (available * scale, 48.0 * scale, 64.0 * scale); check_tabs(&natural, selected, available, minimum, overflow)?; micro.push(micro_entry( "tabs_fit", json!({"tabs": count, "available": available, "selected": selected, "minimum_width": minimum, "overflow_width": overflow, "size_scale": scale}), || { let natural = black_box(&natural); black_box(tabs::fit(natural, black_box(selected), black_box(available), black_box(minimum), black_box(overflow))); }, )); } } for (shape, rect, axis) in [ ("normal", Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0 * scale, 600.0 * scale)), SplitAxis::Horizontal), ("tiny", Rect::from_min_size(pos2(0.0, 0.0), vec2(120.0 * scale, 80.0 * scale)), SplitAxis::Vertical), ] { let (gap, minimum) = (4.0 * scale, 24.0 * scale); let span = match axis { SplitAxis::Horizontal => rect.width(), SplitAxis::Vertical => rect.height(), }; // Dock layout uses `SplitSize::FixedFirst` (`docking/surface.rs`), so both fixed modes are // measured next to the ratio case for every shape and scale. for size in [SplitSize::Ratio(0.31), SplitSize::FixedFirst(span * 0.3), SplitSize::FixedSecond(span * 0.3)] { let layout = check_split(rect, axis, size, gap, minimum)?; micro.push(micro_entry( "split_rect", json!({"shape": shape, "axis": format!("{axis:?}"), "size": format!("{size:?}"), "gap": gap, "minimum": minimum, "size_scale": scale}), || { black_box(split_rect(black_box(rect), black_box(axis), black_box(size), black_box(gap), black_box(minimum))); }, )); micro.push(micro_entry( "split_resized", json!({"shape": shape, "axis": format!("{axis:?}"), "size": format!("{size:?}"), "delta": 12.0 * scale, "minimum": minimum, "size_scale": scale}), || { black_box(black_box(layout).resized(black_box(size), black_box(12.0 * scale), black_box(minimum))); }, )); } } for groups in [8usize, 4096] { let natural: Vec<_> = toolbar_widths(groups).into_iter().map(|width| width * scale).collect(); let available = 1920.0 * scale; check_toolbar(&natural, available, 6.0 * scale, 40.0 * scale)?; micro.push(micro_entry( "fit_toolbar", json!({"groups": groups, "available": available, "spacing": 6.0 * scale, "overflow_width": 40.0 * scale, "size_scale": scale}), || { let natural = black_box(&natural); black_box(fit_toolbar(natural, black_box(available), black_box(6.0 * scale), black_box(40.0 * scale))); }, )); } } let mut dock = Vec::new(); for (tag, panels, floating) in [("dock4", 4u32, true), ("dock32", 32u32, false)] { for viewport in [[800.0f32, 600.0], [1440.0, 900.0]] { for scale in [1.0f32, 2.0] { dock.push(run_case(Case { label: tag, kind: "dock", panels, floating, side_panel: false, viewport, scale }, &fonts)?); } } } let mut integration = Vec::new(); for viewport in [[800.0f32, 600.0], [1440.0, 900.0]] { for scale in [1.0f32, 2.0] { integration.push(run_case( Case { label: "panel_integration", kind: "panel_integration", panels: 6, floating: false, side_panel: true, viewport, scale }, &fonts, )?); } } let case_counts = json!({"micro": micro.len(), "dock": dock.len(), "panel_integration": integration.len()}); let report = json!({ "schema": 3, "harness": "craft-ui/examples/ui_hotpaths", "label": env::var("CRAFT_UI_PERF_LABEL").unwrap_or_default(), "os": env::consts::OS, "arch": env::consts::ARCH, "package_version": env!("CARGO_PKG_VERSION"), "debug_assertions": cfg!(debug_assertions), "build_profile": if cfg!(debug_assertions) { "debug" } else { "release" }, "theme": "dark", "render_config": {"visuals": "dark", "animation_time": 0.0, "text_cursor_blink": false, "max_texture_side": 4096, "window_focused": true, "clock": "frame index / 60 Hz", "pixels_per_point": [1.0, 2.0]}, "notes": [ "CPU-only headless measurement; no GPU renderer, native window, compositor or input.", "Dock frame scope is Context::run_ui + Context::tessellate; texture deltas are counted, not uploaded.", "Fonts are egui default_fonts or the CRAFT_UI_PERF_FONT fixture; no installed system fonts.", "Micro samples are mean-of-batch nanoseconds; frame samples are individual frame nanoseconds.", "Every micro operation black-boxes its inputs and result so loop-invariant hoisting cannot erase the measured work.", "Individual micro latencies are diagnostic only: below about one microsecond the single-call timer overhead dominates.", "Bodies, geometry, interactions and the mesh fingerprint come from the final measured frame; the hover probe is a separate unmeasured frame.", ], "minimum_warmups": WARMUPS, "minimum_warmup_ns": MIN_WARMUP.as_nanos(), "micro_samples": MICRO_SAMPLES, "micro_batch": MICRO_BATCH, "frame_samples": FRAME_SAMPLES, "fonts": {"source": font_source(), "font_file": env::var_os("CRAFT_UI_PERF_FONT").map(|path| path.to_string_lossy().into_owned()), "font_names": font_names, "families": fonts.families.iter().map(|(family, names)| (format!("{family:?}"), names)).collect::>()}, "micro": micro, "dock": dock, "panel_integration": integration, "case_counts": case_counts, }); let text = serde_json::to_string_pretty(&report)?; if let Ok(path) = env::var("CRAFT_UI_PERF_OUT") { std::fs::write(&path, &text)?; eprintln!("ui_hotpaths: wrote {path} ({} bytes)", text.len()); } else { println!("{text}"); } Ok(()) } /// Synthetic tab widths from 40 to 119 pt, cycling with `(i * 17) % 80`. fn tab_widths(count: usize) -> Vec { (0..count).map(|i| 40.0 + ((i * 17) % 80) as f32).collect() } /// Synthetic toolbar widths from 24 to 71 pt, cycling with `(i * 13) % 48`. fn toolbar_widths(count: usize) -> Vec { (0..count).map(|i| 24.0 + ((i * 13) % 48) as f32).collect() } /// Time `op` after at least 50 warmup calls and 3 seconds, record 50 batch-mean samples (2000 /// operations per batch for split cases, 16 otherwise), and also record diagnostic single-call /// latencies. Returns the counts, samples and summaries without p95/p99 where noted. fn micro_entry(name: &str, params: Value, mut op: impl FnMut()) -> Value { let warmup_start = Instant::now(); let mut warmups = 0; while warmups < WARMUPS || warmup_start.elapsed() < MIN_WARMUP { op(); warmups += 1; } let warmup_elapsed_ns = warmup_start.elapsed().as_nanos() as u64; let mut samples = Vec::with_capacity(MICRO_SAMPLES); let batch = if name.starts_with("split_") { 2000 } else { MICRO_BATCH }; for _ in 0..MICRO_SAMPLES { let start = Instant::now(); for _ in 0..batch { op(); } samples.push((start.elapsed().as_nanos() as u64) / batch as u64); } let mut summary = summarize(&samples); if let Some(stats) = summary.as_object_mut() { stats.remove("p95_ns"); stats.remove("p99_ns"); } let mut individual = Vec::with_capacity(MICRO_SAMPLES); for _ in 0..MICRO_SAMPLES { let start = Instant::now(); op(); individual.push(start.elapsed().as_nanos() as u64); } let mut latency = summarize(&individual); if let Some(stats) = latency.as_object_mut() { stats.remove("p99_ns"); } let (sample_count, individual_count) = (samples.len(), individual.len()); json!({"name": name, "params": params, "sample_kind": "mean_of_batch_ns", "iterations_per_sample": batch, "operations": warmups + MICRO_SAMPLES * batch + MICRO_SAMPLES, "warmups": warmups, "warmup_elapsed_ns": warmup_elapsed_ns, "samples": sample_count, "samples_ns": samples, "stats": summary, "individual_samples": individual_count, "individual_latency_ns": individual, "individual_latency_stats": latency, "individual_latency_note": "diagnostic only; timer overhead dominates single sub-microsecond calls"}) } /// Sort a copy of `samples` and return count, median, p95, p99, min and max in nanoseconds. fn summarize(samples: &[u64]) -> Value { let mut sorted = samples.to_vec(); sorted.sort_unstable(); json!({"count": sorted.len(), "median_ns": percentile(&sorted, 50), "p95_ns": percentile(&sorted, 95), "p99_ns": percentile(&sorted, 99), "min_ns": sorted.first().copied().unwrap_or(0), "max_ns": sorted.last().copied().unwrap_or(0)}) } /// Nearest-rank percentile of an ascending slice. Empty input returns 0; the index is clamped to /// the last element. fn percentile(sorted: &[u64], percent: usize) -> u64 { if sorted.is_empty() { return 0; } sorted[(sorted.len() * percent).div_ceil(100).saturating_sub(1).min(sorted.len() - 1)] } /// Use egui's bundled fonts, or an empty definition set with the `CRAFT_UI_PERF_FONT` file inserted /// first in both proportional and monospace families. Returns the used font names. fn font_definitions() -> Result<(egui::FontDefinitions, Vec), Box> { let mut fonts = egui::FontDefinitions::default(); if let Some(path) = env::var_os("CRAFT_UI_PERF_FONT") { fonts = egui::FontDefinitions::empty(); let name = "perf-fixture".to_owned(); fonts.font_data.insert(name.clone(), Arc::new(egui::FontData::from_owned(std::fs::read(&path)?))); for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { fonts.families.entry(family).or_default().insert(0, name.clone()); } } let names = fonts.font_data.keys().cloned().collect(); Ok((fonts, names)) } /// Describe the report's font source: the pinned `CRAFT_UI_PERF_FONT` file when /// set, otherwise egui's bundled default fonts. fn font_source() -> &'static str { if env::var_os("CRAFT_UI_PERF_FONT").is_some() { "CRAFT_UI_PERF_FONT (pinned fixture file)" } else { "egui default_fonts (bundled)" } } /// A headless context at `scale` with the given fonts, dark visuals, zero animation time and no /// cursor blinking. fn context(scale: f32, fonts: &egui::FontDefinitions) -> Context { let ctx = Context::default(); ctx.set_pixels_per_point(scale); ctx.set_fonts(fonts.clone()); ctx.set_visuals(egui::Visuals::dark()); ctx.all_styles_mut(|style| { style.animation_time = 0.0; style.visuals.text_cursor.blink = false; }); ctx } /// A headless screen rectangle from `(0, 0)` to `viewport[0] × viewport[1]`. fn screen_rect(viewport: [f32; 2]) -> Rect { Rect::from_min_size(pos2(0.0, 0.0), vec2(viewport[0], viewport[1])) } /// A focused 60 Hz input for `viewport` with a bounded texture side for headless frames. fn raw_input(viewport: [f32; 2], frame: usize) -> egui::RawInput { egui::RawInput { screen_rect: Some(screen_rect(viewport)), time: Some(frame as f64 / 60.0), focused: true, max_texture_side: Some(4096), ..Default::default() } } /// Check that tab fitting partitions every index exactly once, uses finite nonnegative widths, /// stays within `available` (plus epsilon) including the overflow control, and keeps the selected /// tab (clamped to the last tab). fn check_tabs(natural: &[f32], selected: usize, available: f32, minimum: f32, overflow: f32) -> Result<(), Box> { let fit = tabs::fit(natural, selected, available, minimum, overflow); let mut seen = vec![false; natural.len()]; let mut sum = 0.0f32; for (index, width) in &fit.shown { if *index >= natural.len() || seen[*index] || !width.is_finite() || *width < 0.0 { return Err(format!("tabs_fit invalid shown entry ({index}, {width})").into()); } seen[*index] = true; sum += *width; } for index in &fit.overflow { if *index >= natural.len() || seen[*index] { return Err(format!("tabs_fit invalid overflow index {index}").into()); } seen[*index] = true; } if seen.iter().any(|shown| !*shown) || sum + fit.overflow_width > available + EPS { return Err(format!("tabs_fit did not partition {available}: sum={sum} overflow={}", fit.overflow_width).into()); } let selected = selected.min(natural.len().saturating_sub(1)); if !natural.is_empty() && !fit.shown.iter().any(|(index, _)| *index == selected) { return Err("tabs_fit dropped the selected tab".into()); } Ok(()) } /// Check a split layout: both panes are positive and sum to the span minus the gap, do not overlap /// or escape, `resized` stays finite, and an inverted rect is rejected. Returns the layout. fn check_split(rect: Rect, axis: SplitAxis, size: SplitSize, gap: f32, minimum: f32) -> Result> { let layout = split_rect(rect, axis, size, gap, minimum).ok_or("split_rect returned None")?; let (span, first, second) = match axis { SplitAxis::Horizontal => (rect.width(), layout.first.width(), layout.second.width()), SplitAxis::Vertical => (rect.height(), layout.first.height(), layout.second.height()), }; let (divider_end, second_start) = match axis { SplitAxis::Horizontal => (layout.divider.max.x, layout.second.min.x), SplitAxis::Vertical => (layout.divider.max.y, layout.second.min.y), }; if !(first > 0.0 && second > 0.0) || (first + second + gap.min(span) - span).abs() > EPS { return Err(format!("split_rect geometry: first={first} second={second} span={span}").into()); } if divider_end > second_start + EPS || !rect.expand(EPS).contains_rect(layout.first) || !rect.expand(EPS).contains_rect(layout.second) { return Err("split_rect panes overlap or escape their rect".into()); } let next = layout.resized(size, 12.0, minimum).ok_or("split_resized returned None")?; let finite = match next { SplitSize::Ratio(v) | SplitSize::FixedFirst(v) | SplitSize::FixedSecond(v) => v.is_finite(), }; if !finite { return Err("split_resized returned a non-finite size".into()); } if split_rect(Rect::from_min_max(pos2(10.0, 10.0), pos2(0.0, 0.0)), axis, size, gap, minimum).is_some() { return Err("split_rect accepted an inverted rect".into()); } Ok(layout) } /// Check that toolbar fitting is an exact prefix partition: shown widths match the natural widths, /// overflow is the remaining suffix, a nonempty overflow reserves a positive control, and a /// zero-width group is rejected. fn check_toolbar(natural: &[f32], available: f32, spacing: f32, overflow: f32) -> Result<(), Box> { let fit = fit_toolbar(natural, available, spacing, overflow).ok_or("fit_toolbar returned None")?; let mut expected = 0; for (index, width) in &fit.shown { if *index >= natural.len() || *index != expected || (*width - natural[*index]).abs() > EPS { return Err(format!("fit_toolbar broken prefix at {index}").into()); } expected += 1; } if fit.overflow.iter().any(|index| *index < expected) || expected + fit.overflow.len() != natural.len() { return Err("fit_toolbar did not partition the groups".into()); } if !fit.overflow.is_empty() && fit.overflow_width <= 0.0 { return Err("fit_toolbar overflowed without reserving a control".into()); } if fit_toolbar(&[0.0], available, spacing, overflow).is_some() { return Err("fit_toolbar accepted a zero-width group".into()); } Ok(()) } /// A one-panel tabs node with the panel active. fn leaf(panel: u32) -> Node { Node::Tabs { panels: vec![panel], active: 0 } } /// A balanced recursive split fixture over `panels`, alternating the axis with `depth`. fn tree(panels: &[u32], depth: usize) -> Node { if panels.len() == 1 { return leaf(panels[0]); } let mid = panels.len() / 2; Node::Split { axis: if depth.is_multiple_of(2) { SplitAxis::Horizontal } else { SplitAxis::Vertical }, size: SplitSize::Ratio(0.5), first: Box::new(tree(&panels[..mid], depth + 1)), second: Box::new(tree(&panels[mid..], depth + 1)), } } /// The benchmark layout: with `with_floating`, a fixed 240 pt second column beside a vertical split /// plus floating window id 1; otherwise a balanced tree over `panels`. fn dock_layout(panels: u32, with_floating: bool) -> Layout { if with_floating { Layout { root: Some(Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedSecond(240.0), first: Box::new(Node::Split { axis: SplitAxis::Vertical, size: SplitSize::Ratio(0.55), first: Box::new(leaf(0)), second: Box::new(leaf(1)), }), second: Box::new(leaf(2)), }), floating: vec![Floating::single(vec![3], 0, [80.0, 70.0, 300.0, 220.0]).with_id(1)], } } else { let all: Vec = (0..panels).collect(); Layout { root: Some(tree(&all, 0)), floating: Vec::new() } } } /// Return the benchmark content's logical label, `Panel {panel}`. fn panel_label(panel: &u32) -> String { format!("Panel {panel}") } /// Count texture-set images, their color bytes, and freed images in `delta`. fn texture_counts(delta: &egui::TexturesDelta) -> (usize, usize, usize) { let mut images = 0; let mut bytes = 0; for deltas in delta.set.values() { for image in deltas { images += 1; let egui::ImageData::Color(data) = &image.image; bytes += data.as_raw().len(); } } (images, bytes, delta.free.len()) } /// Sum mesh vertices and indices, rejecting renderer callbacks because benchmark fixtures must not /// produce them. fn count_mesh(primitives: &[egui::epaint::ClippedPrimitive]) -> Result<(usize, usize), Box> { let (mut vertices, mut indices) = (0, 0); for clipped in primitives { match &clipped.primitive { egui::epaint::Primitive::Mesh(mesh) => { vertices += mesh.vertices.len(); indices += mesh.indices.len(); } egui::epaint::Primitive::Callback(_) => return Err("the benchmark fixtures must not produce renderer callbacks".into()), } } Ok((vertices, indices)) } // Fingerprint of one frame's tessellated primitives, beyond equal vertex/index counts. Texture IDs and // clip bounds are included. This is a 64-bit FNV-1a checksum, not an exact mesh identity. Timings stop // before this evidence collection. /// A 64-bit FNV-1a fingerprint over primitive count, clip rects, texture ids, vertex data and /// indices. This is a diagnostic checksum, not exact mesh identity; callbacks are rejected. fn mesh_hash(primitives: &[egui::epaint::ClippedPrimitive]) -> Result> { let mut hash = 0xcbf29ce484222325u64; let mut feed = |bytes: &[u8]| { for byte in bytes { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x100000001b3); } }; feed(&(primitives.len() as u64).to_le_bytes()); for clipped in primitives { for value in [clipped.clip_rect.min.x, clipped.clip_rect.min.y, clipped.clip_rect.max.x, clipped.clip_rect.max.y] { feed(&value.to_bits().to_le_bytes()); } match &clipped.primitive { egui::epaint::Primitive::Mesh(mesh) => { feed(&[0]); match mesh.texture_id { egui::TextureId::Managed(id) => { feed(&[0]); feed(&id.to_le_bytes()); } egui::TextureId::User(id) => { feed(&[1]); feed(&id.to_le_bytes()); } } feed(&(mesh.vertices.len() as u64).to_le_bytes()); feed(&(mesh.indices.len() as u64).to_le_bytes()); for vertex in &mesh.vertices { for value in [vertex.pos.x, vertex.pos.y, vertex.uv.x, vertex.uv.y] { feed(&value.to_bits().to_le_bytes()); } feed(&vertex.color.to_array()); } for index in &mesh.indices { feed(&index.to_le_bytes()); } } egui::epaint::Primitive::Callback(_) => return Err("the benchmark fixtures must not produce renderer callbacks".into()), } } Ok(hash) } /// Run `FRAME_SAMPLES` measured frames after at least 50 warmup frames and 3 seconds. Times and /// completed passes are recorded for every measured frame; the final measured frame also collects /// bodies and the shapes, mesh counts and hash reported in the result. Texture and repaint counters /// aggregate over measured frames, and texture deltas are cleared each frame. fn measure_frames( ctx: &Context, viewport: [f32; 2], paint: &mut impl FnMut(&mut egui::Ui, &mut BTreeMap, bool), ) -> Result> { let mut result = Frames::default(); result.times_ns.reserve(FRAME_SAMPLES); result.passes.reserve(FRAME_SAMPLES); let warmup_start = Instant::now(); let mut frame = 0; while result.times_ns.len() < FRAME_SAMPLES { let warming = frame < WARMUPS || warmup_start.elapsed() < MIN_WARMUP; if !warming && result.times_ns.is_empty() { result.galleys_after_warmup = ctx.fonts(|fonts| fonts.num_galleys_in_cache()); result.warmup_elapsed_ns = warmup_start.elapsed().as_nanos() as u64; } // The last measured frame also collects panel bodies, so bodies, geometry and mesh evidence all // describe the same rendered frame as the reported timings. let collect = !warming && result.times_ns.len() + 1 == FRAME_SAMPLES; result.bodies.clear(); let start = Instant::now(); let mut output = ctx.run_ui(raw_input(viewport, frame), |ui| paint(ui, &mut result.bodies, collect)); let ppp = output.pixels_per_point; let shapes = output.shapes.len(); let primitives = ctx.tessellate(output.shapes, ppp); if !warming { result.times_ns.push(start.elapsed().as_nanos() as u64); result.passes.push(output.platform_output.num_completed_passes); result.shapes = shapes; (result.vertices, result.indices) = count_mesh(&primitives)?; result.mesh_hash = mesh_hash(&primitives)?; let (sets, bytes, frees) = texture_counts(&output.textures_delta); result.set_images += sets; result.set_bytes += bytes; result.free_images += frees; if output.viewport_output.get(&egui::ViewportId::ROOT).is_some_and(|output| output.repaint_delay != std::time::Duration::MAX) { result.repaint_frames += 1; } } else { result.warmups += 1; } output.textures_delta.clear(); frame += 1; } result.next_frame = frame; Ok(result) } /// Check every recorded body is positive, finite and inside the viewport; non-floating bodies may /// not overlap by more than 0.5 pt. Returns the body count. fn check_bodies(screen: Rect, bodies: &BTreeMap, floating: &BTreeSet) -> Result> { for (panel, rect) in bodies { if !rect.is_positive() || !rect.min.is_finite() || !rect.max.is_finite() { return Err(format!("panel {panel} has empty or invalid body geometry {rect:?}").into()); } if !screen.expand(EPS * 4.0).contains_rect(*rect) { return Err(format!("panel {panel} body {rect:?} escapes the viewport {screen:?}").into()); } } let static_rects: Vec<(u32, Rect)> = bodies.iter().filter(|(panel, _)| !floating.contains(*panel)).map(|(panel, rect)| (*panel, *rect)).collect(); for (i, (a, first)) in static_rects.iter().enumerate() { for (b, second) in static_rects.iter().skip(i + 1) { let overlap = first.intersect(*second); if overlap.width() > 0.5 && overlap.height() > 0.5 { return Err(format!("static panels {a} and {b} overlap: {overlap:?}").into()); } } } Ok(bodies.len()) } /// Check every interaction owned by the dock has a response with a positive hit rect inside the /// viewport. Returns the interaction count. fn check_interactions(ctx: &Context, dock_id: Id, screen: Rect) -> Result> { let owned = ctx.data(|data| data.get_temp::>(dock_id.with("owned-interactions"))).unwrap_or_default(); if owned.is_empty() { return Err("dock rendered no owned interactions".into()); } for id in &owned { let response = ctx.read_response(*id).ok_or_else(|| format!("missing response for dock interaction {id:?}"))?; if !response.interact_rect.is_positive() { return Err(format!("dock interaction {id:?} has empty hit rect {:?}", response.interact_rect).into()); } if !screen.expand(EPS * 4.0).contains_rect(response.interact_rect) { return Err(format!("dock interaction {id:?} hit rect {:?} escapes the viewport", response.interact_rect).into()); } } Ok(owned.len()) } /// Move the pointer to the first body's tab center in a separate unmeasured frame and require the /// tab to hover; returns the probe JSON. The probe is excluded from timings and counters. fn probe_tab_hover( ctx: &Context, viewport: [f32; 2], dock_id: Id, bodies: &BTreeMap, frame: usize, paint: &mut impl FnMut(&mut egui::Ui, &mut BTreeMap, bool), ) -> Result> { let panel = bodies.keys().next().copied().ok_or("no panel body for hover probe")?; let tab_id = dock_id.with(("tab", panel)); let rect = ctx.read_response(tab_id).ok_or("no tab response for hover probe")?.rect; let mut input = raw_input(viewport, frame); input.events.push(egui::Event::PointerMoved(rect.center())); let mut probe_bodies = BTreeMap::new(); ctx.run_ui(input, |ui| paint(ui, &mut probe_bodies, true)).textures_delta.clear(); if !ctx.read_response(tab_id).is_some_and(|response| response.hovered()) { return Err(format!("pointer at {panel}'s tab center did not hover its tab").into()); } Ok(json!({"panel": panel, "tab_center": [rect.center().x, rect.center().y], "hovered": true, "scope": "separate unmeasured frame after the samples; excluded from timings and counters"})) } struct Case<'a> { label: &'a str, kind: &'a str, panels: u32, floating: bool, side_panel: bool, viewport: [f32; 2], scale: f32, } /// Render the fixture dock through `DockArea`, optionally recording each panel body's rect on the /// collection frame, and return the dock error. fn render_dock( ui: &mut egui::Ui, dock_id: Id, layout: &Layout, bodies: &mut BTreeMap, style: &DockStyle, collect: bool, ) -> Option { DockArea::new(dock_id) .show( ui, layout, style, panel_label, |_| Permissions::default(), |ui, panel| { if collect { bodies.insert(*panel, ui.max_rect()); } }, ) .error } struct Scene { dock_id: Id, side_panel: bool, style: Option, error: Option, dock_rect: Rect, left_rect: Option, central_rect: Option, } impl Scene { /// Render the dock inside an optional 180 pt left panel and central panel, capturing panel geometry /// and preserving the first dock error; otherwise render at the available rect. fn render(&mut self, layout: &Layout, ui: &mut egui::Ui, bodies: &mut BTreeMap, collect: bool) { if self.style.is_none() { self.style = Some(DockStyle::from_ui(ui)); } let Self { dock_id, side_panel, style, error, dock_rect, left_rect, central_rect } = self; let Some(style) = style.as_ref() else { return }; if *side_panel { let _ = egui::Panel::left("ui-perf-left").resizable(false).exact_size(180.0).show(ui, |ui| { *left_rect = Some(ui.max_rect()); ui.label("Left panel"); }); let _ = egui::CentralPanel::default().show(ui, |ui| { *central_rect = Some(ui.max_rect()); let next = render_dock(ui, *dock_id, layout, bodies, style, collect); if error.is_none() { *error = next; } }); } else { *dock_rect = ui.available_rect_before_wrap(); let next = render_dock(ui, *dock_id, layout, bodies, style, collect); if error.is_none() { *error = next; } } } } /// Run one case end to end: validate the layout, measure frames, gate sample count, warmup, one /// completed pass per frame and dock error, check body count, interactions and geometry, run the /// hover probe, and return the case JSON. fn run_case(case: Case<'_>, fonts: &egui::FontDefinitions) -> Result> { let ctx = context(case.scale, fonts); let dock_id = Id::unique(("ui-perf", case.label)); let layout = dock_layout(case.panels, case.floating); layout.validate()?; let floating: BTreeSet = layout.floating.iter().flat_map(|group| group.panels().copied()).collect(); let mut scene = Scene { dock_id, side_panel: case.side_panel, style: None, error: None, dock_rect: Rect::NOTHING, left_rect: None, central_rect: None }; let frames = { let mut paint = |ui: &mut egui::Ui, bodies: &mut BTreeMap, collect: bool| scene.render(&layout, ui, bodies, collect); measure_frames(&ctx, case.viewport, &mut paint)? }; if frames.times_ns.len() != FRAME_SAMPLES { return Err(format!("{} recorded {} of {FRAME_SAMPLES} frame samples", case.label, frames.times_ns.len()).into()); } if frames.warmups < WARMUPS || frames.warmup_elapsed_ns < MIN_WARMUP.as_nanos() as u64 { return Err(format!("{} failed its warmup gate: warmups={} elapsed_ns={}", case.label, frames.warmups, frames.warmup_elapsed_ns).into()); } if frames.passes.len() != frames.times_ns.len() || frames.passes.iter().any(|passes| *passes != 1) { return Err(format!("{} has measured frames without exactly one completed pass: {:?}", case.label, frames.passes).into()); } if let Some(error) = scene.error { return Err(format!("{} reported {error}", case.label).into()); } let screen = screen_rect(case.viewport); let body_count = check_bodies(screen, &frames.bodies, &floating)?; let interaction_count = check_interactions(&ctx, dock_id, screen)?; if body_count != case.panels as usize { return Err(format!("{} rendered {body_count} of {} panel bodies", case.label, case.panels).into()); } let geometry = if case.side_panel { let left = scene.left_rect.ok_or("left panel did not render")?; let central = scene.central_rect.ok_or("central panel did not render")?; if !left.is_positive() || !central.is_positive() || left.right() > central.left() + EPS { return Err(format!("left/central panel geometry is invalid: left={left:?} central={central:?}").into()); } json!({"left_rect": [left.min.x, left.min.y, left.width(), left.height()], "central_rect": [central.min.x, central.min.y, central.width(), central.height()]}) } else { let dock_rect = scene.dock_rect; if !dock_rect.is_positive() { return Err(format!("{} had an empty available rect", case.label).into()); } json!({"dock_rect": [dock_rect.min.x, dock_rect.min.y, dock_rect.width(), dock_rect.height()]}) }; let hover_probe = { let mut probe = |ui: &mut egui::Ui, bodies: &mut BTreeMap, collect: bool| scene.render(&layout, ui, bodies, collect); probe_tab_hover(&ctx, case.viewport, dock_id, &frames.bodies, frames.next_frame, &mut probe)? }; let frame_stats = summarize(&frames.times_ns); Ok(json!({ "case": case.label, "kind": case.kind, "panels": case.panels, "floating": case.floating, "viewport_points": case.viewport, "viewport_physical": [case.viewport[0] * case.scale, case.viewport[1] * case.scale], "pixels_per_point": case.scale, "frames": frames.times_ns.len(), "warmups": frames.warmups, "warmup_elapsed_ns": frames.warmup_elapsed_ns, "last_measured_mesh_fnv64": format!("{:016x}", frames.mesh_hash), "last_measured_mesh_scope": "64-bit FNV-1a fingerprint of the final measured frame's primitives; not an exact identity", "frame_scope": "Context::run_ui + Context::tessellate", "frame_times_ns": frames.times_ns, "frame_stats": frame_stats, "passes_per_frame": frames.passes, "counter_scope": "shapes, vertices, indices and the mesh fingerprint describe the final measured frame, which also collects bodies and geometry; texture and repaint counters are summed over all measured frames", "checks_scope": "final measured frame", "galleys_after_warmup": frames.galleys_after_warmup, "shapes": frames.shapes, "vertices": frames.vertices, "indices": frames.indices, "texture_set_images": frames.set_images, "texture_set_bytes": frames.set_bytes, "texture_free_images": frames.free_images, "repaint_frames": frames.repaint_frames, "galleys": ctx.fonts(|fonts| fonts.num_galleys_in_cache()), "hover_probe": hover_probe, "checks": {"bodies": body_count, "interactions": interaction_count, "geometry": geometry}, })) }