Compare commits

...
2 changed files with 44 additions and 3 deletions
+25 -3
View File
@@ -811,10 +811,32 @@ fn clamp_selection(st: &mut DocState) {
/// Default for [`History::byte_limit`].
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
use deckcraft_model::text::{Paragraph, Run};
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
}
/// A rough size of comments in memory, replies included.
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
}
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
/// all copied with the slide when an edit changes it.
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
text_cost(&s.notes)
+ comments_cost(&s.comments, 0)
+ s.name.len()
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
+ s.raw_ext.as_ref().map_or(0, String::len)
}
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
let text: usize =
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
let text: usize = sh.text.as_ref().map_or(0, text_cost);
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
}
@@ -832,7 +854,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
let mut n = 64 * doc.slides.len();
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
n += 512 + shapes_cost(&s.shapes);
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
}
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
+19
View File
@@ -3338,6 +3338,25 @@ fn undo_history_is_capped_by_memory_as_well_as_steps() {
assert_eq!(s.doc().unwrap().history.undo.len(), 120);
}
/// A slide's notes count toward the undo budget: small edits to a slide with large notes keep a
/// copy of those notes per step, and must not slip past the budget as if the slide were empty.
#[test]
fn undo_history_counts_slide_notes() {
let mut s = Session::with_new();
let notes = "n".repeat(1 << 20);
s.edit(|doc, _| {
std::sync::Arc::make_mut(&mut doc.slides[0]).notes = deckcraft_model::text::TextBody::from_text(&notes);
Ok(())
})
.unwrap();
s.doc_mut().unwrap().history = History { limit: 500, byte_limit: 8 << 20, ..Default::default() };
for i in 0..40 {
s.execute("slide.rename", &json!({"name": format!("v{i}"), "index": 0})).unwrap();
}
let n = s.doc().unwrap().history.undo.len();
assert!((1..=8).contains(&n), "{n} steps of 1 MB notes kept under an 8 MB budget");
}
#[test]
fn picture_styles_replace_each_other_in_one_step() {
const PNG: &[u8] = &[