4K MULTI LAYEPERFORMANCE OK : SOL 5.6 HIGH feat(perf): add real-time canvas performance measurements
- Introduced a new performance module for the iced canvas to measure input queueing, engine drawing, CPU compositing/staging, GPU upload, and renderer preparation costs. - Implemented a performance probe in the egui reference renderer for comparative diagnostics. - Added methods to record durations, byte transfers, and input timestamps, enabling detailed performance analysis. - Enhanced the CanvasShaderPipeline with a new method to upload full textures without rebuilding GPU resources. - Updated the main application to conditionally log performance diagnostics based on an environment variable. - Created a performance plan document outlining steps to measure and optimize drawing performance in the iced application.
This commit is contained in:
@@ -6,6 +6,7 @@ use eframe::egui;
|
||||
use hcie_engine_api::{LayerData, Tool, VectorEditHandle, VectorShape, ZOOM_MAX, ZOOM_MIN};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub mod perf;
|
||||
pub mod render;
|
||||
pub mod text_overlay;
|
||||
|
||||
@@ -299,6 +300,9 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
));
|
||||
return response;
|
||||
}
|
||||
let input_at = std::time::Instant::now();
|
||||
perf::begin_stroke(input_at);
|
||||
perf::record_input(input_at);
|
||||
// Send current brush state to engine before stroke
|
||||
self.doc.engine.set_color(self.state.primary_color);
|
||||
let mut tip = hcie_engine_api::BrushTip::default();
|
||||
@@ -487,6 +491,7 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
self.state.brush_accumulated_dist = 0.0;
|
||||
let layer_id = self.doc.engine.active_layer_id();
|
||||
if self.state.active_tool != Tool::Pen {
|
||||
let engine_start = std::time::Instant::now();
|
||||
self.doc.engine.begin_stroke(layer_id, ux as f32, uy as f32);
|
||||
// Draw single dab on click (even without movement).
|
||||
// The spacing check in the drag handler prevents
|
||||
@@ -497,6 +502,7 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
uy as f32,
|
||||
self.state.current_pressure,
|
||||
);
|
||||
perf::record_duration("engine_draw", engine_start.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,6 +921,9 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
match self.state.active_tool {
|
||||
Tool::Pen => {
|
||||
if let Some((lx, ly)) = self.state.last_draw_pos {
|
||||
let input_at = std::time::Instant::now();
|
||||
perf::record_input(input_at);
|
||||
let engine_start = std::time::Instant::now();
|
||||
let layer_id = self.doc.engine.active_layer_id();
|
||||
if !self.doc.engine.is_stroke_active() {
|
||||
self.doc
|
||||
@@ -929,6 +938,7 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
uy as f32,
|
||||
self.state.current_pressure,
|
||||
);
|
||||
perf::record_duration("engine_draw", engine_start.elapsed());
|
||||
self.state.last_draw_pos = Some((ux, uy));
|
||||
}
|
||||
}
|
||||
@@ -947,6 +957,9 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
}
|
||||
.max(1.0);
|
||||
if dx >= spacing {
|
||||
let input_at = std::time::Instant::now();
|
||||
perf::record_input(input_at);
|
||||
let engine_start = std::time::Instant::now();
|
||||
let layer_id = self.doc.engine.active_layer_id();
|
||||
self.doc.engine.stroke_to(
|
||||
layer_id,
|
||||
@@ -954,6 +967,10 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
uy as f32,
|
||||
self.state.current_pressure,
|
||||
);
|
||||
perf::record_duration(
|
||||
"engine_draw",
|
||||
engine_start.elapsed(),
|
||||
);
|
||||
self.state.last_draw_pos = Some((ux, uy));
|
||||
}
|
||||
}
|
||||
@@ -1223,6 +1240,7 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
|
||||
self.event_bus.push(AppEvent::RenderRequested);
|
||||
self.event_bus.push(AppEvent::DrawingFinished);
|
||||
perf::request_finish();
|
||||
}
|
||||
Tool::Select => {
|
||||
if let Some(rect) = self.state.selection_rect {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Opt-in canvas performance probe for the egui reference renderer.
|
||||
//!
|
||||
//! **Purpose:** Produces the same stage names as the iced canvas diagnostics so
|
||||
//! immediate-mode and reactive pipelines can be compared on one machine.
|
||||
//! **Logic & Workflow:** `HCIE_CANVAS_PERF=1` enables per-stroke duration and byte
|
||||
//! collection; the first texture update after release emits p50/p95/max values.
|
||||
//! **Side Effects / Dependencies:** Uses a process-wide mutex and the `log` crate.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Mutable measurements for one egui stroke.
|
||||
#[derive(Default)]
|
||||
struct PerfState {
|
||||
stroke: u64,
|
||||
active: bool,
|
||||
finish_pending: bool,
|
||||
latest_input: Option<Instant>,
|
||||
samples: BTreeMap<&'static str, Vec<f64>>,
|
||||
copied_bytes: u64,
|
||||
uploaded_bytes: u64,
|
||||
visible_updates: u64,
|
||||
}
|
||||
|
||||
/// Returns whether `HCIE_CANVAS_PERF` enables diagnostics.
|
||||
pub fn enabled() -> bool {
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*ENABLED.get_or_init(|| {
|
||||
std::env::var("HCIE_CANVAS_PERF")
|
||||
.map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Starts a new per-stroke sample window.
|
||||
pub fn begin_stroke(input_at: Instant) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
with_state(|state| {
|
||||
state.stroke = state.stroke.wrapping_add(1);
|
||||
state.active = true;
|
||||
state.finish_pending = false;
|
||||
state.latest_input = Some(input_at);
|
||||
state.samples.clear();
|
||||
state.copied_bytes = 0;
|
||||
state.uploaded_bytes = 0;
|
||||
state.visible_updates = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// Records the latest pixel-changing input and its immediate dispatch delay.
|
||||
pub fn record_input(input_at: Instant) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
with_state(|state| {
|
||||
if state.active {
|
||||
state.latest_input = Some(input_at);
|
||||
state
|
||||
.samples
|
||||
.entry("input_to_update")
|
||||
.or_default()
|
||||
.push(input_at.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Records one named stage duration.
|
||||
pub fn record_duration(stage: &'static str, duration: Duration) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
with_state(|state| {
|
||||
if state.active {
|
||||
state
|
||||
.samples
|
||||
.entry(stage)
|
||||
.or_default()
|
||||
.push(duration.as_secs_f64() * 1000.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Records CPU copy and texture payload bytes for one visible update.
|
||||
pub fn record_transfer(copied_bytes: usize, uploaded_bytes: usize) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
with_state(|state| {
|
||||
if state.active {
|
||||
state.copied_bytes = state.copied_bytes.saturating_add(copied_bytes as u64);
|
||||
state.uploaded_bytes = state.uploaded_bytes.saturating_add(uploaded_bytes as u64);
|
||||
state.visible_updates += 1;
|
||||
if let Some(input) = state.latest_input {
|
||||
state
|
||||
.samples
|
||||
.entry("input_to_texture_update_proxy")
|
||||
.or_default()
|
||||
.push(input.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Requests summary emission after the final texture update.
|
||||
pub fn request_finish() {
|
||||
if enabled() {
|
||||
with_state(|state| state.finish_pending = state.active);
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the pending summary after a render pass has had an opportunity to upload.
|
||||
pub fn finish_after_render() {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
with_state(|state| {
|
||||
if !state.active || !state.finish_pending {
|
||||
return;
|
||||
}
|
||||
state.active = false;
|
||||
state.finish_pending = false;
|
||||
let mut stages = Vec::new();
|
||||
for (name, samples) in &mut state.samples {
|
||||
samples.sort_by(f64::total_cmp);
|
||||
if let Some(max) = samples.last().copied() {
|
||||
stages.push(format!(
|
||||
"{}[n={},p50={:.3}ms,p95={:.3}ms,max={:.3}ms]",
|
||||
name,
|
||||
samples.len(),
|
||||
percentile(samples, 0.50),
|
||||
percentile(samples, 0.95),
|
||||
max
|
||||
));
|
||||
}
|
||||
}
|
||||
log::info!(
|
||||
target: "hcie_canvas_perf",
|
||||
"egui stroke={} {} counters[copied={}B,uploaded={}B,visible_updates={}]",
|
||||
state.stroke,
|
||||
stages.join(" "),
|
||||
state.copied_bytes,
|
||||
state.uploaded_bytes,
|
||||
state.visible_updates
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Selects the nearest sample for a percentile from an already sorted slice.
|
||||
fn percentile(samples: &[f64], fraction: f64) -> f64 {
|
||||
if samples.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let index = ((samples.len() - 1) as f64 * fraction.clamp(0.0, 1.0)).round() as usize;
|
||||
samples[index]
|
||||
}
|
||||
|
||||
/// Runs an operation while holding the diagnostic accumulator lock.
|
||||
fn with_state<T>(operation: impl FnOnce(&mut PerfState) -> T) -> T {
|
||||
static STATE: OnceLock<Mutex<PerfState>> = OnceLock::new();
|
||||
let mut state = STATE
|
||||
.get_or_init(|| Mutex::new(PerfState::default()))
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
operation(&mut state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::percentile;
|
||||
|
||||
/// Confirms percentile output matches the iced probe for equivalent samples.
|
||||
#[test]
|
||||
fn percentile_uses_nearest_ranked_sample() {
|
||||
let samples = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
assert_eq!(percentile(&samples, 0.50), 3.0);
|
||||
assert_eq!(percentile(&samples, 0.95), 5.0);
|
||||
}
|
||||
}
|
||||
@@ -169,13 +169,17 @@ pub fn render_composition(
|
||||
if needs_render {
|
||||
let t_composite_start = std::time::Instant::now();
|
||||
let (region_result, buf_ptr, buf_size) = doc.engine.render_composite_region();
|
||||
let composite_ms = t_composite_start.elapsed().as_millis();
|
||||
let composite_elapsed = t_composite_start.elapsed();
|
||||
let composite_ms = composite_elapsed.as_millis();
|
||||
super::perf::record_duration("render_composite_region", composite_elapsed);
|
||||
|
||||
let copy_start = std::time::Instant::now();
|
||||
if !buf_ptr.is_null() && buf_size > 0 && buf_size == doc.composite_buffer.len() {
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(buf_ptr, doc.composite_buffer.as_mut_ptr(), buf_size);
|
||||
}
|
||||
}
|
||||
super::perf::record_duration("cpu_staging", copy_start.elapsed());
|
||||
|
||||
let region = match region_result {
|
||||
Some([x0, y0, x1, y1]) if x1 > x0 && y1 > y0 => [x0, y0, x1, y1],
|
||||
@@ -188,6 +192,8 @@ pub fn render_composition(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let texture_start = std::time::Instant::now();
|
||||
let mut uploaded_bytes = 0usize;
|
||||
// Partial texture upload for the dirty region only
|
||||
if let Some(tex) = &mut doc.composite_texture {
|
||||
let size = tex.size();
|
||||
@@ -212,6 +218,7 @@ pub fn render_composition(
|
||||
®ion_pixels,
|
||||
);
|
||||
tex.set_partial([x0 as usize, y0 as usize], region_image, options);
|
||||
uploaded_bytes = region_pixels.len();
|
||||
}
|
||||
} else {
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
@@ -219,6 +226,7 @@ pub fn render_composition(
|
||||
&doc.composite_buffer,
|
||||
);
|
||||
tex.set(color_image, options);
|
||||
uploaded_bytes = doc.composite_buffer.len();
|
||||
}
|
||||
} else {
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
@@ -226,7 +234,10 @@ pub fn render_composition(
|
||||
&doc.composite_buffer,
|
||||
);
|
||||
doc.composite_texture = Some(ctx.load_texture("composite-view", color_image, options));
|
||||
uploaded_bytes = doc.composite_buffer.len();
|
||||
}
|
||||
super::perf::record_duration("texture_update", texture_start.elapsed());
|
||||
super::perf::record_transfer(buf_size, uploaded_bytes);
|
||||
|
||||
doc.thumbnails_dirty = true;
|
||||
doc.engine.clear_dirty_flags();
|
||||
@@ -265,6 +276,7 @@ pub fn render_composition(
|
||||
}
|
||||
}
|
||||
|
||||
super::perf::finish_after_render();
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user