diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 538d684..f013cd7 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -51,6 +51,7 @@ tr-menu-debug-show-bboxes = Show BBoxes tr-menu-debug-show-primitive-indices = Show Primitive Indices tr-menu-debug-show-bend-endpoint-tangents = Show Bend Endpoint Tangents tr-menu-debug-fix-step-rate = Fix Step Rate +tr-menu-debug-profiler = Profiler tr-menu-debug-step-rate = Step Rate tr-menu-debug-step-rate-unit = steps/s diff --git a/topola-egui/Cargo.toml b/topola-egui/Cargo.toml index 9a6e6be..d91495e 100644 --- a/topola-egui/Cargo.toml +++ b/topola-egui/Cargo.toml @@ -11,6 +11,7 @@ edition = "2024" [features] default = ["xdg-portal"] gtk3 = ["rfd/gtk3"] +profiler = ["puffin", "puffin_egui", "puffin_http", "topola/profiler"] xdg-portal = ["rfd/xdg-portal"] [package.metadata.docs.rs] @@ -41,6 +42,9 @@ serde = { version = "1.0", features = ["derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-lite = "2.6" env_logger = "0.11" +puffin = { version = "0.20", optional = true } +puffin_egui = { version = "0.30", optional = true } +puffin_http = { version = "0.17", optional = true } # Web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/topola-egui/src/app.rs b/topola-egui/src/app.rs index 03c9999..be326ee 100644 --- a/topola-egui/src/app.rs +++ b/topola-egui/src/app.rs @@ -125,6 +125,9 @@ impl eframe::App for App { /// Called each time the UI has to be repainted. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + crate::profiler::begin_frame(); + crate::profile_function!(); + self.menu_bar.update( ctx, &mut self.translator, diff --git a/topola-egui/src/controller.rs b/topola-egui/src/controller.rs index 4ad1510..bdcab14 100644 --- a/topola-egui/src/controller.rs +++ b/topola-egui/src/controller.rs @@ -34,6 +34,7 @@ impl Controller { step_rate: Option, dt: f64, ) -> bool { + crate::profile_function!(); let instant = Instant::now(); if step_rate.is_some() { @@ -61,17 +62,18 @@ impl Controller { } pub fn step(&mut self, _tr: &Translator) -> ControlFlow<()> { + crate::profile_function!(); self.master_interactor.step(self.workspace.board_mut()) } pub fn update_appearance_panel(&mut self, ctx: &egui::Context) { + crate::profile_function!(); let Self { workspace, appearance_panel, master_interactor: _, dt_accum, } = self; - appearance_panel.update(ctx, workspace.board_mut()); } @@ -84,6 +86,7 @@ impl Controller { scene_hovered: bool, ui: &mut egui::Ui, ) { + crate::profile_function!(); if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { let board_master = if let MasterInteractor::Autoplacer(interactor) = &self.master_interactor { diff --git a/topola-egui/src/display.rs b/topola-egui/src/display.rs index 379ff6a..f316caf 100644 --- a/topola-egui/src/display.rs +++ b/topola-egui/src/display.rs @@ -23,6 +23,8 @@ impl Display { viewport: &Viewport, workspace: &Controller, ) { + crate::profile_function!(); + self.display_layout(ctx, ui, /*menu_bar,*/ viewport, workspace); self.display_repulsions(ui, viewport, workspace); self.display_attractions(ui, viewport, workspace); @@ -40,6 +42,7 @@ impl Display { viewport: &Viewport, workspace: &Controller, ) { + crate::profile_function!(); let board = workspace.workspace.board(); let layout = board.layout(); @@ -162,6 +165,7 @@ impl Display { } fn display_repulsions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &Controller) { + crate::profile_function!(); let board = workspace.workspace.board(); let stroke = egui::Stroke::new(150.0 / viewport.scale_factor(), egui::Color32::YELLOW); @@ -203,6 +207,7 @@ impl Display { } fn display_retentions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &Controller) { + crate::profile_function!(); let board = workspace.workspace.board(); let layout = board.layout(); let stroke = egui::Stroke::new( @@ -244,6 +249,7 @@ impl Display { } fn display_attractions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &Controller) { + crate::profile_function!(); let board = workspace.workspace.board(); let layout = board.layout(); let stroke = egui::Stroke::new(150.0 / viewport.scale_factor(), egui::Color32::BLUE); @@ -375,6 +381,7 @@ impl Display { viewport: &Viewport, workspace: &Controller, ) { + crate::profile_function!(); let board = workspace.workspace.board(); let layout = board.layout(); @@ -452,6 +459,7 @@ impl Display { viewport: &Viewport, workspace: &Controller, ) { + crate::profile_function!(); let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else { return; }; @@ -522,6 +530,7 @@ impl Display { _viewport: &Viewport, workspace: &Controller, ) { + crate::profile_function!(); let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else { return; }; diff --git a/topola-egui/src/main.rs b/topola-egui/src/main.rs index 157008f..9f15bf2 100644 --- a/topola-egui/src/main.rs +++ b/topola-egui/src/main.rs @@ -11,6 +11,7 @@ mod controller; mod display; mod layers_panel; mod menu_bar; +mod profiler; mod translator; mod viewport; @@ -21,6 +22,8 @@ use crate::app::App; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + profiler::enable(); + let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([400.0, 300.0]) diff --git a/topola-egui/src/menu_bar.rs b/topola-egui/src/menu_bar.rs index 49fa01b..ee3d7bb 100644 --- a/topola-egui/src/menu_bar.rs +++ b/topola-egui/src/menu_bar.rs @@ -27,6 +27,8 @@ use crate::{ pub struct MenuBar { pub fix_step_rate: bool, pub step_rate: f64, + #[serde(default)] + pub show_profiler: bool, } impl MenuBar { @@ -34,6 +36,7 @@ impl MenuBar { Self { fix_step_rate: false, step_rate: 1.0, + show_profiler: false, } } @@ -44,6 +47,8 @@ impl MenuBar { content_sender: Sender>, controller: Option<&mut Controller>, ) { + crate::profile_function!(); + let mut actions = Actions::new(tr); egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { @@ -76,6 +81,15 @@ impl MenuBar { )), ); }); + + #[cfg(feature = "profiler")] + { + ui.separator(); + + if ui.button(tr.text("tr-menu-debug-profiler")).clicked() { + self.show_profiler = true; + } + } }); ui.separator(); @@ -118,5 +132,7 @@ impl MenuBar { } } }); + + crate::profiler::profiler_window(ctx, &mut self.show_profiler); } } diff --git a/topola-egui/src/profiler.rs b/topola-egui/src/profiler.rs new file mode 100644 index 0000000..79b87ee --- /dev/null +++ b/topola-egui/src/profiler.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] +static PUFFIN_SERVER: std::sync::OnceLock = std::sync::OnceLock::new(); + +#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] +pub fn enable() { + let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT); + PUFFIN_SERVER.get_or_init(|| { + eprintln!("Run this to view profiler data: puffin_viewer {server_addr}"); + puffin_http::Server::new(&server_addr).expect("puffin_http server") + }); + puffin::set_scopes_on(true); +} + +#[cfg(any(target_arch = "wasm32", not(feature = "profiler")))] +pub fn enable() {} + +#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] +pub fn begin_frame() { + puffin::GlobalProfiler::lock().new_frame(); +} + +#[cfg(any(target_arch = "wasm32", not(feature = "profiler")))] +pub fn begin_frame() {} + +#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] +pub fn profiler_window(ctx: &egui::Context, open: &mut bool) { + if *open { + *open = puffin_egui::profiler_window(ctx); + } +} + +#[cfg(any(target_arch = "wasm32", not(feature = "profiler")))] +pub fn profiler_window(_ctx: &egui::Context, _open: &mut bool) {} + +#[macro_export] +macro_rules! profile_scope { + ($name:expr) => { + #[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] + puffin::profile_scope!($name); + }; +} + +#[macro_export] +macro_rules! profile_function { + () => { + #[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] + puffin::profile_function!(); + }; +} diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index 369be97..6453ed5 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -28,6 +28,8 @@ impl Viewport { menu_bar: &MenuBar, controller: Option<&mut Controller>, ) { + crate::profile_function!(); + egui::CentralPanel::default().show(ctx, |ui| { egui::Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); diff --git a/topola/Cargo.toml b/topola/Cargo.toml index 3573f7b..dd5aff7 100644 --- a/topola/Cargo.toml +++ b/topola/Cargo.toml @@ -8,6 +8,9 @@ description = "Work-in-progress free and open-source topological (rubberband) ro version = "0.1.0" edition = "2024" +[features] +profiler = ["puffin"] + [dependencies] bidimap = "0.7" dearcut = { version = "0.3", features = ["serde", "undoredo"] } @@ -24,6 +27,7 @@ spade = "2.15" specctra = { path = "../specctra" } stable-vec = "0.4" undoredo.workspace = true +puffin = { version = "0.20", optional = true } [dev-dependencies] walkdir = "2.5" diff --git a/topola/src/autoplacer/interactors/master.rs b/topola/src/autoplacer/interactors/master.rs index 9974dfa..95411fc 100644 --- a/topola/src/autoplacer/interactors/master.rs +++ b/topola/src/autoplacer/interactors/master.rs @@ -49,6 +49,7 @@ impl AutoplacerMasterInteractor { impl Interactor for AutoplacerMasterInteractor { fn step(&mut self, board: &mut Board) -> ControlFlow<()> { + crate::profile_function!(); self.autoplacer.step(board) } diff --git a/topola/src/autoplacer/mod.rs b/topola/src/autoplacer/mod.rs index a3608be..4ab335d 100644 --- a/topola/src/autoplacer/mod.rs +++ b/topola/src/autoplacer/mod.rs @@ -12,7 +12,7 @@ use undoredo::{FlushDelta, ResetDelta}; use crate::{ board::{Board, BoardDelta}, - layout::compounds::ComponentId, + layout::{Layout, compounds::ComponentId}, orientation::Orientation, selections::ComponentSelection, vector::Vector2, @@ -54,6 +54,8 @@ impl Autoplacer { } pub fn step(&mut self, board: &mut Board) -> ControlFlow<()> { + crate::profile_function!(); + if self.step_counter < self.schedule.max_steps { let control_flow = self.step_with_params( board, @@ -88,37 +90,59 @@ impl Autoplacer { board: &mut Board, params: AutoplacerStepParams, ) -> ControlFlow<()> { - for &component in self.components.iter() { - //self.step_component_with_params(component, params); - //let last_cost = self.cost(board, params); - let last_cost = self.component_cost(board, component, params); + crate::profile_function!(); - let dx_gaussian = Normal::new(0.0, params.std_dev).unwrap(); - let dy_gaussian = Normal::new(0.0, params.std_dev).unwrap(); - - //let dx = dx_gaussian.sample(&mut self.rng); - //let dy = dy_gaussian.sample(&mut self.rng); - let dx = dx_gaussian.sample(&mut rand::rng()); - let dy = dy_gaussian.sample(&mut rand::rng()); - - board.move_resolved_components_by(&[component], Vector2::new(dx as i64, dy as i64)); - - //let new_cost = self.cost(board, params); - let new_cost = self.component_cost(board, component, params); - let delta_cost = new_cost - last_cost; - - if delta_cost < 0.0 - || rand::rng().random::() < f64::exp(-delta_cost / params.temperature) - { - self.origin_delta = self.origin_delta.clone().merge_delta(board.flush_delta()); - } else { - board.reset_delta(); - } + for i in 0..self.components.len() { + let component = self.components[i]; + self.step_component(board, component, params); } ControlFlow::Continue(()) } + fn step_component( + &mut self, + board: &mut Board, + component: ComponentId, + params: AutoplacerStepParams, + ) { + crate::profile_function!(); + + let last_cost = self.component_cost(board, component, params); + let translation = self.sample_move(params); + board.move_resolved_components_by(&[component], translation); + let new_cost = self.component_cost(board, component, params); + let delta_cost = new_cost - last_cost; + + if delta_cost < 0.0 + || rand::rng().random::() < f64::exp(-delta_cost / params.temperature) + { + self.accept_move(board); + } else { + self.reject_move(board); + } + } + + fn sample_move(&self, params: AutoplacerStepParams) -> Vector2 { + crate::profile_function!(); + let dx_gaussian = Normal::new(0.0, params.std_dev).unwrap(); + let dy_gaussian = Normal::new(0.0, params.std_dev).unwrap(); + Vector2::new( + dx_gaussian.sample(&mut rand::rng()) as i64, + dy_gaussian.sample(&mut rand::rng()) as i64, + ) + } + + fn accept_move(&mut self, board: &mut Board) { + crate::profile_function!(); + self.origin_delta = self.origin_delta.clone().merge_delta(board.flush_delta()); + } + + fn reject_move(&mut self, board: &mut Board) { + crate::profile_function!(); + board.reset_delta(); + } + /*fn cost(&self, board: &Board, params: AutoplacerStepParams) -> f64 { self.components .iter() @@ -130,26 +154,42 @@ impl Autoplacer { &self, board: &Board, component: ComponentId, - params: AutoplacerStepParams, + _params: AutoplacerStepParams, ) -> f64 { - let layout = board.layout(); + crate::profile_function!(); - let repulsion_cost: i64 = layout - .locate_component_repulsions(component, Orientation::Oblique) - .map(|vector| vector.x.abs() + vector.y.abs()) - .sum(); - let attraction_cost: f64 = layout - .component_attractions(component) - .map(|vector| 1.0 / (1.0 + (vector.x.abs() + vector.y.abs()) as f64)) - .sum(); - let retention_cost: i64 = layout - .component_retentions(component) - .map(|vector| 100 * (vector.x.abs() + vector.y.abs())) - .sum(); + let layout = board.layout(); + let repulsion_cost = self.repulsion_cost(layout, component); + let attraction_cost = self.attraction_cost(layout, component); + let retention_cost = self.retention_cost(layout, component); repulsion_cost as f64 + attraction_cost + retention_cost as f64 } + fn repulsion_cost(&self, layout: &Layout, component: ComponentId) -> i64 { + crate::profile_function!(); + layout + .locate_component_repulsions(component, Orientation::Oblique) + .map(|vector| vector.x.abs() + vector.y.abs()) + .sum() + } + + fn attraction_cost(&self, layout: &Layout, component: ComponentId) -> f64 { + crate::profile_function!(); + layout + .component_attractions(component) + .map(|vector| 1.0 / (1.0 + (vector.x.abs() + vector.y.abs()) as f64)) + .sum() + } + + fn retention_cost(&self, layout: &Layout, component: ComponentId) -> i64 { + crate::profile_function!(); + layout + .component_retentions(component) + .map(|vector| 100 * (vector.x.abs() + vector.y.abs())) + .sum() + } + /*fn step_component_with_params( &mut self, component: ComponentId, diff --git a/topola/src/board/transforms/move_by.rs b/topola/src/board/transforms/move_by.rs index 6660245..e52b5a5 100644 --- a/topola/src/board/transforms/move_by.rs +++ b/topola/src/board/transforms/move_by.rs @@ -19,6 +19,7 @@ impl Board { selection: &[ComponentId], translation: Vector2, ) { + crate::profile_function!(); self.layout.move_components_by(selection, translation); } } diff --git a/topola/src/layout/compounds/pin.rs b/topola/src/layout/compounds/pin.rs index 79007ac..631f771 100644 --- a/topola/src/layout/compounds/pin.rs +++ b/topola/src/layout/compounds/pin.rs @@ -83,6 +83,7 @@ impl Pin { impl Layout { pub fn pin_centroid(&self, pin_id: PinId) -> Vector2 { + crate::profile_function!(); let pin = self.pin(pin_id); let mut sum = Vector2::new(0, 0); let mut count = 0; diff --git a/topola/src/layout/primitives/polygon.rs b/topola/src/layout/primitives/polygon.rs index e8f63bd..11a46d0 100644 --- a/topola/src/layout/primitives/polygon.rs +++ b/topola/src/layout/primitives/polygon.rs @@ -66,6 +66,7 @@ impl Polygon { } pub fn center(&self) -> Vector2 { + crate::profile_function!(); Vector2::::polygon_centroid(&self.vertices) } diff --git a/topola/src/layout/transforms/move_by.rs b/topola/src/layout/transforms/move_by.rs index 867cd02..02ed98e 100644 --- a/topola/src/layout/transforms/move_by.rs +++ b/topola/src/layout/transforms/move_by.rs @@ -13,6 +13,7 @@ impl Layout { } pub fn move_components_by(&mut self, ids: &[ComponentId], translation: Vector2) { + crate::profile_function!(); for id in ids { let component = self.components[id.index()].clone(); diff --git a/topola/src/lib.rs b/topola/src/lib.rs index 050e948..00af393 100644 --- a/topola/src/lib.rs +++ b/topola/src/lib.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 +mod profiler; + mod autoplacer; mod autorouter; pub mod board; diff --git a/topola/src/profiler.rs b/topola/src/profiler.rs new file mode 100644 index 0000000..3295423 --- /dev/null +++ b/topola/src/profiler.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[macro_export] +macro_rules! profile_scope { + ($name:expr) => { + #[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] + puffin::profile_scope!($name); + }; +} + +#[macro_export] +macro_rules! profile_function { + () => { + #[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))] + puffin::profile_function!(); + }; +} diff --git a/topola/src/vector.rs b/topola/src/vector.rs index f59789b..19cc731 100644 --- a/topola/src/vector.rs +++ b/topola/src/vector.rs @@ -247,6 +247,7 @@ macro_rules! impl_polygon_centroid { ($type:ty) => { impl Vector2<$type> { pub fn polygon_centroid(polygon: &[Vector2<$type>]) -> Self { + crate::profile_function!(); let mut sum = Vector2::new(0 as $type, 0 as $type); for vertex in polygon.iter() {