mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-30 22:06:49 -06:00
PNG export (and everything else drawn by raster::render) stroked every polyline primitive with its own tiny-skia stroke_path call: a new path and a newly built paint pipeline per primitive. Hatch patterns, linetype dashes and text strokes produce long runs of short polylines in one colour, so that per-call setup dominated the render. Consecutive hairline polylines (zero width, or at most one pixel when anti-aliased; the default for PNG export) with the same colour, alpha and width are now collected into one path and stroked once. tiny-skia draws hairlines segment by segment straight onto the pixmap with the same per-contour round caps, so the pixels and their blending order are unchanged. Thicker strokes (filled outlines), fills, points, infinite lines and polylines with non-finite or far-out screen coordinates break the run and are drawn exactly as before. Adds crates/io/examples/export_bench.rs, which exports examples/ apartment.dxf repeated on a grid (10x10: 5,800 entities) and reports median/min wall time per format. PNG export, 10x10 grid, 11 interleaved runs (Ryzen, 16 threads, Linux): before: median 1790.7 ms, min 1703.3 ms after: median 1037.6 ms, min 926.0 ms (-42.1 %) A/A noise on the same setup: 1.1 %. Output PNGs are byte-identical (bench at 10x10 and 20x20, CLI export of apartment.dxf and a 23,200- entity drawing). New tests compare render() against stroking each primitive on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.1 KiB
Rust
80 lines
3.1 KiB
Rust
//! Export timing on a realistic large drawing: `examples/apartment.dxf` repeated on a grid
|
|
//! (a floor plan of many apartments).
|
|
//!
|
|
//! `cargo run --release -p cadcraft-io --example export_bench -- [FORMAT] [GRID] [RUNS] [OUT_DIR]`
|
|
//!
|
|
//! FORMAT is `png`, `pdf`, `svg`, `dxf` or `all` (default `png`), GRID the apartments per side
|
|
//! (default 10), RUNS the timed runs per format (default 7). Prints the median and minimum wall
|
|
//! time of each format's export (from the drawing in memory to the file's bytes). With OUT_DIR,
|
|
//! the last run's output is written there as `bench.FORMAT` to compare builds.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use cadcraft_doc::{Drawing, Space};
|
|
use cadcraft_geom::{Mat3, Vec2};
|
|
|
|
fn drawing(grid: usize) -> Drawing {
|
|
let src = include_bytes!("../../../examples/apartment.dxf");
|
|
let Ok(mut d) = cadcraft_io::read(src, "apartment.dxf") else {
|
|
eprintln!("apartment.dxf did not read");
|
|
std::process::exit(1);
|
|
};
|
|
let ents: Vec<_> = d.space(&Space::Model).map(|s| s.iter().map(|e| (**e).clone()).collect()).unwrap_or_default();
|
|
let ext = d.extents(&Space::Model);
|
|
let step = Vec2::new(ext.width() * 1.1, ext.height() * 1.1);
|
|
for i in 0..grid {
|
|
for j in 0..grid {
|
|
if (i, j) == (0, 0) {
|
|
continue;
|
|
}
|
|
let m = Mat3::translate(Vec2::new(step.x * j as f64, step.y * i as f64));
|
|
for e in &ents {
|
|
let mut e = e.clone();
|
|
e.handle = d.new_handle();
|
|
e.kind.transform(&m);
|
|
if let Some(s) = d.space_mut(&Space::Model) {
|
|
s.push(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
d
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let format = args.first().map(String::as_str).unwrap_or("png").to_string();
|
|
let grid: usize = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(10);
|
|
let runs: usize = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(7).max(1);
|
|
let out_dir = args.get(3);
|
|
let d = drawing(grid);
|
|
eprintln!("{} entities", d.entity_count());
|
|
let formats: Vec<&str> = if format == "all" { vec!["dxf", "png", "pdf", "svg"] } else { vec![format.as_str()] };
|
|
for f in formats {
|
|
let name = format!("bench.{f}");
|
|
let mut times: Vec<Duration> = Vec::new();
|
|
let mut last = Vec::new();
|
|
for _ in 0..runs {
|
|
let t = Instant::now();
|
|
match cadcraft_io::write(&d, &name) {
|
|
Ok(bytes) => {
|
|
times.push(t.elapsed());
|
|
last = bytes;
|
|
}
|
|
Err(e) => {
|
|
eprintln!("{f}: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
times.sort();
|
|
let ms = |t: Duration| t.as_secs_f64() * 1000.0;
|
|
println!("{f}: median {:.1} ms, min {:.1} ms, {} runs, {} bytes", ms(times[times.len() / 2]), ms(times[0]), runs, last.len());
|
|
if let Some(dir) = out_dir
|
|
&& let Err(e) = std::fs::write(std::path::Path::new(dir).join(&name), &last)
|
|
{
|
|
eprintln!("{name}: {e}");
|
|
}
|
|
}
|
|
}
|