mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
Merge pull request #4359 from Silvertree2010/perf/export-speed
Make SVG export def ids linear instead of quadratic
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
//! `cargo run --release -p vectorcraft-engine --example export_bench -- [FORMAT] [FILE…]` — wall
|
||||||
|
//! time of `document.export` (default SVG) for each file (default: the documents in `examples/`).
|
||||||
|
//! `ITERS` sets the runs per file (default 7); prints the median and the fastest run.
|
||||||
|
// Example (dev tool): unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||||||
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use vectorcraft_engine::Session;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let format = args.next().unwrap_or_else(|| "svg".into());
|
||||||
|
let mut files: Vec<PathBuf> = args.map(PathBuf::from).collect();
|
||||||
|
if files.is_empty() {
|
||||||
|
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples");
|
||||||
|
files = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().path()).filter(|p| p.extension().is_some_and(|e| e == "vectorcraft")).collect();
|
||||||
|
files.sort();
|
||||||
|
}
|
||||||
|
let iters: usize = std::env::var("ITERS").ok().and_then(|v| v.parse().ok()).unwrap_or(7).max(1);
|
||||||
|
let out_dir = std::env::temp_dir().join(format!("vectorcraft-export-bench-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&out_dir).unwrap();
|
||||||
|
let mut total = 0.0;
|
||||||
|
for file in &files {
|
||||||
|
let mut s = Session::new();
|
||||||
|
s.execute("document.open", &json!({ "path": file.to_string_lossy() })).unwrap();
|
||||||
|
let name = file.file_stem().unwrap().to_string_lossy().into_owned();
|
||||||
|
let out = out_dir.join(format!("{name}.{format}"));
|
||||||
|
let p = json!({ "path": out.to_string_lossy(), "format": format });
|
||||||
|
let mut ms: Vec<f64> = (0..iters)
|
||||||
|
.map(|_| {
|
||||||
|
let t = Instant::now();
|
||||||
|
s.execute("document.export", &p).unwrap();
|
||||||
|
t.elapsed().as_secs_f64() * 1000.0
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
ms.sort_by(f64::total_cmp);
|
||||||
|
let median = ms[ms.len() / 2];
|
||||||
|
total += median;
|
||||||
|
let bytes = std::fs::metadata(&out).map(|m| m.len()).unwrap_or(0);
|
||||||
|
println!("{name}.{format}: median {median:.1} ms, min {:.1} ms ({bytes} bytes)", ms[0]);
|
||||||
|
}
|
||||||
|
println!("total of medians: {total:.1} ms");
|
||||||
|
std::fs::remove_dir_all(&out_dir).ok();
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
|
|||||||
class_ids: HashMap::new(),
|
class_ids: HashMap::new(),
|
||||||
used_ids: HashSet::new(),
|
used_ids: HashSet::new(),
|
||||||
next_suffix: HashMap::new(),
|
next_suffix: HashMap::new(),
|
||||||
|
next_fresh: HashMap::new(),
|
||||||
linked_names: HashSet::new(),
|
linked_names: HashSet::new(),
|
||||||
names: HashMap::new(),
|
names: HashMap::new(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
@@ -207,6 +208,8 @@ struct Writer<'a> {
|
|||||||
used_ids: HashSet<String>,
|
used_ids: HashSet<String>,
|
||||||
/// The next suffix to try for a base name, so a repeated name doesn't probe the ones taken again.
|
/// The next suffix to try for a base name, so a repeated name doesn't probe the ones taken again.
|
||||||
next_suffix: HashMap<String, usize>,
|
next_suffix: HashMap<String, usize>,
|
||||||
|
/// The next number [`Writer::fresh_id`] tries for a prefix (every lower one is taken).
|
||||||
|
next_fresh: HashMap<String, usize>,
|
||||||
/// The names in `linked`.
|
/// The names in `linked`.
|
||||||
linked_names: HashSet<String>,
|
linked_names: HashSet<String>,
|
||||||
names: HashMap<NodeId, String>,
|
names: HashMap<NodeId, String>,
|
||||||
|
|||||||
@@ -131,13 +131,13 @@ impl Writer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn fresh_id(&mut self, prefix: &str) -> String {
|
pub(super) fn fresh_id(&mut self, prefix: &str) -> String {
|
||||||
let mut i = 1;
|
let next = self.next_fresh.entry(prefix.to_owned()).or_insert(1);
|
||||||
loop {
|
loop {
|
||||||
let id = format!("{}{prefix}-{i}", self.id_prefix);
|
let id = format!("{}{prefix}-{next}", self.id_prefix);
|
||||||
|
*next += 1;
|
||||||
if self.used_ids.insert(id.clone()) {
|
if self.used_ids.insert(id.clone()) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
|
||||||
use vectorcraft_doc::{Appearance, Document, Node};
|
use vectorcraft_doc::{Appearance, Document, Node};
|
||||||
use vectorcraft_geom::{Rect, shapes};
|
use vectorcraft_geom::{Rect, shapes};
|
||||||
use vectorcraft_svg::{ExportOptions, ObjectIds, Styling, export, import};
|
use vectorcraft_svg::{ExportOptions, ObjectIds, Styling, export, import};
|
||||||
@@ -86,6 +86,42 @@ fn unique_ids_never_collide() {
|
|||||||
assert!(names(&back).contains(&"Logo mark (v2)".to_string()));
|
assert!(names(&back).contains(&"Logo mark (v2)".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generated def ids count up from 1 and step over numbers an object name already took, also
|
||||||
|
/// when thousands of them are written.
|
||||||
|
#[test]
|
||||||
|
fn generated_ids_count_up_past_taken_names() {
|
||||||
|
let mut d = Document::new(200.0, 200.0);
|
||||||
|
let l = d.layers[0].id;
|
||||||
|
for name in ["linear-gradient-2", "linear-gradient-4"] {
|
||||||
|
let mut n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 5.0, 5.0)), Appearance::default_art());
|
||||||
|
n.name = Some(name.into());
|
||||||
|
d.insert(Some(l), usize::MAX, n).unwrap();
|
||||||
|
}
|
||||||
|
let count = 3000;
|
||||||
|
for i in 0..count {
|
||||||
|
let stops = vec![
|
||||||
|
GradientStop::new(0.0, Color::rgb((i % 50) as f32 / 255.0, (i / 50) as f32 / 255.0, 0.0)),
|
||||||
|
GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0)),
|
||||||
|
];
|
||||||
|
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||||
|
let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 60.0, 60.0)), Appearance::basic(g, Paint::None, 0.0));
|
||||||
|
d.insert(Some(l), usize::MAX, n).unwrap();
|
||||||
|
}
|
||||||
|
for object_ids in [ObjectIds::LayerNames, ObjectIds::Unique] {
|
||||||
|
let s = export(&d, &ExportOptions { object_ids, ..Default::default() });
|
||||||
|
let all = ids(&s);
|
||||||
|
assert_eq!(all.len(), all.iter().collect::<HashSet<_>>().len(), "unique ids");
|
||||||
|
let prefix = all.iter().find_map(|i| i.find("Layer_1").map(|at| i[..at].to_string())).unwrap();
|
||||||
|
let numbers: Vec<usize> =
|
||||||
|
all.iter().filter_map(|i| i.strip_prefix(&format!("{prefix}linear-gradient-"))).map(|n| n.parse().unwrap()).collect();
|
||||||
|
assert_eq!(numbers.len(), count + 2, "the two named objects and every gradient");
|
||||||
|
let mut gradients: Vec<usize> = numbers.into_iter().filter(|n| *n != 2 && *n != 4).collect();
|
||||||
|
gradients.sort_unstable();
|
||||||
|
let want: Vec<usize> = [1, 3].into_iter().chain(5..count + 3).collect();
|
||||||
|
assert_eq!(gradients, want);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn label_attributes_name_objects() {
|
fn label_attributes_name_objects() {
|
||||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:serif="http://www.serif.com/" width="100" height="100">
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:serif="http://www.serif.com/" width="100" height="100">
|
||||||
|
|||||||
Reference in New Issue
Block a user