perf: remove large drawing stalls
Keep resident geometry and GPU resources stable across edits, MSPACE zoom, and Model/Paper switches. Add unified PERF output for tracing remaining costs.
This commit is contained in:
parent
2bc4c1c8f3
commit
12bf126646
26 changed files with 794 additions and 325 deletions
|
|
@ -234,10 +234,11 @@ impl OpenCADStudio {
|
|||
// Toggle the per-rebuild wire-tessellation readout overlay.
|
||||
"PERF" => {
|
||||
self.perf_hud = !self.perf_hud;
|
||||
crate::perf::set_ui_enabled(self.perf_hud);
|
||||
self.command_line.push_info(if self.perf_hud {
|
||||
"PERF HUD on — shows last wire re-tessellation cost"
|
||||
"PERF panel on — tracing render and interaction costs"
|
||||
} else {
|
||||
"PERF HUD off"
|
||||
"PERF panel off"
|
||||
});
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ pub fn start_allowed(cmd: &str) -> bool {
|
|||
| "DONATE"
|
||||
| "WEBVERSION"
|
||||
| "HELP"
|
||||
| "PERF"
|
||||
| "CUI"
|
||||
| "ALIASEDIT"
|
||||
| "CUILOAD"
|
||||
|
|
|
|||
|
|
@ -460,9 +460,9 @@ impl OpenCADStudio {
|
|||
.filter(|h| scene.document.get_entity(*h).is_some())
|
||||
.collect();
|
||||
scene.selected = restored;
|
||||
// Do not call set_current_layout here: it bumps geometry immediately.
|
||||
// Entity deltas currently preserve the layout; direct assignment also
|
||||
// keeps future widened deltas batchable.
|
||||
// Entity deltas currently preserve the layout; direct assignment avoids
|
||||
// clearing layout render caches between steps and keeps future widened
|
||||
// deltas batchable.
|
||||
scene.current_layout = if undo {
|
||||
d.current_layout_before.clone()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -305,7 +305,8 @@ pub(super) struct OpenCADStudio {
|
|||
/// press-drag draws a rectangle marquee.
|
||||
pick_drag_rect: bool,
|
||||
/// Frame-budget HUD (Phase 5.3): overlays the last wire re-tessellation
|
||||
/// cost on the active viewport. Toggled by the `PERF` command.
|
||||
/// cost and the shared performance trace on the active viewport. Toggled
|
||||
/// by the `PERF` command.
|
||||
perf_hud: bool,
|
||||
/// When set, the cycling list box is open: (canvas point, candidates).
|
||||
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
|
||||
|
|
@ -1501,6 +1502,10 @@ pub enum Message {
|
|||
CommandHistoryCopy,
|
||||
/// Clear every line from the command-line history.
|
||||
CommandHistoryClear,
|
||||
/// Copy every line currently retained by the PERF panel.
|
||||
PerfCopy,
|
||||
/// Clear the PERF panel's retained trace.
|
||||
PerfClear,
|
||||
/// Text-editor action from the read-only history dropdown. Only
|
||||
/// non-editing actions (cursor moves, selection, scroll) are applied so
|
||||
/// the log stays read-only while remaining drag-selectable and copyable.
|
||||
|
|
|
|||
|
|
@ -819,8 +819,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
let clone_started = std::time::Instant::now();
|
||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
if crate::perf::enabled() {
|
||||
crate::perf_record!(
|
||||
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
||||
clone_ms,
|
||||
snapshot.entities().count(),
|
||||
|
|
@ -848,8 +848,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
png,
|
||||
viewport,
|
||||
);
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
if crate::perf::enabled() {
|
||||
crate::perf_record!(
|
||||
"[perf] save-thumbnail {:.1}ms wires={}",
|
||||
started.elapsed().as_secs_f64() * 1000.0,
|
||||
wires.len(),
|
||||
|
|
|
|||
|
|
@ -1010,6 +1010,20 @@ impl OpenCADStudio {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
Message::PerfCopy => {
|
||||
let text = crate::perf::snapshot_text();
|
||||
if text.is_empty() {
|
||||
Task::none()
|
||||
} else {
|
||||
iced::clipboard::write(text)
|
||||
}
|
||||
}
|
||||
|
||||
Message::PerfClear => {
|
||||
crate::perf::clear();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::CommandHistoryEdit(action) => {
|
||||
// Read-only: drop edits, keep selection / cursor / scroll so
|
||||
// the user can still highlight and Ctrl+C the log.
|
||||
|
|
@ -1681,8 +1695,8 @@ impl OpenCADStudio {
|
|||
epoch, source, wires, index,
|
||||
)
|
||||
});
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
if crate::perf::enabled() {
|
||||
crate::perf_record!(
|
||||
"[perf] interaction-index-bg {:>7.1}ms installed={installed}",
|
||||
build_ms,
|
||||
);
|
||||
|
|
@ -3729,6 +3743,8 @@ impl OpenCADStudio {
|
|||
|
||||
Message::EnterViewport(handle) => {
|
||||
let i = self.active_tab;
|
||||
let perf = crate::perf::enabled();
|
||||
let total = Instant::now();
|
||||
// Clear paper-space selection before entering model space.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.active_viewport = Some(handle);
|
||||
|
|
@ -3736,13 +3752,32 @@ impl OpenCADStudio {
|
|||
// centre so pan/zoom, paper↔model and the display all agree —
|
||||
// otherwise the camera auto-fits to the model while the cursor
|
||||
// math stays at the origin, jittering as pan toggles the two.
|
||||
let phase = Instant::now();
|
||||
self.tabs[i].scene.normalize_active_viewport_view();
|
||||
let normalize_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// Grid/snap follow the entered viewport.
|
||||
let phase = Instant::now();
|
||||
self.adopt_view_display(i);
|
||||
let display_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// Adopt the entered viewport's own per-viewport UCS.
|
||||
let phase = Instant::now();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
let ucs_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||
let phase = Instant::now();
|
||||
self.refresh_properties();
|
||||
let properties_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||
self.command_line.push_output("MSPACE");
|
||||
if perf {
|
||||
crate::perf_record!(
|
||||
"[perf] viewport-enter total={:.2}ms normalize={:.2}ms display={:.2}ms ucs={:.2}ms properties={:.2}ms handle={}",
|
||||
total.elapsed().as_secs_f64() * 1000.0,
|
||||
normalize_ms,
|
||||
display_ms,
|
||||
ucs_ms,
|
||||
properties_ms,
|
||||
handle.value(),
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ impl OpenCADStudio {
|
|||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let perf_move = std::env::var_os("OCS_PERF").is_some();
|
||||
let perf_move = crate::perf::enabled();
|
||||
let move_started = Instant::now();
|
||||
|
||||
// UCS icon grip drag: map the cursor onto the UCS plane and
|
||||
|
|
@ -606,6 +606,9 @@ impl OpenCADStudio {
|
|||
drop(sel);
|
||||
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
|
||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
} else if self.tabs[i].scene.current_layout == "Model" {
|
||||
|
|
@ -621,6 +624,9 @@ impl OpenCADStudio {
|
|||
drop(sel);
|
||||
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy, pivot);
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
|
||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
|
|
@ -654,6 +660,9 @@ impl OpenCADStudio {
|
|||
// drawing as the view pans under it (#234).
|
||||
self.reproject_box_anchor(i, vp_size.0, vp_size.1);
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Pan, move_started);
|
||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
|
|
@ -886,7 +895,7 @@ impl OpenCADStudio {
|
|||
// value and can monopolize the UI thread during a drag.
|
||||
let total_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf_move && total_ms >= 50.0 {
|
||||
eprintln!(
|
||||
crate::perf_record!(
|
||||
"[perf] grip-move {:>7.1}ms setup={:.1} snap={:.1} apply={:.1} preview={:.1} grips={:.1}",
|
||||
total_ms,
|
||||
setup_ms,
|
||||
|
|
@ -1453,9 +1462,18 @@ impl OpenCADStudio {
|
|||
|
||||
self.sync_dyn_fields();
|
||||
let move_ms = move_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf_move && self.tabs[i].active_cmd.is_some() && move_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] pointer-move {:>7.1}ms",
|
||||
if perf_move
|
||||
&& (self.tabs[i].active_cmd.is_some()
|
||||
|| self.tabs[i].scene.active_viewport.is_some())
|
||||
&& move_ms >= 16.7
|
||||
{
|
||||
let mode = if self.tabs[i].active_cmd.is_some() {
|
||||
"command"
|
||||
} else {
|
||||
"MSPACE"
|
||||
};
|
||||
crate::perf_record!(
|
||||
"[perf] pointer-move mode={mode:<7} {:>7.1}ms",
|
||||
move_ms,
|
||||
);
|
||||
}
|
||||
|
|
@ -3250,12 +3268,16 @@ impl OpenCADStudio {
|
|||
}
|
||||
if is_double {
|
||||
self.tabs[i].scene.fit_all();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Zoom, now);
|
||||
self.command_line.push_output("Zoom Extents");
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
||||
let nav_started = Instant::now();
|
||||
let s = match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. } => y,
|
||||
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
||||
|
|
@ -3310,6 +3332,9 @@ impl OpenCADStudio {
|
|||
// as the view zooms under it (#234).
|
||||
self.reproject_box_anchor(i, vw, vh);
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Zoom, nav_started);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -3460,7 +3485,7 @@ impl OpenCADStudio {
|
|||
{
|
||||
return Task::none();
|
||||
}
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let perf = crate::perf::enabled();
|
||||
let hover_started = Instant::now();
|
||||
let i = dwell.tab;
|
||||
// Re-check the gate — drag / command may have started
|
||||
|
|
@ -3564,10 +3589,10 @@ impl OpenCADStudio {
|
|||
self.hover_dwell = None;
|
||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && hover_ms >= 5.0 {
|
||||
eprintln!(
|
||||
crate::perf_record!(
|
||||
"[perf] hover-detail query={candidate_ms:.1} handles={handles_ms:.1} wire={wire_ms:.1} hatch={hatch_ms:.1} insert={insert_ms:.1} solid={solid_ms:.1} candidates={candidate_count}",
|
||||
);
|
||||
eprintln!(
|
||||
crate::perf_record!(
|
||||
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
|
||||
hover_ms,
|
||||
self.tabs[i].scene.last_tess_wires.get(),
|
||||
|
|
@ -3642,21 +3667,29 @@ impl OpenCADStudio {
|
|||
);
|
||||
return Task::none();
|
||||
}
|
||||
let perf = crate::perf::enabled();
|
||||
let perf_total = Instant::now();
|
||||
let perf_from = self.tabs[i].scene.current_layout.clone();
|
||||
let going_to_paper = name != "Model";
|
||||
// Persist the camera of the layout we're leaving BEFORE switching
|
||||
// so returning to it restores where the user left off (the
|
||||
// periodic sync only fires on a tick, which may not have run
|
||||
// since the last pan/zoom).
|
||||
let perf_phase = Instant::now();
|
||||
self.tabs[i].scene.sync_camera_to_document();
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
let sync_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// Cancel any pending rename/context-menu and active viewport when switching.
|
||||
self.layout_rename_state = None;
|
||||
self.layout_context_menu = None;
|
||||
self.tabs[i].scene.active_viewport = None;
|
||||
self.tabs[i].scene.set_current_layout(name);
|
||||
let perf_phase = Instant::now();
|
||||
self.tabs[i].scene.set_current_layout(name.clone());
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// UCS follows the pane: model header UCS in the Model tab, none
|
||||
// in plain paper space (a viewport's UCS is adopted on entry).
|
||||
let perf_phase = Instant::now();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
|
|
@ -3664,15 +3697,31 @@ impl OpenCADStudio {
|
|||
// sheet viewport in paper space, the model tile in model space)
|
||||
// so model and each layout keep independent grid state.
|
||||
self.adopt_view_display(i);
|
||||
let restore_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// Paper-space tools live in the right-edge side toolbar now, so
|
||||
// entering/leaving a layout no longer hijacks the ribbon tab.
|
||||
let _ = going_to_paper;
|
||||
// Refresh VP freeze columns for the new layout.
|
||||
let perf_phase = Instant::now();
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
let layers_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf {
|
||||
crate::perf_record!(
|
||||
"[perf] layout-switch from={} to={} total={:.2}ms sync={:.2}ms switch={:.2}ms restore={:.2}ms layers={:.2}ms epoch={}",
|
||||
perf_from,
|
||||
name,
|
||||
perf_total.elapsed().as_secs_f64() * 1000.0,
|
||||
sync_ms,
|
||||
switch_ms,
|
||||
restore_ms,
|
||||
layers_ms,
|
||||
self.tabs[i].scene.geometry_epoch,
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
|
|||
use crate::ui::wrap_bar::DensitySwap;
|
||||
use crate::ui::wrap_bar::WrapFlow;
|
||||
use iced::widget::{
|
||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, shader, stack, text,
|
||||
Row, Space,
|
||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader,
|
||||
stack, text, Row, Space,
|
||||
};
|
||||
use iced::window;
|
||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||
|
|
@ -1121,32 +1121,44 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
// Frame-budget HUD (Phase 5.3): toggle with the PERF command. Shows
|
||||
// the cost of the most recent wire re-tessellation — the work avoided
|
||||
// by a warm wire cache — so render-path changes can be compared
|
||||
// PR-to-PR. Reads ~0 ms while panning/zooming on a hit cache.
|
||||
if self.perf_hud && !tab.is_start {
|
||||
// Shared performance panel: terminal PERF lines plus the current
|
||||
// tessellation summary. Copy / Clear mirror the command-history panel.
|
||||
if self.perf_hud {
|
||||
let s = &tab.scene;
|
||||
let label = format!(
|
||||
let perf_w = if render_bar_w.is_finite() && render_bar_w > 1.0 {
|
||||
render_bar_w
|
||||
} else {
|
||||
320.0
|
||||
};
|
||||
let summary = format!(
|
||||
"tess {:.1} ms · {} wires · epoch {}",
|
||||
s.last_tess_ms.get(),
|
||||
s.last_tess_wires.get(),
|
||||
s.geometry_epoch,
|
||||
);
|
||||
let panel = container(text(label).size(12).color(Color {
|
||||
r: 0.6,
|
||||
g: 1.0,
|
||||
b: 0.6,
|
||||
a: 1.0,
|
||||
}))
|
||||
.padding(6)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
r: 0.08,
|
||||
g: 0.08,
|
||||
b: 0.08,
|
||||
a: 0.85,
|
||||
let trace = crate::perf::snapshot_text();
|
||||
let trace = if trace.is_empty() {
|
||||
"No samples yet".to_string()
|
||||
} else {
|
||||
trace
|
||||
};
|
||||
let perf_button_style = |_: &Theme, status: button::Status| button::Style {
|
||||
background: Some(Background::Color(if matches!(status, button::Status::Hovered) {
|
||||
Color {
|
||||
r: 0.24,
|
||||
g: 0.24,
|
||||
b: 0.24,
|
||||
a: 1.0,
|
||||
}
|
||||
} else {
|
||||
Color {
|
||||
r: 0.14,
|
||||
g: 0.14,
|
||||
b: 0.14,
|
||||
a: 1.0,
|
||||
}
|
||||
})),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color {
|
||||
r: 0.35,
|
||||
|
|
@ -1158,6 +1170,62 @@ impl OpenCADStudio {
|
|||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let copy_btn = button(
|
||||
row![
|
||||
crate::ui::icons::tinted(
|
||||
crate::ui::icons::COPY,
|
||||
11.0,
|
||||
Color::from_rgb(0.7, 0.85, 1.0),
|
||||
),
|
||||
text("Copy").size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::PerfCopy)
|
||||
.style(perf_button_style)
|
||||
.padding([2, 6]);
|
||||
let clear_btn = button(
|
||||
row![
|
||||
crate::ui::icons::tinted(
|
||||
crate::ui::icons::TRASH,
|
||||
11.0,
|
||||
Color::from_rgb(1.0, 0.55, 0.55),
|
||||
),
|
||||
text("Clear").size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::PerfClear)
|
||||
.style(perf_button_style)
|
||||
.padding([2, 6]);
|
||||
let header = row![
|
||||
text("PERF").size(12).color(Color::from_rgb(0.6, 1.0, 0.6)),
|
||||
Space::new().width(iced::Length::Fill),
|
||||
copy_btn,
|
||||
clear_btn,
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
let log = scrollable(text(trace).size(11).color(Color::from_rgb(0.8, 0.9, 0.8)))
|
||||
.height(iced::Length::Fixed(220.0))
|
||||
.width(iced::Length::Fill);
|
||||
let panel = container(
|
||||
column![
|
||||
header,
|
||||
text(summary).size(11).color(Color::from_rgb(0.6, 1.0, 0.6)),
|
||||
log,
|
||||
]
|
||||
.spacing(5),
|
||||
)
|
||||
.width(iced::Length::Fixed(perf_w))
|
||||
.padding(6)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: None,
|
||||
border: Border::default(),
|
||||
..Default::default()
|
||||
});
|
||||
viewport_stack = viewport_stack.push(position_canvas_overlay(
|
||||
iced::Point::new(12.0, 40.0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue