mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 22:28:09 +00:00
Save: include a PDF in new native files by default
Squashed from #5320 onto main.
This commit is contained in:
@@ -128,12 +128,17 @@ USAGE:
|
||||
Render FILE (any readable format) fitted to WxH (default 2880x1800) and print ms per frame
|
||||
(warm), multithreaded and single-threaded.
|
||||
|
||||
vectorcraft-cli perf [--paths N] [--deep]
|
||||
vectorcraft-cli perf [--paths N] [--deep] [--native-io [--artboards 1..4]]
|
||||
Check the performance budgets (render, pan, hit test, save/load, SVG, Pathfinder, handle
|
||||
and segment drags) on a synthetic N-path document (default 50000). Exits non-zero if a
|
||||
budget is exceeded, unless the report says the machine is busy. --deep also times work with
|
||||
no budget yet and prints memory use (the working set on Windows), with the peak so far on
|
||||
Windows and Linux.
|
||||
--native-io instead measures the application's native save/open commands (one warmup,
|
||||
five samples, no disk I/O), including preview, PDF, compression and API base64 costs.
|
||||
It reports the default and explicit PDF opt-out, with and without preview/compression;
|
||||
--artboards repeats the same artwork
|
||||
on 1 to 4 overlapping artboards. This mode allows at most 50000 paths.
|
||||
";
|
||||
|
||||
/// The usage text plus the formats `document.open` reads and `document.export` writes.
|
||||
|
||||
@@ -64,6 +64,61 @@ fn median_ms(runs: usize, mut f: impl FnMut()) -> f64 {
|
||||
v.get(v.len() / 2).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Exercise the application's native save/open commands, including previews, PDF output,
|
||||
/// compression and API base64 conversion. Session setup and validation are outside timing.
|
||||
/// Five measured runs follow one warmup; no filesystem or GPU window is involved.
|
||||
fn native_io(doc: &Document) -> Result<(), String> {
|
||||
for (label, compatible, preview) in [
|
||||
("default options", None, false),
|
||||
("PDF disabled", Some(false), false),
|
||||
("preview+gzip", None, true),
|
||||
("preview+gzip, PDF disabled", Some(false), true),
|
||||
] {
|
||||
let mut session = vectorcraft_engine::Session::new();
|
||||
session.add_document(doc.clone(), None);
|
||||
let mut params = json!({"format":"vectorcraft","modified":null,"preview":preview,"compress":preview});
|
||||
if let Some(value) = compatible {
|
||||
params["pdfCompatible"] = json!(value);
|
||||
}
|
||||
let (mut saves, mut opens) = (Vec::new(), Vec::new());
|
||||
let (mut size, mut has_pdf, mut compressed) = (0, false, false);
|
||||
for run in 0..6 {
|
||||
let start = Instant::now();
|
||||
let saved = session.execute("file.saveAs", ¶ms).map_err(|e| e.to_string())?;
|
||||
let save_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
let data = saved["dataBase64"].as_str().ok_or("native save returned no dataBase64")?;
|
||||
let bytes = vectorcraft_format::base64_decode(data).ok_or("native save returned invalid base64")?;
|
||||
size = bytes.len();
|
||||
has_pdf = vectorcraft_format::pdf_content(&bytes).is_some();
|
||||
if compatible == Some(false) && has_pdf {
|
||||
return Err("native save ignored the explicit PDF opt-out".into());
|
||||
}
|
||||
compressed = vectorcraft_format::is_compressed(&bytes);
|
||||
if compressed != preview || vectorcraft_format::preview(&bytes).is_some() != preview {
|
||||
return Err("native save did not apply the requested preview/compression options".into());
|
||||
}
|
||||
let args = json!({"name":"perf.vectorcraft","dataBase64":data});
|
||||
let mut opened = vectorcraft_engine::Session::new();
|
||||
let start = Instant::now();
|
||||
opened.execute("document.open", &args).map_err(|e| e.to_string())?;
|
||||
let open_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
if opened.doc().map_err(|e| e.to_string())?.doc.artboards.len() != doc.artboards.len() {
|
||||
return Err("native round trip changed the artboard count".into());
|
||||
}
|
||||
if run > 0 {
|
||||
saves.push(save_ms);
|
||||
opens.push(open_ms);
|
||||
}
|
||||
}
|
||||
saves.sort_by(f64::total_cmp);
|
||||
opens.sort_by(f64::total_cmp);
|
||||
let save = saves.get(2).copied().ok_or("no save measurements")?;
|
||||
let open = opens.get(2).copied().ok_or("no open measurements")?;
|
||||
outln!(" native {label}: save {save:.2} ms, open {open:.2} ms, {size} bytes, PDF {has_pdf}, compressed {compressed}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 1-minute load average (macOS / Linux), if available.
|
||||
#[cfg(not(windows))]
|
||||
fn load_average() -> Option<f64> {
|
||||
@@ -393,19 +448,36 @@ fn deep(n: usize) -> Result<(), String> {
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
if crate::help_requested(args, &["--paths"]) {
|
||||
if crate::help_requested(args, &["--paths", "--artboards"]) {
|
||||
return crate::help();
|
||||
}
|
||||
let mut n = 50_000usize;
|
||||
let mut want_deep = false;
|
||||
let mut want_native = false;
|
||||
let mut artboards = None::<usize>;
|
||||
let mut it = args.iter();
|
||||
while let Some(a) = it.next() {
|
||||
match a.as_str() {
|
||||
"--deep" => want_deep = true,
|
||||
"--native-io" => want_native = true,
|
||||
"--artboards" => {
|
||||
artboards =
|
||||
Some(it.next().and_then(|v| v.parse().ok()).filter(|n| (1..=4).contains(n)).ok_or("--artboards needs a number from 1 to 4")?)
|
||||
}
|
||||
"--paths" => n = it.next().and_then(|v| v.parse().ok()).ok_or("--paths needs a number")?,
|
||||
other => return Err(format!("unknown perf option `{other}`")),
|
||||
}
|
||||
}
|
||||
if want_native && n > 50_000 {
|
||||
return Err("--native-io allows at most 50000 paths".into());
|
||||
}
|
||||
if want_native && want_deep {
|
||||
return Err("--native-io and --deep are separate measurement modes".into());
|
||||
}
|
||||
if artboards.is_some() && !want_native {
|
||||
return Err("--artboards requires --native-io".into());
|
||||
}
|
||||
let artboards = artboards.unwrap_or(1);
|
||||
let cores = std::thread::available_parallelism().map_or(1, |c| c.get());
|
||||
// The figure is the 1-minute load average, or on Windows the cores busy over one second.
|
||||
#[cfg(not(windows))]
|
||||
@@ -418,8 +490,20 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
outln!("WARNING: the machine is busy; wall-clock timings below are not trustworthy.");
|
||||
}
|
||||
let t = Instant::now();
|
||||
let doc = synthetic(n)?;
|
||||
let mut doc = synthetic(n)?;
|
||||
outln!(" (built in {:.0} ms)", t.elapsed().as_secs_f64() * 1000.0);
|
||||
if want_native {
|
||||
let first = doc.artboards.first().cloned().ok_or("synthetic document has no artboard")?;
|
||||
// Overlapping artboards intentionally repeat the same paths on every PDF page.
|
||||
for index in 1..artboards {
|
||||
let mut page = first.clone();
|
||||
page.id = first.id.checked_add(index as u32).ok_or("artboard id overflow")?;
|
||||
page.name = format!("Perf {}", index + 1);
|
||||
doc.artboards.push(page);
|
||||
}
|
||||
outln!(" application native I/O: {artboards} overlapping artboard(s), one warmup + five samples; medians; no disk I/O");
|
||||
return native_io(&doc);
|
||||
}
|
||||
|
||||
let mut rows: Vec<(&str, f64, f64)> = vec![];
|
||||
let opts = RenderOptions::default();
|
||||
@@ -513,6 +597,24 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_io_exercises_the_application_save_and_open_commands() {
|
||||
native_io(&synthetic(3).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_io_rejects_conflicting_modes_and_excessive_workloads() {
|
||||
for (args, expected) in [
|
||||
(vec!["--native-io", "--deep"], "separate measurement modes"),
|
||||
(vec!["--native-io", "--paths", "50001"], "at most 50000"),
|
||||
(vec!["--native-io", "--artboards", "5"], "1 to 4"),
|
||||
(vec!["--artboards", "1"], "requires --native-io"),
|
||||
(vec!["--artboards", "2"], "requires --native-io"),
|
||||
] {
|
||||
assert!(run(&args.into_iter().map(str::to_owned).collect::<Vec<_>>()).unwrap_err().contains(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn busy_share_leaves_out_idle_time() {
|
||||
// In one second four processors log 40,000,000 ticks of 100 ns of kernel and user time,
|
||||
|
||||
Reference in New Issue
Block a user