diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index f74b5df0..b60aac39 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -29,6 +29,8 @@ pub(crate) const MAX_GSAVE: usize = 1000; const MAX_MEMORY: usize = 1 << 30; pub(crate) struct Interp<'a> { + /// The list of the arrays and dictionaries made since this started ([`super::obj::track`]). + made: u64, pub lex: Lexer<'a>, pub stack: Vec, pub dicts: Vec, @@ -101,6 +103,7 @@ const CATEGORIES: &str = "Category Generic Font CIDFont CMap FontSet Encoding Fo impl<'a> Interp<'a> { pub fn new(src: &'a [u8], g: GState, out: Out) -> Self { + let made = super::obj::track(); let system = new_dict(); { let mut d = system.borrow_mut(); @@ -140,6 +143,7 @@ impl<'a> Interp<'a> { system.borrow_mut().insert(Key::name(name), Obj::Dict(d.clone())); } Self { + made, lex: Lexer::new(src), stack: vec![], dicts: vec![system.clone(), global.clone(), user.clone()], @@ -205,6 +209,8 @@ impl<'a> Interp<'a> { self.dicts.clear(); self.stack.clear(); self.journal.clear(); + // What the program linked into itself, reachable or not. + super::obj::release_tracked(self.made); } // ---------- the operand stack ---------- diff --git a/crates/eps/src/import/obj.rs b/crates/eps/src/import/obj.rs index ae8f7a8e..f540500c 100644 --- a/crates/eps/src/import/obj.rs +++ b/crates/eps/src/import/obj.rs @@ -3,7 +3,7 @@ use std::cell::{Ref, RefCell}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use super::data::Stream; use super::graphics::GState; @@ -17,10 +17,95 @@ pub(crate) struct DictRef(Rc>); impl DictRef { pub fn new(d: Dict) -> Self { - Self(Rc::new(RefCell::new(d))) + let d = Rc::new(RefCell::new(d)); + note(Made::Dict(Rc::downgrade(&d))); + Self(d) } } +/// An array or a dictionary a program made. +enum Made { + Array(Weak>>), + Dict(Weak>), +} + +/// The arrays and dictionaries made on this thread since [`track`], for [`release_tracked`]. +struct Tracked { + owner: u64, + made: Vec, + /// The list is cleared of dropped objects when it reaches this length. + sweep_at: usize, +} + +const SWEEP_MIN: usize = 4096; + +#[cfg(test)] +thread_local! { + /// Tests: the arrays of the list last released on this thread that were still alive after it + /// (the ones the interpreter itself still held). + pub(crate) static ARRAYS_LEFT: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +thread_local! { + static TRACKED: RefCell> = const { RefCell::new(None) }; +} + +/// Start listing the arrays and dictionaries made on this thread (an interpreter starting) → the +/// list's owner, for [`release_tracked`]. A list already there is dropped: its objects are freed +/// as before this existed, unless they hold each other. +pub(crate) fn track() -> u64 { + static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); + let owner = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // (Nothing to list on when the thread is ending.) + let _ = TRACKED.try_with(|t| t.try_borrow_mut().map(|mut t| *t = Some(Tracked { owner, made: vec![], sweep_at: SWEEP_MIN }))); + owner +} + +fn note(m: Made) { + let _ = TRACKED.try_with(|t| { + let Ok(mut t) = t.try_borrow_mut() else { return }; + let Some(t) = t.as_mut() else { return }; + if t.made.len() >= t.sweep_at { + // Twice what is alive at most, so the list costs a program no more than its objects. + t.made.retain(|m| match m { + Made::Array(a) => a.strong_count() > 0, + Made::Dict(d) => d.strong_count() > 0, + }); + t.sweep_at = (t.made.len() * 2).max(SWEEP_MIN); + } + t.made.push(m); + }); +} + +/// Empty every array and dictionary still alive of the list `owner` started (the interpreter is +/// done with them), so those holding themselves or each other are freed too: reference counts +/// alone never free `/a 1 array def a 0 a put`, and a program can leave as much of that behind as +/// it may allocate, on every import (#1714). +pub(crate) fn release_tracked(owner: u64) { + let made = TRACKED.try_with(|t| { + let mut t = t.try_borrow_mut().ok()?; + t.take_if(|t| t.owner == owner).map(|t| t.made) + }); + let made = made.ok().flatten().unwrap_or_default(); + #[cfg(test)] + let arrays: Vec<_> = made.iter().filter_map(|m| if let Made::Array(a) = m { Some(a.clone()) } else { None }).collect(); + for m in made { + // What it holds is taken out first and dropped with nothing borrowed. + match m { + Made::Array(a) => { + let held = a.upgrade().and_then(|a| a.try_borrow_mut().ok().map(|mut values| std::mem::take(&mut *values))); + drop_held(held.unwrap_or_default().into_iter()); + } + Made::Dict(d) => { + let held = d.upgrade().and_then(|d| d.try_borrow_mut().ok().map(|mut d| std::mem::take(&mut *d))); + drop_held(held.unwrap_or_default().into_values()); + } + } + } + #[cfg(test)] + ARRAYS_LEFT.with(|n| n.set(arrays.iter().filter(|a| a.strong_count() > 0).count())); +} + impl std::ops::Deref for DictRef { type Target = Rc>; fn deref(&self) -> &Self::Target { @@ -70,6 +155,8 @@ fn drop_held(held: impl Iterator) { pub(crate) trait Held: Sized { /// Drop the values of storage nothing refers to any more. fn release(_values: Vec) {} + /// Storage of these values was made. + fn made(_storage: &Rc>>) {} } impl Held for u8 {} @@ -78,6 +165,9 @@ impl Held for Obj { fn release(values: Vec) { drop_held(values.into_iter()); } + fn made(storage: &Rc>>) { + note(Made::Array(Rc::downgrade(storage))); + } } /// A dictionary key. @@ -219,7 +309,9 @@ impl Drop for Shared { impl Shared { pub fn new(v: Vec) -> Self { - Self { len: v.len(), buf: Rc::new(RefCell::new(v)), at: 0 } + let (len, buf) = (v.len(), Rc::new(RefCell::new(v))); + T::made(&buf); + Self { len, buf, at: 0 } } pub fn len(&self) -> usize { diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 63cf89b4..678ae993 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -19,6 +19,23 @@ pub(super) fn read(body: &str) -> crate::Imported { import(&eps(body)).unwrap() } +/// #1714: arrays and dictionaries a program links into themselves or each other are freed with +/// the interpreter, also the ones it no longer reaches and when it stops on an error. +#[test] +fn arrays_and_dictionaries_holding_themselves_are_freed() { + // The arrays still alive once a program's interpreter has released what it made: the ones + // the interpreter itself holds, the same for every program here. + let left = |program: &str| { + let _ = import(&eps(program)); + crate::import::obj::ARRAYS_LEFT.with(std::cell::Cell::get) + }; + let plain = left("/a 1 array def /b 1 array def a 0 b put"); + let cycles = "/a 1 array def a 0 a put /b 1 array def /c 1 array def b 0 c put c 0 b put /d 1 dict def /e 1 array def d /e e put e 0 d put 1 array dup 0 1 index put pop /a null def /b null def"; + for program in [cycles.to_string(), format!("{cycles} 1 0 div"), format!("{cycles} /p {{ p }} def")] { + assert_eq!(left(&program), plain, "{program}"); + } +} + /// The objects of the document's layer. pub(super) fn objects(d: &Document) -> Vec> { d.layers[0].children().unwrap().clone()