Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 99 additions & 5 deletions crates/app/src/ui/canvas/workspace_render_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,20 @@ fn desktop_frames(app: &mut PlotiumApp, label: &str) {
let mut title = None;
let mut samples = Vec::new();
let mut tessellation = Vec::new();
for frame in 0..13 {
let mut totals = Vec::new();
let mut vertices = 0;
const DESKTOP_WARMUP_FRAMES: usize = 90;
for frame in 0..DESKTOP_WARMUP_FRAMES + MEASURED_FRAMES {
if frame == 60 {
// Freeze the settled fit before timing Full detail; spring movement
// otherwise keeps refreshing the interactive-detail grace period.
app.session.viewport_mode = plotium_core::state::ViewportMode::Manual;
}
let start = Instant::now();
let output = ctx.run_ui(
egui::RawInput {
// Advance beyond the navigation detail grace period before sampling.
time: Some(frame as f64 / 60.0),
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(SCREEN_WIDTH, SCREEN_HEIGHT),
Expand All @@ -113,17 +123,39 @@ fn desktop_frames(app: &mut PlotiumApp, label: &str) {
);
let ui_time = start.elapsed();
let start = Instant::now();
std::hint::black_box(ctx.tessellate(output.shapes, output.pixels_per_point));
if frame >= 3 {
let primitives = ctx.tessellate(output.shapes, output.pixels_per_point);
let tessellation_time = start.elapsed();
if frame >= DESKTOP_WARMUP_FRAMES {
assert_eq!(
super::render_detail::workspace_render_detail(&ctx, frame as f64 / 60.0),
ScreenRenderDetail::Full
);
vertices = primitives
.iter()
.map(|primitive| match &primitive.primitive {
egui::epaint::Primitive::Mesh(mesh) => mesh.vertices.len(),
egui::epaint::Primitive::Callback(_) => 0,
})
.sum::<usize>();
samples.push(ui_time);
tessellation.push(start.elapsed());
totals.push(ui_time + tessellation_time);
tessellation.push(tessellation_time);
}
}
samples.sort_unstable();
tessellation.sort_unstable();
totals.sort_unstable();
println!(
"{label} total CPU: median={:?} p95={:?}; vertices={vertices}",
totals[totals.len() / 2],
totals[(totals.len() * 95).div_ceil(100) - 1]
);
println!(
"{label} desktop UI: median={:?} p95={:?}; tessellation median={:?} p95={:?}",
samples[5], samples[9], tessellation[5], tessellation[9]
samples[samples.len() / 2],
samples[(samples.len() * 95).div_ceil(100) - 1],
tessellation[tessellation.len() / 2],
tessellation[(tessellation.len() * 95).div_ceil(100) - 1]
);
}

Expand Down Expand Up @@ -266,3 +298,65 @@ fn add_stats(total: &mut RenderStats, frame: &RenderStats) {
total.contour_segments_visited += frame.contour_segments_visited;
total.contour_segments_submitted += frame.contour_segments_submitted;
}

/// CPU UI construction and tessellation only, not end-to-end GPU frame time.
/// Run with cargo test --release -p plotium bench_stress_workspace -- --ignored --nocapture.
#[test]
#[ignore = "synthetic CPU benchmark; use a release build"]
fn bench_stress_workspace() {
use plotium_core::actions::Action;
use plotium_core::state::{BoardFitTarget, Dataset, NmrDataset, ViewportMode};
for count in [12, 60, 120, 500] {
let mut app = PlotiumApp::new_with_settings(plotium_core::settings::Settings::default());
for index in 0..count {
let data = plotium_io::NmrData {
points: (0..32768)
.map(|i| {
let x = i as f64 / 32768.0;
num_complex::Complex64::new(
(-((x - 0.35) / 0.01).powi(2)).exp()
+ 0.4 * (-((x - 0.7) / 0.02).powi(2)).exp(),
0.0,
)
})
.collect(),
domain: plotium_io::Domain::Frequency,
spectral_width_hz: 4000.0,
observe_freq_mhz: 400.0,
carrier_ppm: 5.0,
nucleus: "1H".to_owned(),
source: format!("synthetic-{index}"),
group_delay: 0.0,
};
let dataset = Dataset::Nmr(Box::new(NmrDataset::load(data).unwrap()));
// Prepare each independent page without measuring repeated import/history
// bookkeeping; this benchmark measures only the finished workspace.
let mut page_app =
PlotiumApp::new_with_settings(plotium_core::settings::Settings::default());
page_app
.try_execute_action(Action::insert_dataset_with_default_canvas(
&page_app,
dataset,
format!("Spectrum {index}"),
[160.0, 100.0],
))
.unwrap();
wait_for_compute(&mut page_app);
let mut canvas = page_app.doc.canvases.remove(0);
let size = canvas.size_pt();
canvas.board_pos = [
(index % 3) as f32 * (size[0] + plotium_core::state::BOARD_GUTTER_PT),
(index / 3) as f32 * (size[1] + plotium_core::state::BOARD_GUTTER_PT),
];
app.doc.datasets.append(&mut page_app.doc.datasets);
app.doc.canvases.push(canvas);
}
app.activate_canvas(0);
app.session.viewport_mode = ViewportMode::Fit(BoardFitTarget::AllFrames);
desktop_frames(&mut app, &format!("{count} overview"));
app.session.viewport_mode = ViewportMode::Fit(BoardFitTarget::Frame(
plotium_core::state::BoardFrameId::Page(app.doc.canvases[count / 2].resource_id),
));
desktop_frames(&mut app, &format!("{count} single-canvas"));
}
}
12 changes: 6 additions & 6 deletions crates/render/src/screen_contour_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,20 @@ pub(super) fn screen_contour_segments_cached<'a>(
let cache = shared_cache(ctx);
if let Some(lod) = lock_cache(&cache).get(key) {
return match detail {
ScreenRenderDetail::Full => ScreenContourSegments::full(segments, 0),
ScreenRenderDetail::Interactive => {
ScreenContourSegments::from_lod(segments, lod, budget, 0)
ScreenRenderDetail::Full if budget == usize::MAX => {
ScreenContourSegments::full(segments, 0)
}
_ => ScreenContourSegments::from_lod(segments, lod, budget, 0),
};
}

let lod = prepare_contour_lod(segments, viewport);
lock_cache(&cache).insert(key, Arc::clone(&lod));
match detail {
ScreenRenderDetail::Full => ScreenContourSegments::full(segments, segments.len()),
ScreenRenderDetail::Interactive => {
ScreenContourSegments::from_lod(segments, lod, budget, segments.len())
ScreenRenderDetail::Full if budget == usize::MAX => {
ScreenContourSegments::full(segments, segments.len())
}
_ => ScreenContourSegments::from_lod(segments, lod, budget, segments.len()),
}
}

Expand Down
10 changes: 6 additions & 4 deletions crates/render/src/screen_lod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::screen::ScreenRenderDetail;
use std::sync::Arc;

pub(super) const FULL_MIN_LINE_COLUMNS: usize = 2_048;
pub(super) const FULL_MIN_LINE_COLUMNS: usize = 64;
pub(super) const FULL_MAX_LINE_COLUMNS: usize = 16_384;
pub(super) const INTERACTIVE_MIN_LINE_COLUMNS: usize = 64;
pub(super) const INTERACTIVE_MAX_LINE_COLUMNS: usize = 2_048;
Expand Down Expand Up @@ -90,10 +90,12 @@ pub(super) fn contour_segment_budget(
plot_height: f32,
pixels_per_point: f32,
) -> usize {
if detail == ScreenRenderDetail::Full {
let scale = valid_pixel_scale(pixels_per_point);
// Keep inspection-sized plots exact; only small overview plots use a
// segment budget. Export renderers do not use this screen-only policy.
if detail == ScreenRenderDetail::Full && plot_width.max(plot_height) * scale >= 256.0 {
return usize::MAX;
}
let scale = valid_pixel_scale(pixels_per_point);
let physical_area = (plot_width * scale).max(1.0) * (plot_height * scale).max(1.0);
((physical_area / 8.0) as usize).clamp(
INTERACTIVE_MIN_CONTOUR_SEGMENTS,
Expand All @@ -107,7 +109,7 @@ pub(super) fn screen_contour_segments(
viewport: [f64; 4],
budget: usize,
) -> ScreenContourSegments<'_> {
if detail == ScreenRenderDetail::Full {
if detail == ScreenRenderDetail::Full && budget == usize::MAX {
return ScreenContourSegments::full(segments, 0);
}
let selected = prepare_contour_lod(segments, viewport);
Expand Down
51 changes: 45 additions & 6 deletions crates/render/src/screen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn short_trace_keeps_its_real_samples() {
let points: Vec<_> = (0..2_000)
.map(|index| [index as f64, (index % 7) as f64])
.collect();
let drawn = line_points(&points, 0.0, 2_000.0, FULL_MIN_LINE_COLUMNS);
let drawn = line_points(&points, 0.0, 2_000.0, 2_048);
assert!(drawn.points.as_ref() == points.as_slice());
assert!(matches!(drawn.points, std::borrow::Cow::Borrowed(_)));
}
Expand Down Expand Up @@ -169,10 +169,7 @@ fn non_monotonic_x_is_pooled_without_unsafe_clipping() {

#[test]
fn columns_track_device_pixels_within_bounds() {
assert_eq!(
line_columns(ScreenRenderDetail::Full, 320.0, 1.0),
FULL_MIN_LINE_COLUMNS
);
assert_eq!(line_columns(ScreenRenderDetail::Full, 320.0, 1.0), 640);
assert_eq!(line_columns(ScreenRenderDetail::Full, 900.0, 2.0), 3_600);
assert_eq!(
line_columns(ScreenRenderDetail::Full, 9_000.0, 2.0),
Expand Down Expand Up @@ -231,7 +228,7 @@ fn interactive_contour_budget_is_stable_and_prioritizes_visible_segments() {
&segments,
ScreenRenderDetail::Full,
[0.0, 1.0, 0.0, 1.0],
512,
usize::MAX,
);
assert_eq!(full.len(), source_len);
}
Expand Down Expand Up @@ -356,3 +353,45 @@ fn log_y_pooling_ignores_samples_without_a_position() {
let interior = &drawn.points[1..drawn.points.len() - 1];
assert!(interior.iter().all(|p| p[1] > 0.0));
}

#[test]
fn overview_full_detail_contours_are_bounded_but_inspection_is_exact() {
use crate::screen_contour_cache::{ContourCacheKey, screen_contour_segments_cached};

let small = contour_segment_budget(ScreenRenderDetail::Full, 40.0, 30.0, 1.0);
let larger = contour_segment_budget(ScreenRenderDetail::Full, 200.0, 150.0, 1.0);
assert!(small < larger);
let segments = vec![[[0.0, 0.0], [1.0, 1.0]]; 20_000];
let ctx = egui::Context::default();
let viewport = [0.0, 1.0, 0.0, 1.0];
let key = ContourCacheKey::new(7, 0, segments.len(), viewport);
let draw = |budget| {
screen_contour_segments_cached(
&ctx,
Some(key),
&segments,
ScreenRenderDetail::Full,
viewport,
budget,
)
};

let overview = draw(small);
assert_eq!(overview.len(), small);
assert_eq!(overview.source_segments_scanned(), segments.len());

let inspection = draw(contour_segment_budget(
ScreenRenderDetail::Full,
800.0,
600.0,
1.0,
));
assert_eq!(inspection.len(), segments.len());
assert_eq!(inspection.source_segments_scanned(), 0);

let overview_again = draw(small);
assert_eq!(overview_again.len(), small);
assert_eq!(overview_again.source_segments_scanned(), 0);
assert_eq!(line_columns(ScreenRenderDetail::Full, 4.0, 1.0), 64);
assert_eq!(line_columns(ScreenRenderDetail::Full, 1200.0, 1.0), 2400);
}
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/ui-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ introduces the same regions in walkthrough form.
near another frame, it snaps to that frame's edges and the standard gap.
Hold <kbd>Alt</kbd> while dragging to bypass snapping. See
[Navigation](/reference/shortcuts/#navigation) for fitting and moving the view.
Screen plots adapt line and contour detail to their displayed size. Zoom in
to inspect fine detail; figure exports are unaffected by this screen simplification.
- **Data sheet** — the panel docked below the board where a table is edited.
The board shrinks to make room. Drag its top edge to resize it; its header
collapses, maximizes, or closes it. See [Tables](/guides/tables/#editing).
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/zh-cn/reference/ui-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Plotium 的界面为英文;手册中加粗的英文词即界面上的原文标
数据表标题可调整位置;靠近其他框架时,会吸附到其边缘和框架间的标准间距。
拖动时按住 <kbd>Alt</kbd> 可暂时绕过吸附。视野适配与移动操作见
[导航](/zh-cn/reference/shortcuts/#导航)。
屏幕上的曲线和等高线细节会随显示尺寸调整。检查精细结构时请放大视图;
图形导出不受这种屏幕简化影响。
- **数据工作表(Data sheet)**——停靠在画板下方、用于编辑表格的面板,画板会相应
变矮。拖动其上边缘可调整高度;标题栏可折叠、最大化或关闭它。参见
[表格](/zh-cn/guides/tables/#编辑)。
Expand Down
Loading