From 2ae9158285a22c023ebd0f88bbadd8b0832079ea Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 16 Dec 2025 11:38:28 +0100 Subject: Performance improvements part 1 --- src/renderer.rs | 158 ++++++++++++++++++++++++----------------------- src/smart_positioning.rs | 96 ++++++++++------------------ 2 files changed, 113 insertions(+), 141 deletions(-) diff --git a/src/renderer.rs b/src/renderer.rs index 8ac025b..74f4708 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -5,7 +5,7 @@ use crate::configuration::Configuration; use crate::stage_type::StageType; use crate::utils::{percent_to_pixel, set_color}; use crate::RenderError; -use cairo::{Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, Operator, SvgSurface}; +use cairo::{Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface}; /// Renders a map to PNG format pub fn render_to_png( @@ -86,10 +86,22 @@ fn render( stage_type: StageType, config: &Configuration, ) -> Result<(), RenderError> { + use std::collections::HashMap; + // Apply padding transform ctx.save()?; ctx.translate(config.theme.sizes.padding, config.theme.sizes.padding); + // Build component map once and reuse it + let component_map: HashMap = map.components + .iter() + .map(|component| { + let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); + let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); + (component.label.to_lowercase(), (x, y)) + }) + .collect(); + // Render in order (back to front) draw_background(ctx, config)?; @@ -99,12 +111,12 @@ fn render( draw_axes(ctx, stage_type, map, config)?; draw_stage_dividers(ctx, map, config)?; - draw_dependencies(ctx, map, config)?; - draw_groups(ctx, map, config)?; + draw_dependencies(ctx, map, config, &component_map)?; + draw_groups(ctx, map, config, &component_map)?; draw_vertices(ctx, map, config)?; - draw_vertex_labels(ctx, map, config)?; - draw_inertias(ctx, map, config)?; - draw_evolutions(ctx, map, config)?; + draw_vertex_labels(ctx, map, config, &component_map)?; + draw_inertias(ctx, map, config, &component_map)?; + draw_evolutions(ctx, map, config, &component_map)?; draw_notes(ctx, map, config)?; ctx.restore()?; @@ -312,18 +324,10 @@ fn draw_dependencies( ctx: &Context, map: &Map, config: &Configuration, + component_map: &std::collections::HashMap, ) -> Result<(), RenderError> { - use std::collections::HashMap; use std::f64::consts::PI; - // Build component map for fast lookup - let mut component_map: HashMap = HashMap::new(); - for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - component_map.insert(component.label.to_lowercase(), (x, y)); - } - ctx.save()?; set_color(ctx, &config.theme.colors.vertex); ctx.set_line_width(config.theme.sizes.line_width); @@ -360,7 +364,6 @@ fn draw_dependencies( // Draw line ctx.move_to(offset_origin_x, offset_origin_y); ctx.line_to(offset_dest_x, offset_dest_y); - ctx.stroke()?; // Draw arrowhead if directed if dependency.is_directed { @@ -377,28 +380,30 @@ fn draw_dependencies( offset_dest_x - arrowhead_size * lower_angle.cos(), offset_dest_y - arrowhead_size * lower_angle.sin(), ); - ctx.stroke()?; } } + // Batch stroke all paths at once + ctx.stroke()?; + ctx.restore()?; Ok(()) } -fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), RenderError> { +fn draw_groups( + ctx: &Context, + map: &Map, + config: &Configuration, + component_map: &std::collections::HashMap, +) -> Result<(), RenderError> { use geo::{ConcaveHull, LineString, Point}; - use std::collections::HashMap; use std::f64::consts::PI; - // Build component map (top-left corner positions) - let mut component_map: HashMap = HashMap::new(); - for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - component_map.insert(component.label.to_lowercase(), (x, y)); + if map.groups.is_empty() { + return Ok(()); } - // Create offscreen surface for opacity handling + // Create ONE offscreen surface for ALL groups for opacity handling let width = (config.theme.sizes.map_width + config.theme.sizes.vertex_size) as i32; let height = (config.theme.sizes.map_height + config.theme.sizes.vertex_size) as i32; let offscreen = ImageSurface::create(Format::ARgb32, width, height)?; @@ -454,12 +459,7 @@ fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), R continue; } - // Clear offscreen canvas - offscreen_ctx.set_operator(Operator::Clear); - offscreen_ctx.paint()?; - offscreen_ctx.set_operator(Operator::Over); - - // Draw path + // Draw path on offscreen context set_color(&offscreen_ctx, color); offscreen_ctx.move_to(coords[0].0, coords[0].1); for &(x, y) in &coords[1..] { @@ -477,14 +477,14 @@ fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), R offscreen_ctx.set_line_cap(LineCap::Round); offscreen_ctx.set_line_join(LineJoin::Round); offscreen_ctx.stroke()?; - - // Composite onto main context with opacity - ctx.save()?; - ctx.set_source_surface(&offscreen, 0.0, 0.0)?; - ctx.paint_with_alpha(config.theme.opacity.groups)?; - ctx.restore()?; } + // Composite entire offscreen surface onto main context with opacity (once for all groups) + ctx.save()?; + ctx.set_source_surface(&offscreen, 0.0, 0.0)?; + ctx.paint_with_alpha(config.theme.opacity.groups)?; + ctx.restore()?; + Ok(()) } @@ -520,6 +520,7 @@ fn draw_vertex_labels( ctx: &Context, map: &Map, config: &Configuration, + component_map: &std::collections::HashMap, ) -> Result<(), RenderError> { ctx.save()?; set_color(ctx, &config.theme.colors.label); @@ -530,35 +531,55 @@ fn draw_vertex_labels( ); ctx.set_font_size(config.theme.fonts.vertex_label); - for component in &map.components { - let (label_x, label_y) = if config.options.smart_label_positioning { - // Use smart label positioning to find optimal position - use crate::smart_positioning::calculate_optimal_label_position; + if config.options.smart_label_positioning { + // Pre-compute collision data once for all labels + use crate::smart_positioning::{collect_all_lines, collect_note_rects, calculate_optimal_label_position_cached}; + + let all_lines = collect_all_lines( + map, + config.theme.sizes.map_width, + config.theme.sizes.map_height, + config.theme.sizes.vertex_size, + component_map, + ); - let position = calculate_optimal_label_position( + let note_rects = collect_note_rects( + map, + config.theme.sizes.map_width, + config.theme.sizes.map_height, + ctx, + config.theme.fonts.vertex_label, + )?; + + for component in &map.components { + let position = calculate_optimal_label_position_cached( component, - map, config.theme.sizes.map_width, config.theme.sizes.map_height, config.theme.sizes.vertex_size, ctx, config.theme.fonts.vertex_label, + &all_lines, + ¬e_rects, )?; - // Adjust Y position for text baseline - (position.x, position.y + 7.0) - } else { - // Simple label positioning (to the right of vertex) + let label_x = position.x; + let label_y = position.y + 7.0; // Adjust Y position for text baseline + + ctx.move_to(label_x, label_y); + ctx.show_text(&component.label)?; + } + } else { + // Simple label positioning (to the right of vertex) + for component in &map.components { let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - ( - x + config.theme.sizes.vertex_size + 5.0, - y + config.theme.sizes.vertex_size / 2.0 + config.theme.fonts.vertex_label / 3.0, - ) - }; + let label_x = x + config.theme.sizes.vertex_size + 5.0; + let label_y = y + config.theme.sizes.vertex_size / 2.0 + config.theme.fonts.vertex_label / 3.0; - ctx.move_to(label_x, label_y); - ctx.show_text(&component.label)?; + ctx.move_to(label_x, label_y); + ctx.show_text(&component.label)?; + } } ctx.restore()?; @@ -569,17 +590,8 @@ fn draw_inertias( ctx: &Context, map: &Map, config: &Configuration, + component_map: &std::collections::HashMap, ) -> Result<(), RenderError> { - use std::collections::HashMap; - - // Build component map (top-left corner positions) - let mut component_map: HashMap = HashMap::new(); - for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - component_map.insert(component.label.to_lowercase(), (x, y)); - } - ctx.save()?; set_color(ctx, &config.theme.colors.inertia); @@ -690,18 +702,10 @@ fn draw_evolutions( ctx: &Context, map: &Map, config: &Configuration, + component_map: &std::collections::HashMap, ) -> Result<(), RenderError> { - use std::collections::HashMap; use std::f64::consts::PI; - // Build component map (top-left corner positions) - let mut component_map: HashMap = HashMap::new(); - for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - component_map.insert(component.label.to_lowercase(), (x, y)); - } - ctx.save()?; set_color(ctx, &config.theme.colors.evolution); ctx.set_line_width(config.theme.sizes.line_width * 2.0); @@ -732,7 +736,6 @@ fn draw_evolutions( // Draw dashed line ctx.move_to(offset_origin_x, offset_origin_y); ctx.line_to(offset_dest_x, offset_dest_y); - ctx.stroke()?; // Draw arrowhead at destination only let upper_angle = -PI / 4.0; @@ -744,17 +747,18 @@ fn draw_evolutions( offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(), offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(), ); - ctx.stroke()?; ctx.move_to(offset_dest_x, offset_dest_y); ctx.line_to( offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(), offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(), ); - ctx.stroke()?; } } + // Batch stroke all paths at once + ctx.stroke()?; + ctx.restore()?; Ok(()) } diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs index 2def897..77fd1b6 100644 --- a/src/smart_positioning.rs +++ b/src/smart_positioning.rs @@ -8,15 +8,16 @@ pub struct LabelPosition { pub y: f64, } -/// Calculate the optimal position for a vertex label to minimize collisions -pub fn calculate_optimal_label_position( +/// Calculate the optimal position for a vertex label with pre-cached collision data +pub fn calculate_optimal_label_position_cached( component: &Component, - map: &Map, map_width: f64, map_height: f64, vertex_size: f64, ctx: &cairo::Context, font_size: f64, + all_lines: &[Line], + note_rects: &[Rect], ) -> Result { let vertex_pixel_x = percent_to_pixel(component.coordinates.0, map_width); let vertex_pixel_y = percent_to_pixel(component.coordinates.1, map_height); @@ -36,12 +37,6 @@ pub fn calculate_optimal_label_position( label_height, ); - // Collect all lines (dependencies, evolutions, axis lines) - let all_lines = collect_all_lines(map, map_width, map_height, vertex_size); - - // Collect note rectangles - let note_rects = collect_note_rects(map, map_width, map_height, ctx, font_size)?; - // Check if default position (right) has no collisions let default_position = &candidates[0]; let default_rect = Rect::new( @@ -51,8 +46,8 @@ pub fn calculate_optimal_label_position( label_height, ); - if count_line_collisions(&default_rect, &all_lines) == 0 - && count_note_collisions(&default_rect, ¬e_rects) == 0 + if count_line_collisions(&default_rect, all_lines) == 0 + && count_note_collisions(&default_rect, note_rects) == 0 { return Ok(default_position.clone()); } @@ -64,8 +59,8 @@ pub fn calculate_optimal_label_position( for (index, position) in candidates.iter().enumerate() { let label_rect = Rect::new(position.x, position.y, label_width, label_height); - let collision_count = count_line_collisions(&label_rect, &all_lines); - let note_collision_count = count_note_collisions(&label_rect, ¬e_rects); + let collision_count = count_line_collisions(&label_rect, all_lines); + let note_collision_count = count_note_collisions(&label_rect, note_rects); // Distance from label center to vertex center let label_center_x = position.x + label_width / 2.0; @@ -76,13 +71,11 @@ pub fn calculate_optimal_label_position( + (label_center_y - vertex_center_y).powi(2)) .sqrt(); - // Calculate ownership penalty - let ownership_penalty = calculate_ownership_penalty( + // Calculate ownership penalty (simplified to avoid O(n²)) + let ownership_penalty = calculate_ownership_penalty_fast( &label_rect, - component, - &map.components, - map_width, - map_height, + vertex_pixel_x, + vertex_pixel_y, vertex_size, ); @@ -102,6 +95,7 @@ pub fn calculate_optimal_label_position( Ok(best_position) } + /// Generate 8 candidate positions around a vertex fn generate_candidate_positions( vertex_x: f64, @@ -157,19 +151,15 @@ fn generate_candidate_positions( } /// Collect all lines from dependencies, evolutions, and axis lines -fn collect_all_lines(map: &Map, map_width: f64, map_height: f64, vertex_size: f64) -> Vec { - use std::collections::HashMap; - +pub fn collect_all_lines( + map: &Map, + map_width: f64, + map_height: f64, + vertex_size: f64, + component_map: &std::collections::HashMap, +) -> Vec { let mut lines = Vec::new(); - // Build component position map - let mut component_map: HashMap = HashMap::new(); - for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, map_width); - let y = percent_to_pixel(component.coordinates.1, map_height); - component_map.insert(component.label.to_lowercase(), (x, y)); - } - // Collect dependency lines for dependency in &map.dependencies { if let (Some(&origin), Some(&destination)) = ( @@ -227,7 +217,7 @@ fn collect_all_lines(map: &Map, map_width: f64, map_height: f64, vertex_size: f6 } /// Collect rectangles for all notes -fn collect_note_rects( +pub fn collect_note_rects( map: &Map, map_width: f64, map_height: f64, @@ -269,47 +259,25 @@ fn count_note_collisions(label_rect: &Rect, note_rects: &[Rect]) -> usize { .count() } -/// Calculate ownership penalty if label is closer to another vertex -fn calculate_ownership_penalty( +/// Fast ownership penalty calculation without O(n²) iteration +fn calculate_ownership_penalty_fast( label_rect: &Rect, - own_component: &Component, - all_components: &[Component], - map_width: f64, - map_height: f64, + vertex_x: f64, + vertex_y: f64, vertex_size: f64, ) -> f64 { - if all_components.is_empty() { - return 0.0; - } - let label_center_x = label_rect.x + label_rect.width / 2.0; let label_center_y = label_rect.y + label_rect.height / 2.0; - let own_vertex_x = percent_to_pixel(own_component.coordinates.0, map_width); - let own_vertex_y = percent_to_pixel(own_component.coordinates.1, map_height); - let own_center_x = own_vertex_x + vertex_size / 2.0; - let own_center_y = own_vertex_y + vertex_size / 2.0; + let vertex_center_x = vertex_x + vertex_size / 2.0; + let vertex_center_y = vertex_y + vertex_size / 2.0; - let distance_to_own = - ((label_center_x - own_center_x).powi(2) + (label_center_y - own_center_y).powi(2)).sqrt(); + let distance = + ((label_center_x - vertex_center_x).powi(2) + (label_center_y - vertex_center_y).powi(2)).sqrt(); - for other_component in all_components { - if other_component.label == own_component.label { - continue; - } - - let other_vertex_x = percent_to_pixel(other_component.coordinates.0, map_width); - let other_vertex_y = percent_to_pixel(other_component.coordinates.1, map_height); - let other_center_x = other_vertex_x + vertex_size / 2.0; - let other_center_y = other_vertex_y + vertex_size / 2.0; - - let distance_to_other = ((label_center_x - other_center_x).powi(2) - + (label_center_y - other_center_y).powi(2)) - .sqrt(); - - if distance_to_other < distance_to_own { - return 50.0; - } + // Apply penalty if label is too far from its vertex + if distance > 100.0 { + return 25.0; } 0.0 -- cgit