mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 21:51:36 +00:00
Undo: the open documents' histories share one budget of a quarter of installed memory
`Session::history_budget` (default `default_history_budget()`) caps the memory the undo steps of all open documents keep together. After each command, tool edit, interaction commit, direct edit, framed script run and closed undo group, `Session::trim_histories` drops the oldest steps of any document first, undo steps before redo steps. Each document keeps its last eight undo steps and next eight redo steps, and a held history keeps all of its steps. While a script runs or an undo group is open, no steps are dropped, so a canceled group leaves the other documents' steps in place. Closing a document frees its share. The steps are not counted again while neither the budget nor any history changed. Each document's own `byte_budget` stays as its ceiling.
This commit is contained in:
@@ -171,7 +171,8 @@ pub struct History {
|
||||
/// Maximum undo depth (Illustrator has no hard limit; snapshots are cheap here).
|
||||
pub limit: usize,
|
||||
/// About the most memory the steps may keep ([`HistoryEntry::bytes`]): past it the oldest
|
||||
/// steps go, but never the last [`History::MIN_STEPS`] undo (and redo) steps.
|
||||
/// steps go, but never the last [`History::MIN_STEPS`] undo (and redo) steps. The histories of a
|
||||
/// session's open documents also share [`Session::history_budget`](crate::Session::history_budget).
|
||||
pub byte_budget: usize,
|
||||
/// Counts the changes made in place to steps below the newest ([`History::put_back`]).
|
||||
pub(crate) rewrites: u64,
|
||||
@@ -339,7 +340,80 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`History::byte_budget`]: a quarter of the installed memory, or on Linux of the
|
||||
/// Let the oldest steps of `histories` go while together they keep more than `budget`
|
||||
/// ([`History::bytes`]): undo steps first, the oldest of all the histories first, then redo steps,
|
||||
/// the furthest ahead first. Each history keeps its last [`History::MIN_STEPS`] undo (and redo)
|
||||
/// steps, and a held one ([`History::mark`]) keeps all of its steps; their memory still counts.
|
||||
pub(crate) fn trim_together<'a>(histories: impl IntoIterator<Item = &'a mut History>, budget: usize) {
|
||||
let all: Vec<&mut History> = histories.into_iter().collect();
|
||||
let mut total = all.iter().fold(0usize, |t, h| t.saturating_add(h.bytes()));
|
||||
if total <= budget {
|
||||
return;
|
||||
}
|
||||
fn steps(h: &History, redo: bool) -> &[HistoryEntry] {
|
||||
if redo { &h.redo } else { &h.undo }
|
||||
}
|
||||
let mut free: Vec<&mut History> = all.into_iter().filter(|h| !h.held).collect();
|
||||
for redo in [false, true] {
|
||||
// How many of each history's oldest steps go.
|
||||
let mut gone = vec![0usize; free.len()];
|
||||
while total > budget {
|
||||
// The oldest step any history may still let go.
|
||||
let next = free
|
||||
.iter()
|
||||
.zip(&gone)
|
||||
.enumerate()
|
||||
.filter(|(_, (h, n))| steps(h, redo).len().saturating_sub(**n) > History::MIN_STEPS)
|
||||
.filter_map(|(i, (h, n))| steps(h, redo).get(*n).map(|e| (e.serial, i, e.bytes)))
|
||||
.min();
|
||||
let Some((_, i, bytes)) = next else { break };
|
||||
total = total.saturating_sub(bytes);
|
||||
if let Some(n) = gone.get_mut(i) {
|
||||
*n += 1;
|
||||
}
|
||||
}
|
||||
for (h, n) in free.iter_mut().zip(gone) {
|
||||
let steps = if redo { &mut h.redo } else { &mut h.undo };
|
||||
steps.drain(..n.min(steps.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`Session::trim_histories`](crate::Session::trim_histories) last left: the budget, and for
|
||||
/// each open document its uid and what a new, undone, redone, merged, recosted, removed or put-back
|
||||
/// step changes in its history.
|
||||
pub(crate) type HistoriesSeen = (usize, Vec<(u64, (u64, u64), usize, usize, usize, u64, bool)>);
|
||||
|
||||
impl crate::Session {
|
||||
/// Let the oldest undo and redo steps of the open documents go while together they keep more
|
||||
/// than [`Session::history_budget`](crate::Session::history_budget) ([`trim_together`]). While a
|
||||
/// script runs, its commands leave the histories as they are: the run's end trims them. While an
|
||||
/// undo group is open in any document, the histories stay as they are too, so a canceled group
|
||||
/// leaves the other documents' steps in place: closing the groups trims them. When
|
||||
/// neither the budget nor any history changed since the last call, the steps are not counted
|
||||
/// again.
|
||||
pub(crate) fn trim_histories(&mut self) {
|
||||
if self.script_in_run || self.docs.iter().any(|d| d.undo_group.is_some()) {
|
||||
return;
|
||||
}
|
||||
if self.histories_seen.as_ref() == Some(&self.histories_now()) {
|
||||
return;
|
||||
}
|
||||
trim_together(self.docs.iter_mut().map(|d| &mut d.history), self.history_budget);
|
||||
self.histories_seen = Some(self.histories_now());
|
||||
}
|
||||
|
||||
fn histories_now(&self) -> HistoriesSeen {
|
||||
let doc = |d: &crate::DocState| {
|
||||
let h = &d.history;
|
||||
(d.uid, h.step(), h.undo.len(), h.redo.len(), h.undo.last().map_or(0, |e| e.bytes), h.rewrites, h.held)
|
||||
};
|
||||
(self.history_budget, self.docs.iter().map(doc).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`History::byte_budget`] and [`Session::history_budget`](crate::Session::history_budget),
|
||||
/// which the open documents' histories share: a quarter of the installed memory, or on Linux of the
|
||||
/// process's cgroup memory limit where that is lower ([`vectorcraft_render::physical_memory`], see
|
||||
/// [`history_budget_for`]); 1 GiB where the installed memory can't be read, and 256 MiB on the web.
|
||||
pub fn default_history_budget() -> usize {
|
||||
|
||||
@@ -59,6 +59,13 @@ pub struct Session {
|
||||
pub clipboard: Clipboard,
|
||||
/// Executed commands (for actions and debugging), the most recent ones kept ([`Journal`]).
|
||||
pub journal: Journal,
|
||||
/// About the most memory the undo steps of all the open documents may keep together
|
||||
/// ([`History::bytes`]): past it the oldest steps of any of them go, but each keeps its last
|
||||
/// [`History::MIN_STEPS`] undo (and redo) steps. [`default_history_budget`] by default.
|
||||
pub history_budget: usize,
|
||||
/// The budget and histories [`Session::trim_histories`] last left, so it does not count the
|
||||
/// steps again while none of them changed.
|
||||
pub(crate) histories_seen: Option<history::HistoriesSeen>,
|
||||
pub(crate) tool: Box<dyn Tool>,
|
||||
/// While Cmd lends `tool` (a selection tool) for a drag: the tool it was lent to, which comes
|
||||
/// back as it was at the release ([`Session::pointer`]).
|
||||
|
||||
@@ -1021,7 +1021,11 @@ impl Session {
|
||||
self.script_key = Some(key);
|
||||
None
|
||||
}
|
||||
Ok(Ok(Progress::Done(_))) => Some(self.script_commit(job)),
|
||||
Ok(Ok(Progress::Done(_))) => {
|
||||
let r = self.script_commit(job);
|
||||
self.trim_histories();
|
||||
Some(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ impl Session {
|
||||
fill_active: true,
|
||||
clipboard: Clipboard::default(),
|
||||
journal: Journal::default(),
|
||||
history_budget: crate::default_history_budget(),
|
||||
histories_seen: None,
|
||||
tool: vectorcraft_tools::create("selection"),
|
||||
lender: None,
|
||||
held_input: Default::default(),
|
||||
|
||||
@@ -112,7 +112,7 @@ impl Session {
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
match result {
|
||||
let r = match result {
|
||||
Ok(v) => {
|
||||
st.selection.prune(&st.doc);
|
||||
// An edit that selects what it unlocks, shows or pastes leaves locked guides out,
|
||||
@@ -153,7 +153,12 @@ impl Session {
|
||||
cmd::globaledit::follow_history(st);
|
||||
Err(e)
|
||||
}
|
||||
};
|
||||
// An edit a frontend makes itself, outside a command, keeps to the shared budget too.
|
||||
if r.is_ok() && self.depth == 0 && !self.in_tool_actions {
|
||||
self.trim_histories();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// [`Session::edit`] with the step named `template`, its `{name}` placeholder filled with `name`
|
||||
@@ -394,6 +399,9 @@ impl Session {
|
||||
let (doc, journal, grouped) = (self.active().map(|d| d.uid), self.journal.len(), self.undo_group_docs());
|
||||
self.journal.push(preview);
|
||||
self.journal_in_undo_groups(doc, &grouped, journal);
|
||||
if self.depth == 0 {
|
||||
self.trim_histories();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ impl Session {
|
||||
}
|
||||
if self.depth == 0 {
|
||||
self.journal_in_undo_groups(doc, &grouped, journal);
|
||||
self.trim_histories();
|
||||
self.after_command(Some(id));
|
||||
} else if matches!(id, "edit.undo" | "edit.redo") {
|
||||
// A step of `command.batch`: the tool hears of it as it does of the command alone.
|
||||
|
||||
@@ -160,6 +160,10 @@ impl Session {
|
||||
*n -= below(*n);
|
||||
}
|
||||
}
|
||||
// The shared budget, which waits while an undo group is open, applies again once none is.
|
||||
if self.depth == 0 {
|
||||
self.trim_histories();
|
||||
}
|
||||
}
|
||||
|
||||
/// When journal entry `n` is a `command.batch` with `steps` noted in canceled undo groups and
|
||||
|
||||
@@ -10,6 +10,12 @@ use crate::*;
|
||||
/// A session with one document holding `n` rectangles, all selected.
|
||||
fn session_with_rects(n: usize) -> Session {
|
||||
let mut s = Session::new();
|
||||
open_with_rects(&mut s, n);
|
||||
s
|
||||
}
|
||||
|
||||
/// Open a new document in `s` holding `n` rectangles, all selected.
|
||||
fn open_with_rects(s: &mut Session, n: usize) {
|
||||
s.execute("file.new", &json!({"width": 2000, "height": 2000})).unwrap();
|
||||
s.edit("Add", |d, _| {
|
||||
let layer = d.layers[0].id;
|
||||
@@ -22,7 +28,6 @@ fn session_with_rects(n: usize) -> Session {
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn first_x(s: &Session) -> f64 {
|
||||
@@ -75,6 +80,178 @@ fn within_the_budget_the_step_limit_still_counts() {
|
||||
assert!(st.history.bytes() < st.history.byte_budget);
|
||||
}
|
||||
|
||||
fn moves(s: &mut Session, n: usize) {
|
||||
for _ in 0..n {
|
||||
s.execute("object.move", &json!({"dx": 1, "dy": 0})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_lens(s: &Session) -> Vec<usize> {
|
||||
s.docs.iter().map(|d| d.history.undo.len()).collect()
|
||||
}
|
||||
|
||||
fn history_bytes(s: &Session) -> usize {
|
||||
s.docs.iter().map(|d| d.history.bytes()).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_document_keeps_its_steps_within_the_session_budget() {
|
||||
let mut s = session_with_rects(500);
|
||||
moves(&mut s, 1);
|
||||
let step = s.doc().unwrap().history.undo.last().unwrap().bytes;
|
||||
s.history_budget = 3 * step;
|
||||
moves(&mut s, 20);
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), History::MIN_STEPS, "only the last few steps are kept");
|
||||
assert_eq!(st.history.byte_budget, default_history_budget(), "the document's own budget is unchanged");
|
||||
// A lower budget applies at the next command, even one that adds no step.
|
||||
s.history_budget = usize::MAX;
|
||||
moves(&mut s, 4);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), History::MIN_STEPS + 4);
|
||||
s.history_budget = 3 * step;
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), History::MIN_STEPS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_documents_share_one_budget_and_the_oldest_steps_go_first() {
|
||||
let n = 300;
|
||||
let mut s = session_with_rects(n);
|
||||
moves(&mut s, 1);
|
||||
let step = s.doc().unwrap().history.undo.last().unwrap().bytes;
|
||||
// Room for about 22 moves in all the open documents together.
|
||||
s.history_budget = 22 * step;
|
||||
moves(&mut s, 11);
|
||||
assert_eq!(undo_lens(&s), [13], "Add and 12 moves fit in the budget");
|
||||
open_with_rects(&mut s, n);
|
||||
moves(&mut s, 12);
|
||||
let lens = undo_lens(&s);
|
||||
assert_eq!(lens[1], 13, "the newer document keeps all of its steps: {lens:?}");
|
||||
assert!((History::MIN_STEPS..13).contains(&lens[0]), "the older document's oldest steps went: {lens:?}");
|
||||
assert!(history_bytes(&s) <= s.history_budget, "{} bytes kept, budget {}", history_bytes(&s), s.history_budget);
|
||||
// Closing the older document frees its share: the other keeps more steps than before.
|
||||
assert!(s.close_document(0));
|
||||
assert!(s.set_active(0));
|
||||
moves(&mut s, 6);
|
||||
assert_eq!(undo_lens(&s), [19], "Add and 18 moves fit in the budget alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undo_group_lets_no_other_documents_steps_go_until_it_ends() {
|
||||
let mut s = session_with_rects(50);
|
||||
moves(&mut s, 12);
|
||||
open_with_rects(&mut s, 50);
|
||||
// The steps kept now fill the budget exactly.
|
||||
s.history_budget = history_bytes(&s);
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
assert_eq!(undo_lens(&s), [13, 1]);
|
||||
// A scrubbed field's edits in the second document, then canceled.
|
||||
s.begin_undo_group();
|
||||
moves(&mut s, 3);
|
||||
assert_eq!(undo_lens(&s), [13, 2], "the group's step waits for the group to end");
|
||||
s.end_undo_group(true);
|
||||
assert_eq!(undo_lens(&s), [13, 1], "a canceled group leaves the other document's steps");
|
||||
// The same edits kept: the other document's oldest steps go.
|
||||
s.begin_undo_group();
|
||||
moves(&mut s, 3);
|
||||
s.end_undo_group(false);
|
||||
let lens = undo_lens(&s);
|
||||
assert_eq!(lens[1], 2);
|
||||
assert!((History::MIN_STEPS..13).contains(&lens[0]), "the older document's oldest steps went: {lens:?}");
|
||||
assert!(history_bytes(&s) <= s.history_budget);
|
||||
// A scrub through an undo scope: its step counts once the scope ends.
|
||||
s.history_budget = usize::MAX;
|
||||
assert!(s.set_active(0));
|
||||
moves(&mut s, 6);
|
||||
assert!(s.set_active(1));
|
||||
s.history_budget = history_bytes(&s);
|
||||
let before = undo_lens(&s)[0];
|
||||
let scope = s.begin_undo_scope().unwrap();
|
||||
moves(&mut s, 3);
|
||||
assert_eq!(undo_lens(&s), [before, 3], "the scope's step waits for the scope to end");
|
||||
s.end_undo_scope(scope, false);
|
||||
let lens = undo_lens(&s);
|
||||
assert!(lens[0] < before, "the older document's oldest steps went once the scope ended: {lens:?}");
|
||||
assert!(history_bytes(&s) <= s.history_budget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_kept_outside_a_command_keep_to_the_shared_budget() {
|
||||
let mut s = session_with_rects(50);
|
||||
moves(&mut s, 12);
|
||||
open_with_rects(&mut s, 50);
|
||||
assert_eq!(undo_lens(&s), [13, 1]);
|
||||
// A drag the frontend commits itself.
|
||||
s.begin_interaction("Move").unwrap();
|
||||
s.preview("object.move", &json!({"dx": 3, "dy": 0})).unwrap();
|
||||
s.history_budget = 1;
|
||||
assert_eq!(undo_lens(&s), [13, 1]);
|
||||
s.commit_interaction().unwrap();
|
||||
assert_eq!(undo_lens(&s), [History::MIN_STEPS, 2], "the drag's step counts against the other document's");
|
||||
// A script run across frames, whose step is kept after `script.run` returns.
|
||||
s.history_budget = usize::MAX;
|
||||
assert!(s.set_active(0));
|
||||
moves(&mut s, 4);
|
||||
assert!(s.set_active(1));
|
||||
s.script_frames = true;
|
||||
let source = "for (var i = 0; i < 3000000; i++) {} vc.run('object.move', {dx: 1, dy: 0});";
|
||||
assert_eq!(s.execute("script.run", &json!({"source": source, "name": "t.js"})).unwrap()["running"], true);
|
||||
s.history_budget = 1;
|
||||
let mut slices = 0;
|
||||
let r = loop {
|
||||
slices += 1;
|
||||
assert!(slices < 100_000, "the script ends");
|
||||
if let Some(r) = s.script_pump() {
|
||||
break r;
|
||||
}
|
||||
};
|
||||
assert_eq!(r.unwrap()["ok"], true);
|
||||
assert_eq!(undo_lens(&s), [History::MIN_STEPS, 3], "the run's step counts against the other document's");
|
||||
// An edit a frontend makes itself, outside a command.
|
||||
s.history_budget = usize::MAX;
|
||||
assert!(s.set_active(0));
|
||||
moves(&mut s, 4);
|
||||
assert!(s.set_active(1));
|
||||
s.history_budget = 1;
|
||||
s.edit("Add", |d, _| {
|
||||
let id = d.alloc_id();
|
||||
let r = shapes::rectangle(kurbo::Rect::new(0.0, 20.0, 10.0, 30.0));
|
||||
d.insert(Some(d.layers[0].id), usize::MAX, Node::path(id, r, Appearance::default_art()))?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(undo_lens(&s), [History::MIN_STEPS, 4], "the edit's step counts against the other document's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_document_keeps_its_last_steps_however_small_the_budget() {
|
||||
let mut s = Session::new();
|
||||
// Smaller than any one step.
|
||||
s.history_budget = 1;
|
||||
for _ in 0..4 {
|
||||
open_with_rects(&mut s, 50);
|
||||
moves(&mut s, 12);
|
||||
}
|
||||
assert_eq!(undo_lens(&s), [History::MIN_STEPS; 4]);
|
||||
// The kept steps undo and redo, and the document now is as it was.
|
||||
let x = first_x(&s);
|
||||
assert_eq!(x, 12.0);
|
||||
for _ in 0..History::MIN_STEPS {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
assert_eq!(first_x(&s), x - History::MIN_STEPS as f64);
|
||||
assert!(s.execute("edit.undo", &json!({})).is_err(), "the older steps went");
|
||||
assert_eq!(s.doc().unwrap().history.redo.len(), History::MIN_STEPS);
|
||||
// A held history keeps all of its steps until it is released.
|
||||
assert!(s.set_active(0));
|
||||
s.docs[0].history.held = true;
|
||||
moves(&mut s, 3);
|
||||
assert_eq!(undo_lens(&s)[0], History::MIN_STEPS + 3, "a held history keeps its steps");
|
||||
s.docs[0].history.held = false;
|
||||
s.trim_histories();
|
||||
assert_eq!(undo_lens(&s), [History::MIN_STEPS, History::MIN_STEPS, History::MIN_STEPS, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn the_default_budget_is_a_quarter_of_installed_memory() {
|
||||
@@ -97,6 +274,7 @@ fn the_default_budget_follows_the_installed_memory_read() {
|
||||
let memory = vectorcraft_render::physical_memory();
|
||||
assert_eq!(default_history_budget(), history_budget_for(memory), "{memory:?}");
|
||||
assert_eq!(History::default().byte_budget, default_history_budget());
|
||||
assert_eq!(Session::new().history_budget, default_history_budget(), "the open documents share it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1298,6 +1298,9 @@ impl Session {
|
||||
let outer = std::mem::replace(&mut self.in_tool_actions, true);
|
||||
let r = self.apply_tool_actions(acts);
|
||||
self.in_tool_actions = outer;
|
||||
if !outer && self.depth == 0 {
|
||||
self.trim_histories();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
|
||||
@@ -1486,7 +1486,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
An image whose pixels can't be decoded, such as a PNG cut short or an image over the 2 GB decoding allowance as RGBA that can't be drawn at reduced size, is tried once: the renderer notes it and does not try to decode it again on later frames. An image refused only because the images being drawn leave no room for it is tried again on the next render. On the canvas such an image draws as a light gray box crossed from corner to corner, and the status bar says once for each image that it couldn't be decoded; exports leave it out and report it as before.
|
||||
A PNG, JPEG, GIF or WebP image that takes more than 2 GiB as RGBA (512 MiB in the web app) draws in the canvas and in raster exports at the largest size within that allowance, instead of as that crossed box on the canvas and not at all in exports. Each pixel of the result is the mean of the image's pixels it covers, and the document keeps the whole file. A PNG that isn't interlaced is read a row at a time, and other images are read whole when their decoded samples fit in the allowance. A 16-bit PNG that isn't interlaced, whose decoded samples take more than the allowance and whose RGBA fits in it, draws at its own size, read a row at a time. An image of more pixels than the allowance has bytes (on the desktop, 46,341 × 46,341 is more) still draws as the crossed box, and so does one whose decoded samples take more than the allowance, other than a PNG that isn't interlaced. Open, Place and the Links panel say "an image of W × H pixels is shown at reduced size", and say "too large to draw" of an image that is not drawn at reduced size.
|
||||
The renderers keep the decoded images they share within a quarter of the installed memory on Windows, macOS, Linux and FreeBSD (on Linux, of the process's cgroup memory limit where that is lower, and in a 32-bit build at most a quarter of the address space), within 1 GiB where the installed memory can't be read, and within 256 MiB in the web build; while the decoded images add up to less than that budget, they are not decoded again when the view changes.
|
||||
Each document's undo history keeps its steps within a quarter of the installed memory on the same systems (at least 64 MiB, and at most a quarter of the address space in a 32-bit build), within 1 GiB where the installed memory can't be read, and within 256 MiB in the web build; past that budget the oldest steps are dropped, but the last eight undo steps and the next eight redo steps stay.
|
||||
The undo histories of all the open documents together keep their steps within a quarter of the installed memory on the same systems (at least 64 MiB, and at most a quarter of the address space in a 32-bit build), within 1 GiB where the installed memory can't be read, and within 256 MiB in the web build; past that budget the oldest steps of any open document are dropped first, but each document keeps its last eight undo steps and its next eight redo steps. Closing a document frees its share for the others.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user