mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 15:25:16 +00:00
Ruler guides were a list kept beside the layers. Each guide is now an object of its layer or sublayer, always directly in it and above the layer's art. These behave as on main: - Guides draw on screen only. Exports (SVG, PDF, PNG, PSD), Fit All, Select All, the art bounds, clicks and marquees, and a layer's Pathfinder and Transform effects leave them out. The Layers panel lists them as guide rows of their layer, the highest guide.list index first. - Deleting a layer deletes its guides, and merging layers keeps their guides in the merged layer. - The guide commands number guides as guide.list does, in the order they were made. New: each guide takes an id from the document's object ids. An object command given a guide's id fails with an error, and node.move cannot put a guide into a group or out of its layers. A guide made while an opacity mask, a symbol or a pattern is edited moves to the top visible, unlocked layer when editing ends. File format: native files still save the guides in the document's guides list, in every file version, so older builds read them. Each entry may now also carry the guide's id, name, locked and hidden flags; older builds ignore those fields. Opening a file puts each saved guide on its layer (on the top visible, unlocked layer when the file names none or a layer it does not have), and each saved guide counts against the object limit for opening a file. A ruler guide node that a file holds below a layer's art moves up to the layer's guides, and one anywhere else (in a group or another object, beside the layers, or in a symbol) is removed. Ruler guides read from an .ai file become guide objects on the layer they were on, numbered in the order the file lists them.
459 lines
21 KiB
Rust
459 lines
21 KiB
Rust
//! How much memory a document keeps that another doesn't share: what an undo step costs.
|
|
//!
|
|
//! Documents share structure (`Arc<Node>` children, [`crate::Shared`] resources), so an old
|
|
//! version kept in the undo history holds only what the edits after it replaced. The estimate
|
|
//! walks two versions side by side and visits only what isn't shared: a subtree whose `Arc` both
|
|
//! hold costs nothing and isn't entered, so the walk is about as long as the edit was (plus a pass
|
|
//! over each changed child list). One walk gives both directions: what the older version keeps
|
|
//! that the newer one doesn't (the undo step's cost) and the other way round (the cost of the redo
|
|
//! step that undoing it makes), so undo and redo needn't walk again. It counts the heap the values
|
|
//! own (struct sizes, vector capacities, strings, JSON) plus a typical allocator overhead per
|
|
//! allocation, without following every last field: an estimate in the right ballpark, for a
|
|
//! memory budget, not an exact figure.
|
|
|
|
use std::collections::HashSet;
|
|
use std::mem::size_of;
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::Value;
|
|
use vectorcraft_color::{Paint, Swatch, SwatchGroup};
|
|
use vectorcraft_geom::{Affine, Anchor, PathData, SubPath};
|
|
|
|
use crate::live::{BlendSpec, EnvelopeKind};
|
|
use crate::text::{ParaStyle, TextObject, TextRun, TextStyleDef};
|
|
use crate::{Appearance, AppearanceItem, Document, Effect, GraphicStyle, ImageObject, Node, NodeExtras, NodeKind, SavedSelection, Shared};
|
|
|
|
/// Bookkeeping and rounding a typical allocator adds to each allocation.
|
|
const ALLOC: usize = 16;
|
|
/// An `Arc`'s two reference counts.
|
|
const ARC: usize = 16;
|
|
/// The kinds of node this doesn't break down (meshes, repeats' settings, placed documents…): a
|
|
/// typical size of what they own besides the node.
|
|
const OTHER_KIND: usize = 256;
|
|
|
|
impl Document {
|
|
/// Roughly how many bytes of heap this document keeps that `newer` doesn't share with it: what
|
|
/// keeping this version in the undo history costs while `newer` (a later version) is kept too.
|
|
/// Visits only the parts that aren't shared (see the [module docs](self)).
|
|
pub fn retained_bytes(&self, newer: &Document) -> usize {
|
|
self.retained_bytes_each(newer).0
|
|
}
|
|
|
|
/// [`Document::retained_bytes`] both ways in one walk: what this document keeps that `other`
|
|
/// doesn't share, and what `other` keeps that this one doesn't.
|
|
pub fn retained_bytes_each(&self, other: &Document) -> (usize, usize) {
|
|
if std::ptr::eq(self, other) {
|
|
return (0, 0);
|
|
}
|
|
let own = size_of::<Document>() + ALLOC + ARC;
|
|
let mut w = Walk { docs: [self, other], seen: [None, None], bytes: [own, own] };
|
|
w.list(&self.layers, &other.layers);
|
|
for i in 0..self.symbols.len().max(other.symbols.len()) {
|
|
match (self.symbols.get(i), other.symbols.get(i)) {
|
|
(Some(a), Some(b)) => w.pair(&a.art, &b.art),
|
|
(a, b) => {
|
|
a.into_iter().for_each(|a| w.alone(0, &a.art));
|
|
b.into_iter().for_each(|b| w.alone(1, &b.art));
|
|
}
|
|
}
|
|
}
|
|
for i in 0..self.patterns.len().max(other.patterns.len()) {
|
|
match (self.patterns.get(i), other.patterns.get(i)) {
|
|
(Some(a), Some(b)) => w.list(&a.art, &b.art),
|
|
(a, b) => {
|
|
a.into_iter().flat_map(|a| &a.art).for_each(|n| w.alone(0, n));
|
|
b.into_iter().flat_map(|b| &b.art).for_each(|n| w.alone(1, n));
|
|
}
|
|
}
|
|
}
|
|
let [mut a, mut b] = w.bytes;
|
|
a += self.lists_bytes() + self.resources_bytes(other);
|
|
b += other.lists_bytes() + other.resources_bytes(self);
|
|
(a, b)
|
|
}
|
|
|
|
/// The symbol and pattern lists themselves (their art is counted as nodes).
|
|
fn lists_bytes(&self) -> usize {
|
|
vec_bytes(&self.symbols)
|
|
+ self.symbols.iter().map(|s| s.name.capacity()).sum::<usize>()
|
|
+ vec_bytes(&self.patterns)
|
|
+ self.patterns.iter().map(|p| p.name.capacity()).sum::<usize>()
|
|
}
|
|
|
|
/// The document-wide lists and settings: a copy of the document copies those that aren't
|
|
/// [`Shared`]; the shared ones count only when `newer` has its own.
|
|
fn resources_bytes(&self, newer: &Document) -> usize {
|
|
fn shared<T>(a: &Shared<T>, b: &Shared<T>, f: impl FnOnce(&T) -> usize) -> usize {
|
|
if Shared::ptr_eq(a, b) { 0 } else { ALLOC + ARC + f(a) }
|
|
}
|
|
let mut n = 0;
|
|
n += vec_bytes(&self.artboards) + self.artboards.iter().map(|a| a.name.capacity()).sum::<usize>();
|
|
n += vec_bytes(&self.saved_guides) + vec_bytes(&self.views) + vec_bytes(&self.slices) + vec_bytes(&self.assets);
|
|
n += vec_bytes(&self.text_threads) + self.text_threads.iter().map(vec_bytes).sum::<usize>();
|
|
n += self.images.iter().map(|(k, b)| 4 * size_of::<usize>() + k.capacity() + b.mime.capacity() + size_of_val(b)).sum::<usize>();
|
|
n += map_bytes(&self.export_settings) + map_bytes(&self.extra) + self.print_setup.as_ref().map_or(0, json_bytes);
|
|
n += self.title.capacity();
|
|
n += shared(&self.swatches, &newer.swatches, |s| swatches_bytes(s));
|
|
n += shared(&self.swatch_groups, &newer.swatch_groups, |g| {
|
|
vec_bytes(g) + g.iter().map(|g: &SwatchGroup| g.name.capacity() + swatches_bytes(&g.swatches)).sum::<usize>()
|
|
});
|
|
n += shared(&self.graphic_styles, &newer.graphic_styles, |g| {
|
|
vec_bytes(g) + g.iter().map(|g: &GraphicStyle| g.name.capacity() + appearance_heap_bytes(&g.appearance)).sum::<usize>()
|
|
});
|
|
let styles = |s: &Vec<TextStyleDef>| vec_bytes(s) + s.iter().map(|s| s.name.capacity() + map_bytes(&s.attrs)).sum::<usize>();
|
|
n += shared(&self.char_styles, &newer.char_styles, styles);
|
|
n += shared(&self.para_styles, &newer.para_styles, styles);
|
|
n += shared(&self.composite_fonts, &newer.composite_fonts, |c| {
|
|
let part = |p: &crate::CompositePart| p.font.capacity() + p.style.capacity();
|
|
vec_bytes(c)
|
|
+ c.iter().map(|f| f.name.capacity() + crate::CharClass::ALL.into_iter().map(|k| part(f.parts.get(k))).sum::<usize>()).sum::<usize>()
|
|
});
|
|
n += shared(&self.saved_selections, &newer.saved_selections, |s| {
|
|
vec_bytes(s) + s.iter().map(|s: &SavedSelection| s.name.capacity() + vec_bytes(&s.objects)).sum::<usize>()
|
|
});
|
|
n += shared(&self.variables, &newer.variables, |v| {
|
|
// Map entries: the key and value plus a share of the tree's nodes.
|
|
let entry = 2 * size_of::<usize>();
|
|
vec_bytes(&v.variables)
|
|
+ v.variables.iter().map(|v| v.name.capacity()).sum::<usize>()
|
|
+ vec_bytes(&v.datasets)
|
|
+ v.datasets.iter().map(|d| d.name.capacity() + d.values.keys().map(|k| entry + k.capacity() + 32).sum::<usize>()).sum::<usize>()
|
|
+ v.bindings.values().map(|b| entry + 8 + b.iter().map(|s| entry + s.capacity()).sum::<usize>()).sum::<usize>()
|
|
});
|
|
n += shared(&self.unknown, &newer.unknown, map_bytes);
|
|
n
|
|
}
|
|
}
|
|
|
|
/// One estimate in progress: two documents walked side by side.
|
|
struct Walk<'a> {
|
|
docs: [&'a Document; 2],
|
|
/// For each side, every node the other document holds (by address), built the first time a
|
|
/// node of that side that the other may share has no counterpart in place: then a node moved
|
|
/// elsewhere (grouped, say) is still found shared.
|
|
seen: [Option<HashSet<usize>>; 2],
|
|
/// For each side, what it keeps that the other doesn't.
|
|
bytes: [usize; 2],
|
|
}
|
|
|
|
impl Walk<'_> {
|
|
/// Two versions of a child list: the nodes in the same place on both sides (the same `Arc`, or
|
|
/// versions of the same object) are compared in place; the others (inserted, deleted, moved)
|
|
/// may still be shared from elsewhere.
|
|
fn list(&mut self, a: &[Arc<Node>], b: &[Arc<Node>]) {
|
|
let same = |x: &Arc<Node>, y: &Arc<Node>| Arc::ptr_eq(x, y) || x.id == y.id;
|
|
// In place from the start and from the end: an insertion or deletion shifts only what is
|
|
// between.
|
|
let head = a.iter().zip(b).take_while(|(x, y)| same(x, y)).count();
|
|
let (a_rest, b_rest) = (a.get(head..).unwrap_or_default(), b.get(head..).unwrap_or_default());
|
|
let tail = a_rest.iter().rev().zip(b_rest.iter().rev()).take_while(|(x, y)| same(x, y)).count();
|
|
for (x, y) in a.iter().zip(b).take(head).chain(a_rest.iter().rev().zip(b_rest.iter().rev()).take(tail)) {
|
|
self.pair(x, y);
|
|
}
|
|
let a_mid = a_rest.get(..a_rest.len().saturating_sub(tail)).unwrap_or_default();
|
|
let b_mid = b_rest.get(..b_rest.len().saturating_sub(tail)).unwrap_or_default();
|
|
for i in 0..a_mid.len().max(b_mid.len()) {
|
|
match (a_mid.get(i), b_mid.get(i)) {
|
|
(Some(x), Some(y)) if same(x, y) => self.pair(x, y),
|
|
(x, y) => {
|
|
x.into_iter().for_each(|x| self.alone(0, x));
|
|
y.into_iter().for_each(|y| self.alone(1, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Node `a` and `b` in the same place on the two sides: nothing when they are the same `Arc`,
|
|
/// both nodes and what under them isn't shared when they are versions of the same object.
|
|
fn pair(&mut self, a: &Arc<Node>, b: &Arc<Node>) {
|
|
if Arc::ptr_eq(a, b) {
|
|
return;
|
|
}
|
|
if a.id != b.id {
|
|
self.alone(0, a);
|
|
self.alone(1, b);
|
|
return;
|
|
}
|
|
self.bytes[0] += node_bytes(a, Some(b));
|
|
self.bytes[1] += node_bytes(b, Some(a));
|
|
match (a.children(), b.children()) {
|
|
(Some(x), Some(y)) => self.list(x, y),
|
|
(x, y) => {
|
|
x.into_iter().flatten().for_each(|c| self.alone(0, c));
|
|
y.into_iter().flatten().for_each(|c| self.alone(1, c));
|
|
}
|
|
}
|
|
match (&a.extras().mask, &b.extras().mask) {
|
|
(Some(x), Some(y)) => self.pair(&x.art, &y.art),
|
|
(x, y) => {
|
|
x.iter().for_each(|m| self.alone(0, &m.art));
|
|
y.iter().for_each(|m| self.alone(1, &m.art));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Node `n` of side `side` without a counterpart in place, and what under it the other side
|
|
/// doesn't hold.
|
|
fn alone(&mut self, side: usize, n: &Arc<Node>) {
|
|
if self.other_holds(side, n) {
|
|
return;
|
|
}
|
|
if let Some(b) = self.bytes.get_mut(side) {
|
|
*b += node_bytes(n, None);
|
|
}
|
|
for c in n.children().into_iter().flatten() {
|
|
self.alone(side, c);
|
|
}
|
|
if let Some(m) = &n.extras().mask {
|
|
self.alone(side, &m.art);
|
|
}
|
|
}
|
|
|
|
/// Whether the document across from `side` holds node `n` somewhere.
|
|
fn other_holds(&mut self, side: usize, n: &Arc<Node>) -> bool {
|
|
// Held only where it was found: not in the other document (which would hold it too). So
|
|
// new art inserted by the edit is told apart without looking.
|
|
if Arc::strong_count(n) == 1 {
|
|
return false;
|
|
}
|
|
let (Some(other), Some(seen)) = (self.docs.get(1 - side.min(1)), self.seen.get_mut(side)) else { return false };
|
|
let seen = seen.get_or_insert_with(|| {
|
|
fn add(n: &Arc<Node>, out: &mut HashSet<usize>) {
|
|
// A node already seen was entered already (a node shared within the document).
|
|
if !out.insert(Arc::as_ptr(n) as usize) {
|
|
return;
|
|
}
|
|
for c in n.children().into_iter().flatten() {
|
|
add(c, out);
|
|
}
|
|
if let Some(m) = &n.extras().mask {
|
|
add(&m.art, out);
|
|
}
|
|
}
|
|
let mut out = HashSet::new();
|
|
let roots = other.layers.iter().chain(other.symbols.iter().map(|s| &s.art)).chain(other.patterns.iter().flat_map(|p| &p.art));
|
|
for n in roots {
|
|
add(n, &mut out);
|
|
}
|
|
out
|
|
});
|
|
seen.contains(&(Arc::as_ptr(n) as usize))
|
|
}
|
|
}
|
|
|
|
/// One node's own heap: the node, and what it owns besides its children and mask art (counted
|
|
/// as nodes of their own). Its appearance counts unless `other`, its other version, shares it.
|
|
fn node_bytes(n: &Node, other: Option<&Node>) -> usize {
|
|
fn boxed<T>(b: &Option<Box<T>>) -> usize {
|
|
b.as_ref().map_or(0, |_| ALLOC + size_of::<T>())
|
|
}
|
|
let mut b = ALLOC + ARC + size_of::<Node>();
|
|
b += n.name.as_ref().map_or(0, String::capacity);
|
|
if !other.is_some_and(|m| Shared::ptr_eq(&m.appearance, &n.appearance)) {
|
|
b += ALLOC + ARC + appearance_heap_bytes(&n.appearance);
|
|
}
|
|
if let Some(e) = n.extras_box() {
|
|
b += ALLOC + size_of::<NodeExtras>();
|
|
b += boxed(&e.mask) + boxed(&e.graph) + boxed(&e.shaper) + boxed(&e.attrs) + boxed(&e.slice) + boxed(&e.perspective);
|
|
b += boxed(&e.dimension) + boxed(&e.live_paint_gaps) + boxed(&e.colorize);
|
|
b += e.trace.as_ref().map_or(0, |t| ALLOC + size_of::<Value>() + json_bytes(t));
|
|
b += e.intertwine.as_deref().map_or(0, |a| ALLOC + size_of::<PathData>() + path_bytes(a));
|
|
if e.intertwine_tiers.capacity() > 0 {
|
|
b += ALLOC + e.intertwine_tiers.capacity() * size_of::<PathData>() + e.intertwine_tiers.iter().map(path_bytes).sum::<usize>();
|
|
}
|
|
b += e.symbol_overrides.capacity() * size_of::<crate::SymbolOverride>();
|
|
b += e.objects_on_path.as_deref().map_or(0, |o| {
|
|
ALLOC + size_of::<crate::objects_on_path::ObjectsOnPath>() + o.members.capacity() * size_of::<crate::objects_on_path::Member>()
|
|
});
|
|
}
|
|
b += match &n.kind {
|
|
NodeKind::Path { path, live, .. } => path_bytes(path) + boxed(live),
|
|
NodeKind::Layer { children, .. }
|
|
| NodeKind::Group { children, .. }
|
|
| NodeKind::Compound { children, .. }
|
|
| NodeKind::Intertwine { children } => vec_bytes(children),
|
|
NodeKind::Blend { children, .. } => vec_bytes(children) + ALLOC + size_of::<BlendSpec>(),
|
|
NodeKind::Envelope { content, .. } => vec_bytes(content) + 2 * ALLOC + size_of::<EnvelopeKind>() + size_of::<Affine>(),
|
|
NodeKind::Repeat(r) => vec_bytes(&r.source) + OTHER_KIND,
|
|
NodeKind::Text(t) => ALLOC + text_bytes(t),
|
|
NodeKind::Image(im) => ALLOC + size_of::<ImageObject>() + im.key.capacity(),
|
|
NodeKind::SymbolInstance { symbol, .. } => symbol.capacity(),
|
|
_ => OTHER_KIND,
|
|
};
|
|
b
|
|
}
|
|
|
|
fn path_bytes(p: &PathData) -> usize {
|
|
let anchors: usize =
|
|
p.subpaths.iter().map(|s| if s.anchors.capacity() == 0 { 0 } else { ALLOC + s.anchors.capacity() * size_of::<Anchor>() }).sum();
|
|
anchors + if p.subpaths.capacity() == 0 { 0 } else { ALLOC + p.subpaths.capacity() * size_of::<SubPath>() }
|
|
}
|
|
|
|
fn text_bytes(t: &TextObject) -> usize {
|
|
size_of::<TextObject>()
|
|
+ vec_bytes(&t.runs)
|
|
+ t.runs.iter().map(|r: &TextRun| r.text.capacity()).sum::<usize>()
|
|
+ t.paras.capacity() * size_of::<ParaStyle>()
|
|
+ vec_bytes(&t.wrap)
|
|
+ vec_bytes(&t.cached_baselines)
|
|
}
|
|
|
|
/// Approximate retained memory of an appearance and its owned buffers, without serialization.
|
|
pub fn appearance_heap_bytes(a: &Appearance) -> usize {
|
|
size_of::<Appearance>()
|
|
+ vec_bytes(&a.items)
|
|
+ a.items
|
|
.iter()
|
|
.map(|item| {
|
|
let paint = paint_heap_bytes(item.paint());
|
|
let effects = effects_heap(item.effects());
|
|
let stroke = match item {
|
|
AppearanceItem::Stroke(s) => {
|
|
s.dash.as_ref().map_or(0, |d| vec_bytes(&d.pattern))
|
|
+ s.profile.as_ref().map_or(0, |p| vec_bytes(&p.points))
|
|
+ s.pressure.as_ref().map_or(0, |p| vec_bytes(&p.points))
|
|
+ s.brush.as_ref().map_or(0, String::capacity)
|
|
}
|
|
_ => 0,
|
|
};
|
|
paint + effects + stroke
|
|
})
|
|
.sum::<usize>()
|
|
+ effects_heap(&a.effects)
|
|
}
|
|
|
|
fn effects_heap(effects: &Vec<Effect>) -> usize {
|
|
vec_bytes(effects) + effects.iter().map(|e| e.id.capacity() + json_bytes(&e.params)).sum::<usize>()
|
|
}
|
|
|
|
/// Approximate heap retained by one paint, without serializing it.
|
|
pub fn paint_heap_bytes(p: &Paint) -> usize {
|
|
match p {
|
|
Paint::Solid { swatch, .. } => swatch.as_ref().map_or(0, String::capacity),
|
|
Paint::Gradient(g) => {
|
|
ALLOC
|
|
+ size_of_val(&**g)
|
|
+ vec_bytes(&g.gradient.stops)
|
|
+ g.gradient.stops.iter().map(|s| s.swatch.as_ref().map_or(0, String::capacity)).sum::<usize>()
|
|
+ g.swatch.as_ref().map_or(0, String::capacity)
|
|
+ g.freeform.as_ref().map_or(0, |f| vec_bytes(&f.points) + vec_bytes(&f.lines) + f.lines.iter().map(vec_bytes).sum::<usize>())
|
|
}
|
|
Paint::Pattern { pattern, .. } => pattern.capacity(),
|
|
Paint::None => 0,
|
|
}
|
|
}
|
|
|
|
fn swatches_bytes(s: &[Swatch]) -> usize {
|
|
ALLOC + std::mem::size_of_val(s) + s.iter().map(|w| w.name.capacity() + paint_heap_bytes(&w.paint)).sum::<usize>()
|
|
}
|
|
|
|
/// A vector's buffer (not what its elements own).
|
|
fn vec_bytes<T>(v: &Vec<T>) -> usize {
|
|
if v.capacity() == 0 { 0 } else { ALLOC + v.capacity() * size_of::<T>() }
|
|
}
|
|
|
|
fn map_bytes(m: &serde_json::Map<String, Value>) -> usize {
|
|
m.iter().map(|(k, v)| ALLOC + 4 * size_of::<usize>() + k.capacity() + size_of::<Value>() + json_bytes(v)).sum()
|
|
}
|
|
|
|
/// The heap a JSON value owns.
|
|
fn json_bytes(v: &Value) -> usize {
|
|
match v {
|
|
Value::String(s) => s.capacity(),
|
|
Value::Array(a) => vec_bytes(a) + a.iter().map(json_bytes).sum::<usize>(),
|
|
Value::Object(m) => map_bytes(m),
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use vectorcraft_geom::shapes;
|
|
|
|
fn doc_with_rects(n: usize) -> Document {
|
|
let mut d = Document::new(1000.0, 1000.0);
|
|
let layer = d.layers[0].id;
|
|
for i in 0..n {
|
|
let id = d.alloc_id();
|
|
let r = shapes::rectangle(kurbo::Rect::new(i as f64, 0.0, i as f64 + 10.0, 10.0));
|
|
d.insert(Some(layer), usize::MAX, Node::path(id, r, Appearance::default_art())).unwrap();
|
|
}
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn an_unchanged_copy_keeps_nothing_of_its_own() {
|
|
let d = doc_with_rects(10);
|
|
let copy = d.clone();
|
|
// Only the document itself and its own lists: no node, no shared resource.
|
|
assert!(d.retained_bytes(©) < 2048, "{}", d.retained_bytes(©));
|
|
assert_eq!(d.retained_bytes(&d), 0);
|
|
}
|
|
|
|
/// Moving every rectangle: each keeps its old node and anchors (its appearance stays shared).
|
|
/// The process grew by about 775 bytes per moved rectangle when measured (#1244), about 490
|
|
/// once nodes kept their rarely set parts out of line.
|
|
#[test]
|
|
fn moving_rectangles_costs_their_nodes() {
|
|
let n = 1000;
|
|
let before = doc_with_rects(n);
|
|
let mut after = before.clone();
|
|
let ids: Vec<_> = after.layers[0].children().unwrap().iter().map(|c| c.id).collect();
|
|
for id in ids {
|
|
let node = after.node_mut(id).unwrap();
|
|
if let NodeKind::Path { path, .. } = &mut node.kind {
|
|
for a in path.subpaths.iter_mut().flat_map(|s| s.anchors.iter_mut()) {
|
|
a.p.x += 5.0;
|
|
}
|
|
}
|
|
}
|
|
let per = before.retained_bytes(&after) / n;
|
|
assert!((400..1200).contains(&per), "{per} bytes per moved rectangle");
|
|
}
|
|
|
|
#[test]
|
|
fn a_grouped_object_is_found_shared() {
|
|
let before = doc_with_rects(100);
|
|
let mut after = before.clone();
|
|
let ids: Vec<_> = after.layers[0].children().unwrap().iter().map(|c| c.id).collect();
|
|
// Put the rectangles in a group (the same Arcs, elsewhere).
|
|
let gid = after.alloc_id();
|
|
let layer = Arc::make_mut(&mut after.layers[0]);
|
|
let kids = std::mem::take(layer.children_mut().unwrap());
|
|
layer.children_mut().unwrap().push(Arc::new(Node::group(gid, kids)));
|
|
assert_eq!(ids.len(), 100);
|
|
let bytes = before.retained_bytes(&after);
|
|
assert!(bytes < 8192, "only the old layer and its child list: {bytes}");
|
|
}
|
|
|
|
/// One walk gives both ways, each what a walk the other way gives; an insertion and a deletion
|
|
/// cost the side that has the objects.
|
|
#[test]
|
|
fn both_ways_at_once() {
|
|
let before = doc_with_rects(100);
|
|
let mut after = before.clone();
|
|
let layer = after.layers[0].id;
|
|
for at in [0, 50, usize::MAX] {
|
|
let id = after.alloc_id();
|
|
let r = shapes::rectangle(kurbo::Rect::new(0.0, 0.0, 10.0, 10.0));
|
|
after.insert(Some(layer), at, Node::path(id, r, Appearance::default_art())).unwrap();
|
|
}
|
|
let (kept, added) = before.retained_bytes_each(&after);
|
|
assert_eq!((kept, added), (before.retained_bytes(&after), after.retained_bytes(&before)));
|
|
assert_eq!(after.retained_bytes_each(&before), (added, kept));
|
|
// The old layer and its list are the old version's own; the new one holds three new nodes.
|
|
assert!(kept < 4096, "{kept}");
|
|
assert!(added > kept + 3 * 300, "{added} for three new rectangles");
|
|
}
|
|
|
|
#[test]
|
|
fn a_resource_counts_only_when_changed() {
|
|
let before = doc_with_rects(1);
|
|
let mut after = before.clone();
|
|
let same = before.retained_bytes(&after);
|
|
after.swatches.push(before.swatches[1].clone());
|
|
assert!(before.retained_bytes(&after) > same + 1000, "the old swatch list is the old version's own");
|
|
}
|
|
}
|