Add profiling capabilities (including GUI debug profiler)

This commit is contained in:
Mikolaj Wielgus 2026-06-07 03:33:41 +02:00
parent 8df8d760c3
commit a7fcf57a75
19 changed files with 206 additions and 41 deletions

View File

@ -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

View File

@ -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]

View File

@ -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,

View File

@ -34,6 +34,7 @@ impl Controller {
step_rate: Option<f64>,
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 {

View File

@ -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;
};

View File

@ -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])

View File

@ -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<Result<DsnFile, ParseErrorContext>>,
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);
}
}

View File

@ -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<puffin_http::Server> = 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!();
};
}

View File

@ -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();

View File

@ -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"

View File

@ -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)
}

View File

@ -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>() < 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>() < f64::exp(-delta_cost / params.temperature)
{
self.accept_move(board);
} else {
self.reject_move(board);
}
}
fn sample_move(&self, params: AutoplacerStepParams) -> Vector2<i64> {
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,

View File

@ -19,6 +19,7 @@ impl Board {
selection: &[ComponentId],
translation: Vector2<i64>,
) {
crate::profile_function!();
self.layout.move_components_by(selection, translation);
}
}

View File

@ -83,6 +83,7 @@ impl Pin {
impl Layout {
pub fn pin_centroid(&self, pin_id: PinId) -> Vector2<i64> {
crate::profile_function!();
let pin = self.pin(pin_id);
let mut sum = Vector2::new(0, 0);
let mut count = 0;

View File

@ -66,6 +66,7 @@ impl Polygon {
}
pub fn center(&self) -> Vector2<i64> {
crate::profile_function!();
Vector2::<i64>::polygon_centroid(&self.vertices)
}

View File

@ -13,6 +13,7 @@ impl Layout {
}
pub fn move_components_by(&mut self, ids: &[ComponentId], translation: Vector2<i64>) {
crate::profile_function!();
for id in ids {
let component = self.components[id.index()].clone();

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod profiler;
mod autoplacer;
mod autorouter;
pub mod board;

19
topola/src/profiler.rs Normal file
View File

@ -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!();
};
}

View File

@ -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() {