Files
vectorcraft/crates/engine/src/history.rs
T

244 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{Arc, Document, Selection};
/// One undo step.
#[derive(Clone, Debug)]
pub struct HistoryEntry {
pub label: String,
/// The document as it was. It keeps no lookup cache ([`Document::drop_lookup_cache`]): a
/// table per step would add up, and undoing builds it again once.
pub doc: Arc<Document>,
/// The selection as it was, shared with the neighbouring steps that have the same one.
pub selection: Arc<Selection>,
/// Roughly the memory this step keeps of its own: the unshared document data
/// ([`Document::retained_bytes`]) plus owned color-mode paint snapshots.
pub bytes: usize,
/// The same the other way round: what that next state keeps that `doc` doesn't share. Undoing
/// (redoing) this step makes a step that keeps the next state: this is its `bytes`.
next_bytes: usize,
/// Which document the figures were taken against (its address): the next state.
next: usize,
/// The template and name `label` was made from ([`Session::edit_named`]).
pub(crate) named: Option<NamedStep>,
/// A number no other step takes ([`History::step`]).
serial: u64,
/// Original and converted new-art colors, only for steps that change document color mode.
pub(crate) color_mode_paints: Option<(vectorcraft_tools::PaintDefaults, vectorcraft_tools::PaintDefaults)>,
/// Owned new-art snapshots included in both directional estimates.
color_mode_bytes: usize,
}
/// The next [`HistoryEntry::serial`]: 1 for the first step made, so 0 never names a step.
fn next_serial() -> u64 {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
/// A step name made by filling the `{name}` placeholder of a template with a name
/// ([`Session::edit_named`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NamedStep {
/// The English template, with one `{name}` placeholder (`Apply {name}`).
pub template: &'static str,
/// The name in the placeholder, as the document holds it (a style's name).
pub name: String,
/// The name is one VectorCraft gives (`[Normal Character Style]`), not one the user typed.
pub built_in: bool,
}
impl NamedStep {
/// The step name: the template with the name in its placeholder.
pub fn label(&self) -> String {
self.template.replace("{name}", &self.name)
}
}
impl HistoryEntry {
/// A step keeping `doc` and `selection`, as they were before the edit that made `next`.
pub(crate) fn kept(label: String, doc: Arc<Document>, selection: Arc<Selection>, next: &Document) -> Self {
doc.drop_lookup_cache();
let (bytes, next_bytes) = doc.retained_bytes_each(next);
HistoryEntry {
label,
doc,
selection,
bytes,
next_bytes,
next: std::ptr::from_ref(next) as usize,
named: None,
serial: next_serial(),
color_mode_paints: None,
color_mode_bytes: 0,
}
}
/// The template and name the step's label was made from, while the label is still the one
/// they make: `None` for a step named any other way, and for one whose label was changed
/// since it was recorded.
pub fn named(&self) -> Option<&NamedStep> {
self.named.as_ref().filter(|n| n.label() == self.label)
}
/// The step that undoing (redoing) the step `costs` came from makes: it keeps `doc`, the
/// document before the undo, whose next state is now `now` (that step's document). Its figures
/// are that step's the other way round when `doc` is still the document they were taken
/// against (nothing replaced it since), so undo and redo needn't walk the documents again. A
/// change made to it in place without an undo step (a setting, a refresh) goes unnoticed: a
/// small error, for an estimate.
pub(crate) fn turned(label: String, doc: Arc<Document>, selection: Arc<Selection>, now: &Arc<Document>, costs: StepCosts) -> Self {
if costs.next != Arc::as_ptr(&doc) as usize {
return Self::kept(label, doc, selection, now);
}
doc.drop_lookup_cache();
HistoryEntry {
label,
doc,
selection,
bytes: costs.next_bytes,
next_bytes: costs.bytes,
next: Arc::as_ptr(now) as usize,
named: None,
serial: next_serial(),
color_mode_paints: None,
color_mode_bytes: 0,
}
}
/// Recount the separately owned paints after a color-mode step or group changes them.
pub(crate) fn recost_paints(&mut self) {
let owned = |p: &vectorcraft_tools::PaintDefaults| {
std::mem::size_of::<vectorcraft_tools::PaintDefaults>()
+ vectorcraft_doc::paint_heap_bytes(&p.fill)
+ vectorcraft_doc::paint_heap_bytes(&p.stroke)
+ p.appearance.as_ref().map_or(0, vectorcraft_doc::appearance_heap_bytes)
+ p.style.as_ref().map_or(0, String::capacity)
};
let bytes = self.color_mode_paints.as_ref().map_or(0, |(before, after)| owned(before).saturating_add(owned(after)));
self.bytes = self.bytes.saturating_sub(self.color_mode_bytes).saturating_add(bytes);
self.next_bytes = self.next_bytes.saturating_sub(self.color_mode_bytes).saturating_add(bytes);
self.color_mode_bytes = bytes;
}
/// The document-only figures [`HistoryEntry::turned`] reuses. Its paint snapshots are
/// attached and counted separately, so swapping direction never doubles their cost.
pub(crate) fn costs(&self) -> StepCosts {
StepCosts {
bytes: self.bytes.saturating_sub(self.color_mode_bytes),
next_bytes: self.next_bytes.saturating_sub(self.color_mode_bytes),
next: self.next,
}
}
}
/// A step's memory figures ([`HistoryEntry::costs`]).
#[derive(Clone, Copy, Debug)]
pub(crate) struct StepCosts {
bytes: usize,
next_bytes: usize,
next: usize,
}
/// The undo history of a document: bounded by a number of steps (Preferences › Performance ›
/// History States) and by the memory the steps keep ([`History::byte_budget`]).
#[derive(Clone, Debug)]
pub struct History {
pub undo: Vec<HistoryEntry>,
pub redo: Vec<HistoryEntry>,
/// 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.
pub byte_budget: usize,
}
impl Default for History {
fn default() -> Self {
History { undo: vec![], redo: vec![], limit: 0, byte_budget: default_history_budget() }
}
}
impl History {
/// Undo (and redo) steps kept whatever memory they take.
pub const MIN_STEPS: usize = 8;
/// Roughly the memory the steps keep of their own ([`HistoryEntry::bytes`]).
pub fn bytes(&self) -> usize {
self.undo.iter().chain(&self.redo).map(|e| e.bytes).sum()
}
/// Which step the document is at: the newest undo step and the next redo step, by numbers no
/// other step takes (0 for none). Every new step, Undo, Redo and edit merged into the newest
/// step (an undo group) changes it; a selection change does not.
pub fn step(&self) -> (u64, u64) {
(self.undo.last().map_or(0, |e| e.serial), self.redo.last().map_or(0, |e| e.serial))
}
/// The document at row `row` of the History panel, `now` being the document now: the rows before
/// it are the undo steps, oldest first, and the rows after it the redo steps, nearest first.
pub(crate) fn state<'a>(&'a self, now: &'a Arc<Document>, row: usize) -> Option<&'a Arc<Document>> {
let current = self.undo.len();
match row.cmp(&current) {
std::cmp::Ordering::Less => self.undo.get(row).map(|e| &e.doc),
std::cmp::Ordering::Equal => Some(now),
std::cmp::Ordering::Greater => self.redo.len().checked_sub(row - current).and_then(|i| self.redo.get(i)).map(|e| &e.doc),
}
}
/// Let the oldest steps go while over the step limit or the memory budget: undo steps first
/// (the oldest), then redo steps (the furthest ahead).
pub fn trim(&mut self) {
let over = self.undo.len().saturating_sub(self.limit);
if over > 0 {
self.undo.drain(..over);
}
let mut total = self.bytes();
if total <= self.byte_budget {
return;
}
let mut n = 0;
while total > self.byte_budget && self.undo.len() - n > Self::MIN_STEPS {
total -= self.undo.get(n).map_or(0, |e| e.bytes);
n += 1;
}
self.undo.drain(..n);
let mut n = 0;
while total > self.byte_budget && self.redo.len() - n > Self::MIN_STEPS {
total -= self.redo.get(n).map_or(0, |e| e.bytes);
n += 1;
}
self.redo.drain(..n);
}
/// Estimate the newest undo step again, its next state now being `now` (the steps after it were
/// merged into it, or an undo group extended it). It takes a new number ([`History::step`]).
pub(crate) fn recost_last(&mut self, now: &Document) {
if let Some(e) = self.undo.last_mut() {
let (before, after) = e.doc.retained_bytes_each(now);
e.bytes = before.saturating_add(e.color_mode_bytes);
e.next_bytes = after.saturating_add(e.color_mode_bytes);
e.next = std::ptr::from_ref(now) as usize;
e.serial = next_serial();
}
}
}
/// The default [`History::byte_budget`]: an eighth of the machine's memory where it is known
/// (Linux), at most 1 GiB (the figure elsewhere); 256 MiB on the web, whose memory is small.
pub fn default_history_budget() -> usize {
const MAX: usize = 1 << 30;
if cfg!(target_arch = "wasm32") {
return 256 << 20;
}
static BUDGET: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*BUDGET.get_or_init(|| physical_memory().map_or(MAX, |m| (m / 8).clamp(64 << 20, MAX)))
}
/// The machine's memory, where cheap to learn without platform calls.
fn physical_memory() -> Option<usize> {
if !cfg!(target_os = "linux") {
return None;
}
let info = std::fs::read_to_string("/proc/meminfo").ok()?;
let kb: usize = info.lines().find_map(|l| l.strip_prefix("MemTotal:"))?.trim().trim_end_matches("kB").trim().parse().ok()?;
kb.checked_mul(1024)
}