M8: a shared parameters value is remembered by the object it ends at

Names kept what a value names by its first reference, so streams naming
one array each through an object of their own that refers to it missed
the memo every time: quadratic time, and now memory too (8000 such
streams with 8000 entries, 1.4 MB: 30 s, 504 MiB). It is keyed by the
object the references end at, so another reference, or a chain, finds it
(the same file: 0.29 s, 7 MiB, measured by the review). matches and
added use the same key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-02 14:03:42 +01:00
1 parent 4acc7e040f
commit f5aa20f7a9
2 files changed
+29 -9

No files matched your search

+16 -5
View File
@@ -23,19 +23,28 @@ pub(crate) fn kept(doc: &Document) -> HashSet<u32> {
/// `o`, or what references lead to from it. A ring of them ends as a reference, which is no dictionary. /// `o`, or what references lead to from it. A ring of them ends as a reference, which is no dictionary.
pub(crate) fn followed(doc: &Document, o: &Object) -> Arc<Object> { pub(crate) fn followed(doc: &Document, o: &Object) -> Arc<Object> {
followed_to(doc, o).0
}
/// [`followed`], and the last reference on the way: the one that names what it ends at, whichever
/// reference or chain of them led there. `None` when `o` was given directly.
fn followed_to(doc: &Document, o: &Object) -> (Arc<Object>, Option<pdfcraft_cos::ObjRef>) {
let mut last = o.as_ref();
let mut at = doc.resolve(o); let mut at = doc.resolve(o);
for _ in 0..MAX_HOPS { for _ in 0..MAX_HOPS {
let next = match &*at { let next = match &*at {
Object::Ref(next) => *next, Object::Ref(next) => *next,
_ => break, _ => break,
}; };
last = Some(next);
at = doc.get(next); at = doc.get(next);
} }
at (at, last)
} }
/// What a parameters value names as `/JBIG2Globals`, and the key it is remembered by: a value the /// What a parameters value names as `/JBIG2Globals`, and the key it is remembered by: a value the
/// file refers to, by its object number and whether it is read as a list of entries. /// file refers to, by the number of the object the references end at and whether it is read as a
/// list of entries.
pub(crate) type Listed = (Rc<[u32]>, Option<(u32, bool)>); pub(crate) type Listed = (Rc<[u32]>, Option<(u32, bool)>);
/// Every object a stream names as `/JBIG2Globals`, and each reference on the way to one: in any entry /// Every object a stream names as `/JBIG2Globals`, and each reference on the way to one: in any entry
@@ -43,7 +52,7 @@ pub(crate) type Listed = (Rc<[u32]>, Option<(u32, bool)>);
/// reader may use less of this, but leaving a dictionary behind is the failure that matters here. /// reader may use less of this, but leaving a dictionary behind is the failure that matters here.
/// ///
/// What a value the file refers to names is read once a run, so streams that share one, by /// What a value the file refers to names is read once a run, so streams that share one, by
/// reference or through an entry, cost it once however many there are. /// reference, through a chain of them or through an entry, cost it once however many there are.
#[derive(Default)] #[derive(Default)]
pub(crate) struct Names { pub(crate) struct Names {
memo: HashMap<(u32, bool), Rc<[u32]>>, memo: HashMap<(u32, bool), Rc<[u32]>>,
@@ -60,12 +69,14 @@ impl Names {
/// What `v` names: one entry, or (`list`) an array of them or one. /// What `v` names: one entry, or (`list`) an array of them or one.
fn value(&mut self, doc: &Document, v: &Object, list: bool) -> Listed { fn value(&mut self, doc: &Document, v: &Object, list: bool) -> Listed {
let key = v.as_ref().map(|r| (r.num, list)); // By the object the references end at: another reference to it, or a chain, finds it too.
let (held, last) = followed_to(doc, v);
let key = last.map(|r| (r.num, list));
if let Some(hit) = key.and_then(|k| self.memo.get(&k)) { if let Some(hit) = key.and_then(|k| self.memo.get(&k)) {
return (hit.clone(), key); return (hit.clone(), key);
} }
self.read += 1; self.read += 1;
let names: Rc<[u32]> = match (&*followed(doc, v), list) { let names: Rc<[u32]> = match (&*held, list) {
(Object::Array(entries), true) => { (Object::Array(entries), true) => {
let mut all: Vec<u32> = entries.iter().flat_map(|e| self.value(doc, e, false).0.to_vec()).collect(); let mut all: Vec<u32> = entries.iter().flat_map(|e| self.value(doc, e, false).0.to_vec()).collect();
all.sort_unstable(); all.sort_unstable();
+13 -4
View File
@@ -1836,17 +1836,26 @@ fn what_is_made_direct_is_small_however_often_a_large_object_is_named() {
assert_eq!(made.dict.get(b"Filter").and_then(Object::as_array).map(Vec::len), Some(9)); assert_eq!(made.dict.get(b"Filter").and_then(Object::as_array).map(Vec::len), Some(9));
} }
/// What streams share by reference is read once a run, however many share it: a parameters array /// What streams share by reference is read once a run, however many share it and however they
/// of 5,000 entries named by 100 streams costs the array and its one entry, once. /// refer to it: a parameters array of 5,000 entries named by 100 streams, directly, each through an
/// object of its own that refers to it, or through a chain of two, costs the array and its one
/// entry, once.
#[test] #[test]
fn a_shared_parameters_array_is_read_once_however_many_streams_share_it() { fn a_shared_parameters_array_is_read_once_however_many_streams_share_it() {
let entries = " 4 0 R".repeat(5000); let entries = " 4 0 R".repeat(5000);
let mut objects = let mut objects =
vec![b"<< /Type /Catalog >>".to_vec(), format!("[{entries}]").into_bytes(), stream("", b"symbols"), b"<< /JBIG2Globals 3 0 R >>".to_vec()]; vec![b"<< /Type /Catalog >>".to_vec(), format!("[{entries}]").into_bytes(), stream("", b"symbols"), b"<< /JBIG2Globals 3 0 R >>".to_vec()];
objects.extend((0..100).map(|_| stream("/Filter /JBIG2Decode /DecodeParms 2 0 R", b""))); // Objects 5 to 54 refer to the array, and 55 to 5.
objects.extend((0..50).map(|_| b"2 0 R".to_vec()));
objects.push(b"5 0 R".to_vec());
// Streams 56 to 155: fifty name the array directly, forty-nine through objects 5 to 53, one
// through the chain from 55.
objects.extend((0..50).map(|_| stream("/Filter /JBIG2Decode /DecodeParms 2 0 R", b"")));
objects.extend((5..54).map(|n| stream(&format!("/Filter /JBIG2Decode /DecodeParms {n} 0 R"), b"")));
objects.push(stream("/Filter /JBIG2Decode /DecodeParms 55 0 R", b""));
let doc = pdf(objects); let doc = pdf(objects);
let mut names = crate::globals::Names::default(); let mut names = crate::globals::Names::default();
for n in 5..105 { for n in 56..156 {
let Object::Stream(s) = &*doc.get(ObjRef::new(n, 0)) else { panic!("object {n} is a stream") }; let Object::Stream(s) = &*doc.get(ObjRef::new(n, 0)) else { panic!("object {n} is a stream") };
let lists = names.of(&doc, s); let lists = names.of(&doc, s);
assert_eq!(lists.iter().map(|(list, _)| list.to_vec()).collect::<Vec<_>>(), [vec![3]]); assert_eq!(lists.iter().map(|(list, _)| list.to_vec()).collect::<Vec<_>>(), [vec![3]]);