mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2056 from dotCooCoo/feat/perf-deep-peak-working-set
CLI: perf --deep reports the peak working set on Windows and the peak resident memory on Linux
This commit is contained in:
@@ -114,7 +114,8 @@ USAGE:
|
||||
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).
|
||||
no budget yet and prints memory use (the working set on Windows), with the peak so far on
|
||||
Windows and Linux.
|
||||
";
|
||||
|
||||
/// The usage text plus the formats `document.open` reads and `document.export` writes.
|
||||
|
||||
@@ -136,31 +136,63 @@ fn edit_drags(n: usize) -> Result<(f64, f64), String> {
|
||||
Ok((handle, segment))
|
||||
}
|
||||
|
||||
/// Resident memory of this process in MB (`ps`), if available.
|
||||
/// Resident memory of this process in MB (`ps`), if available, with the most it has held so far
|
||||
/// on Linux (`VmHWM` in `/proc/self/status`). The peak is None on macOS, which has no `/proc`.
|
||||
#[cfg(not(windows))]
|
||||
fn rss_mb() -> Option<f64> {
|
||||
fn rss_mb() -> Option<(f64, Option<f64>)> {
|
||||
let pid = std::process::id().to_string();
|
||||
let out = std::process::Command::new("ps").args(["-o", "rss=", "-p", &pid]).output().ok().filter(|o| o.status.success())?;
|
||||
String::from_utf8_lossy(&out.stdout).trim().parse::<f64>().ok().map(|kb| kb / 1024.0)
|
||||
let peak = std::fs::read_to_string("/proc/self/status").ok().and_then(|status| vm_hwm_mb(&status));
|
||||
String::from_utf8_lossy(&out.stdout).trim().parse::<f64>().ok().map(|kb| (kb / 1024.0, peak))
|
||||
}
|
||||
|
||||
/// The peak resident set size in MB from the `VmHWM` line of a `/proc/<pid>/status` text, which
|
||||
/// gives it in kB. It is None when the text has no `VmHWM` line in kB.
|
||||
#[cfg(any(not(windows), test))]
|
||||
fn vm_hwm_mb(status: &str) -> Option<f64> {
|
||||
let kb = status.lines().find_map(|line| line.strip_prefix("VmHWM:"))?.trim().strip_suffix("kB")?.trim().parse::<f64>().ok()?;
|
||||
Some(kb / 1024.0)
|
||||
}
|
||||
|
||||
/// The working set of this process in MB (`GetProcessMemoryInfo`), if available: its pages that
|
||||
/// are in physical memory, the figure `ps` reports as resident memory elsewhere.
|
||||
/// are in physical memory, the figure `ps` reports as resident memory elsewhere. The same call
|
||||
/// gives the peak working set, the most the working set has held so far.
|
||||
#[cfg(windows)]
|
||||
fn rss_mb() -> Option<f64> {
|
||||
fn rss_mb() -> Option<(f64, Option<f64>)> {
|
||||
let counters = winsafe::HPROCESS::GetCurrentProcess().GetProcessMemoryInfo().ok()?;
|
||||
Some(counters.WorkingSetSize as f64 / (1024.0 * 1024.0))
|
||||
let mb = |bytes: usize| bytes as f64 / (1024.0 * 1024.0);
|
||||
Some((mb(counters.WorkingSetSize), Some(mb(counters.PeakWorkingSetSize))))
|
||||
}
|
||||
|
||||
/// A tracked row of `--deep`: the work's name and its median in ms. Names are padded to 54
|
||||
/// characters.
|
||||
fn tracked_row(name: &str, ms: f64) -> String {
|
||||
format!(" {name:<54} {ms:>10.2} ms")
|
||||
}
|
||||
|
||||
/// A memory row of `--deep`: the reading's name and value in MB, then the peak so far when
|
||||
/// there is one. Names are padded to 56 characters.
|
||||
fn memory_row(name: &str, mb: f64, peak: Option<f64>) -> String {
|
||||
let peak = peak.map_or_else(String::new, |peak| format!(" peak {peak:>8.0} MB"));
|
||||
format!(" {name:<56} {mb:>8.0} MB{peak}")
|
||||
}
|
||||
|
||||
/// A row of the budget table: the budget's name, the median and the budget in ms, then "ok" or
|
||||
/// "OVER". Names are padded to 42 characters, as in the table's header.
|
||||
fn budget_row(name: &str, ms: f64, budget: f64, ok: bool) -> String {
|
||||
format!(" {name:<42} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" })
|
||||
}
|
||||
|
||||
/// `--deep`: tracked rows (no budget yet) for work the budgets above don't reach: editing a whole
|
||||
/// selection, live effects, every Pathfinder operation, typing in a long story, and memory.
|
||||
/// selection, live effects, every Pathfinder operation, typing in a long story, and memory, with
|
||||
/// the peak so far where `rss_mb` reads one.
|
||||
fn deep(n: usize) -> Result<(), String> {
|
||||
let err = |e: vectorcraft_engine::EngineError| e.to_string();
|
||||
let mut rows: Vec<(String, f64)> = vec![];
|
||||
let mut mem: Vec<(String, f64)> = vec![];
|
||||
let mut mem: Vec<(String, f64, Option<f64>)> = vec![];
|
||||
let mut note = |what: &str| {
|
||||
if let Some(m) = rss_mb() {
|
||||
mem.push((what.to_string(), m));
|
||||
if let Some((m, peak)) = rss_mb() {
|
||||
mem.push((what.to_string(), m, peak));
|
||||
}
|
||||
};
|
||||
note("start");
|
||||
@@ -271,14 +303,15 @@ fn deep(n: usize) -> Result<(), String> {
|
||||
let saved = vectorcraft_format::save_file(&doc);
|
||||
let reopened = vectorcraft_format::load(&saved).map_err(|e| e.to_string())?;
|
||||
outln!(" output hash: SVG export {:016x}, save→open→save {:016x}", fnv(svg.as_bytes()), fnv(&vectorcraft_format::save_file(&reopened)));
|
||||
note("after the SVG export and save→open→save");
|
||||
outln!(" tracked (no budget yet)");
|
||||
for (name, ms) in &rows {
|
||||
outln!(" {name:<52} {ms:>10.2} ms");
|
||||
outln!("{}", tracked_row(name, *ms));
|
||||
}
|
||||
// `rss_mb` reads the working set on Windows.
|
||||
let memory = if cfg!(windows) { "working set" } else { "memory" };
|
||||
for (what, mb) in &mem {
|
||||
outln!(" {:<52} {mb:>8.0} MB", format!("{memory} {what}"));
|
||||
for (what, mb, peak) in &mem {
|
||||
outln!("{}", memory_row(&format!("{memory} {what}"), *mb, *peak));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -381,11 +414,11 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let mut over = 0;
|
||||
outln!(" {:<40} {:>10} {:>10}", "", "measured", "budget");
|
||||
outln!(" {:<42} {:>10} {:>10}", "", "measured", "budget");
|
||||
for (name, ms, budget) in &rows {
|
||||
let ok = ms <= budget;
|
||||
over += usize::from(!ok);
|
||||
outln!(" {name:<40} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" });
|
||||
outln!("{}", budget_row(name, *ms, *budget, ok));
|
||||
}
|
||||
match (over, noisy) {
|
||||
(0, _) => Ok(()),
|
||||
@@ -427,7 +460,57 @@ mod tests {
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_reads_the_working_set() {
|
||||
let mb = rss_mb();
|
||||
let mb = rss_mb().map(|(mb, _)| mb);
|
||||
assert!(mb.is_some_and(|mb| mb > 0.0 && mb < 10_000.0), "{mb:?}");
|
||||
}
|
||||
|
||||
/// On Windows and Linux `rss_mb` also gives the peak: after this test writes 64 MB and frees
|
||||
/// it, the peak is no more than 16 MB below the resident memory read while it held the 64 MB.
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
#[test]
|
||||
fn freed_memory_stays_in_the_peak() {
|
||||
let buffer = std::hint::black_box(vec![1u8; 64 << 20]);
|
||||
let held = rss_mb();
|
||||
drop(buffer);
|
||||
let (Some((held, _)), Some((now, peak))) = (held, rss_mb()) else {
|
||||
eprintln!("no memory reading (Linux without `ps`): nothing to check");
|
||||
return;
|
||||
};
|
||||
assert!(peak.is_some_and(|peak| peak > held - 16.0), "holding 64 MB: {held:.1} MB, after freeing it: {now:.1} MB, peak {peak:?}");
|
||||
}
|
||||
|
||||
/// `vm_hwm_mb` reads the peak resident set size from the `VmHWM` line of a status text that
|
||||
/// also has a `VmRSS` line.
|
||||
#[test]
|
||||
fn vm_hwm_is_the_peak_resident_set_size() {
|
||||
let status = "Name:\tvectorcraft-cli\nVmPeak:\t 2097152 kB\nVmSize:\t 1572864 kB\nVmHWM:\t 819200 kB\nVmRSS:\t 204800 kB\n";
|
||||
assert_eq!(vm_hwm_mb(status), Some(800.0));
|
||||
// A text without a `VmHWM` line gives no peak.
|
||||
assert_eq!(vm_hwm_mb("Name:\tkthreadd\nState:\tS (sleeping)\n"), None);
|
||||
}
|
||||
|
||||
/// A memory row ends with the peak when there is one; without a peak it ends with the value
|
||||
/// in MB. With the longest name on Windows, the value and the peak line up with the other rows.
|
||||
#[test]
|
||||
fn memory_rows_end_with_the_peak_when_there_is_one() {
|
||||
let row = memory_row("working set start", 346.4, Some(470.2));
|
||||
assert!(row.starts_with(" working set start ") && row.ends_with(" 346 MB peak 470 MB"), "{row}");
|
||||
let longest = memory_row("working set after rendering the photos with 3 renderers", 2514.0, Some(2541.0));
|
||||
assert_eq!(longest.chars().count(), row.chars().count(), "{longest}\n{row}");
|
||||
let row = memory_row("memory start", 346.4, None);
|
||||
assert!(row.starts_with(" memory start ") && row.ends_with(" 346 MB") && !row.contains("peak"), "{row}");
|
||||
}
|
||||
|
||||
/// With the longest names, the values of a tracked row and of the budget table's rows line
|
||||
/// up with the rows that have short names.
|
||||
#[test]
|
||||
fn the_longest_tracked_and_budget_names_fit_their_columns() {
|
||||
let longest = tracked_row("render 4 embedded 24 MP photos with 3 renderers (cold)", 774.61);
|
||||
let short = tracked_row("Pathfinder crop, 300 paths", 30.94);
|
||||
assert_eq!(longest.chars().count(), short.chars().count(), "{longest}\n{short}");
|
||||
for name in ["handle drag, 5,000-anchor path (per move)", "segment drag, 5,000-anchor path (per move)"] {
|
||||
let (longest, short) = (budget_row(name, 0.12, 8.0, true), budget_row("export SVG", 7.81, 500.0, true));
|
||||
assert_eq!(longest.chars().count(), short.chars().count(), "{longest}\n{short}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ fn help_lists_readable_formats() {
|
||||
#[test]
|
||||
fn help_lists_perf_deep() {
|
||||
let out = ok(&["--help"]);
|
||||
assert!(out.contains("vectorcraft-cli perf [--paths N] [--deep]") && out.contains("--deep also times"), "{out}");
|
||||
assert!(
|
||||
out.contains("vectorcraft-cli perf [--paths N] [--deep]") && out.contains("--deep also times") && out.contains("with the peak so far on"),
|
||||
"{out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `info` reports the import warnings: an EPS read only up to an error names it.
|
||||
|
||||
@@ -581,6 +581,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
cursors. DPI/hotspot and preference checks in `cursors.rs` and `tests_prefs.rs` cover #1269;
|
||||
actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663).
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. Print-size images (more than 512 MB decoded, up to 2 GB) draw in the canvas and in raster exports and can be trimmed with Crop Image; an image with a side past 65,535 px is shrunk to fit, a raster export says when an image could not be decoded, and Crop Image's trim leaves CMYK images alone (#1517). On Windows `vectorcraft-cli perf` reports the cores busy and, with `--deep`, its working set, and warns on a busy machine as it does on macOS and Linux (#1502).
|
||||
With `--deep`, each memory row of `vectorcraft-cli perf` also gives the peak so far: the peak working set on Windows and the peak resident memory (`VmHWM`) on Linux. A new last row gives the memory after the SVG export and the save → open → save at the end of the run.
|
||||
The renderer composes the two ink planes of a CMYK document that shows transparency into screen colors on up to as many threads as it rasterizes with, instead of on one. At 2880 × 1800 on 8 threads, a page of 300 shapes at 20% to 90% opacity takes 30 ms a frame instead of 55 ms, and 50,000 paths with one at 99% opacity take 258 ms instead of 361 ms (`vectorcraft-cli bench`), with the same pixels.
|
||||
With Overprint Preview or Separations Preview on, the renderer keeps the art it prepared for the preview from one frame to the next. It prepares again the objects that changed, and all of the art when Discard White Overprint changes or, with Separations Preview, when the plates shown, the proof setup, the swatches, the Spot Colors option or the color settings change or a profile is loaded. It draws the other objects from the art, geometry and stroke outlines it kept for them.
|
||||
The Pathfinder panel, Offset Path, Clean Up, Divide Objects Below, Make and Release Blend, Envelope Distort's Make and Release, Repeat (Radial, Grid, Mirror and Release), Vector Halftone, New and Redefine Symbol, Delete Symbol, erasing with the Symbol Sprayer, Rasterize, Convert to Artboards, Flatten Transparency, Clear Guides, Shape Builder, Live Paint, Release Image Trace, Make and Release Opacity Mask, Delete with direct-selected anchor points, Remove Anchor Points, Cut Path, and the Shaper, Knife, Eraser, Path Eraser, Line Cut, Rectangle Cut, Mirror & Cut and Blob Brush tools add and remove objects in one pass over each layer or group they change, and Outline Stroke and Expand write each new object in the place of the object it replaces (#1429).
|
||||
|
||||
Reference in New Issue
Block a user