//! Phase 7 exit criterion. //! //! > Large PDF (50+ pages) loads and renders first page in <200ms. Rapid //! > document switching doesn't leak memory or show stale pages. //! //! The timing assertion uses a deliberately loose budget. A CI runner is //! shared and unpredictable, so a tight threshold would produce flaky //! failures that get muted, and a muted test is worse than no test. The //! budget here catches an order-of-magnitude regression — re-parsing every //! page to show the first one, say — which is the failure the review is //! actually guarding against. use std::time::Instant; use nigig_pdf_document::PdfDocument; use nigig_pdf_graphics::cache::{CachedPage, PageCache}; use nigig_pdf_graphics::content::parse_content_stream; use nigig_pdf_graphics::recording::RecordingDevice; use nigig_pdf_graphics::worker::{PageState, PendingPages, RenderRequest, RenderWorker}; const LARGE: &[u8] = include_bytes!("../../tests/corpus/perf/large_60_pages.pdf"); /// The review's threshold. Generous relative to the real figure so that /// shared-runner noise cannot make this flaky. const FIRST_PAGE_BUDGET_MS: u128 = 200; #[test] fn the_large_fixture_really_is_large() { let doc = PdfDocument::parse(LARGE).expect("fixture parses"); assert!( doc.page_count() >= 50, "the exit criterion needs 50+ pages, found {}", doc.page_count() ); } /// The exit criterion's timing clause. #[test] fn the_first_page_of_a_large_document_renders_within_budget() { let start = Instant::now(); let mut doc = PdfDocument::parse(LARGE).expect("parses"); let page = doc.page(0).expect("page 0"); let ops = parse_content_stream(&page.content_data).expect("content parses"); let commands = RecordingDevice::from_ops(&ops); let elapsed = start.elapsed(); assert!( !commands.is_empty(), "page 0 must actually have rendered something" ); assert!( elapsed.as_millis() < FIRST_PAGE_BUDGET_MS, "first page took {}ms, budget is {FIRST_PAGE_BUDGET_MS}ms", elapsed.as_millis() ); println!( "first page of {} pages in {}ms", doc.page_count(), elapsed.as_millis() ); } /// Opening the document must not cost the whole document. #[test] fn opening_a_large_document_does_not_render_every_page() { // Parsing the document and reaching page 0 must be far cheaper than // rendering all 60 pages. If someone makes open() eager, this fails. let open_start = Instant::now(); let mut doc = PdfDocument::parse(LARGE).expect("parses"); let page_count = doc.page_count(); let first = doc.page(0).expect("page 0"); let _ = parse_content_stream(&first.content_data).expect("parses"); let open_ms = open_start.elapsed().as_secs_f64() * 1000.0; let all_start = Instant::now(); for index in 0..page_count { if let Ok(page) = doc.page(index) { if let Ok(ops) = parse_content_stream(&page.content_data) { let _ = RecordingDevice::from_ops(&ops); } } } let all_ms = all_start.elapsed().as_secs_f64() * 1000.0; println!("open+first {open_ms:.1}ms, all {page_count} pages {all_ms:.1}ms"); // Rendering everything must cost meaningfully more than the first page, // or the "first page" path is secretly doing all the work. assert!( all_ms > open_ms, "rendering {page_count} pages ({all_ms:.1}ms) should cost more than \ opening and rendering one ({open_ms:.1}ms)" ); } #[test] fn a_cached_page_is_much_cheaper_than_re_parsing_it() { let mut doc = PdfDocument::parse(LARGE).expect("parses"); let mut cache = PageCache::new(8 << 20); let generation = cache.begin_document(); // Cold: parse and interpret. let cold_start = Instant::now(); let page = doc.page(0).expect("page 0"); let ops = parse_content_stream(&page.content_data).expect("parses"); let commands = RecordingDevice::from_ops(&ops); let cold = cold_start.elapsed(); cache.insert( generation, 0, CachedPage { commands, images: Default::default(), page_height: page.height(), appearance_dirty: false, }, ); // Warm: read the command list back. let warm_start = Instant::now(); let cached = cache.get(0).expect("cached"); let count = cached.commands.len(); let warm = warm_start.elapsed(); assert!(count > 0); println!("cold {}us, warm {}us", cold.as_micros(), warm.as_micros()); assert!( warm < cold, "replaying a cached page ({}us) must beat re-parsing it ({}us)", warm.as_micros(), cold.as_micros() ); } /// The exit criterion's "doesn't leak memory" clause. #[test] fn rapid_document_switching_does_not_grow_memory() { const BUDGET: usize = 1 << 20; let mut cache = PageCache::new(BUDGET); let mut doc = PdfDocument::parse(LARGE).expect("parses"); for _ in 0..30 { let generation = cache.begin_document(); // Render a handful of pages of each "document". for index in 0..6 { let Ok(page) = doc.page(index) else { continue }; let Ok(ops) = parse_content_stream(&page.content_data) else { continue; }; cache.insert( generation, index, CachedPage { commands: RecordingDevice::from_ops(&ops), images: Default::default(), page_height: page.height(), appearance_dirty: false, }, ); } } // After 30 switches the cache must be bounded by its budget, not by the // total of everything ever rendered. assert!( cache.used_bytes() <= BUDGET * 2, "cache holds {} bytes after 30 document switches, budget {BUDGET}", cache.used_bytes() ); assert!(cache.len() <= 6, "only the last document's pages remain"); } /// The exit criterion's "doesn't show stale pages" clause, end to end /// through the worker. #[test] fn a_page_from_an_abandoned_document_is_never_shown() { let mut doc = PdfDocument::parse(LARGE).expect("parses"); let mut cache = PageCache::new(8 << 20); let worker = RenderWorker::spawn(); // Document A: queue several pages. let doc_a = cache.begin_document(); for index in 0..8 { let Ok(page) = doc.page(index) else { continue }; worker.request(RenderRequest { generation: doc_a, page_index: index, content: page.content_data.clone(), page_height: page.height(), color_spaces: page.color_spaces.clone(), ext_gstates: page.ext_gstates.clone(), }); } // The user immediately switches to document B. let doc_b = cache.begin_document(); // Every result from A must be refused, however late it arrives. let mut refused = 0; for _ in 0..8 { let Some(result) = worker.recv_blocking() else { break; }; if result.generation == doc_a { assert!( !cache.insert(result.generation, result.page_index, result.page), "a page of the abandoned document must not enter the cache" ); refused += 1; } } assert_eq!(refused, 8, "all eight stale pages must be refused"); assert!( cache.is_empty(), "nothing from the abandoned document may be shown" ); assert!(cache.accepts(doc_b)); } #[test] fn pages_show_a_placeholder_until_their_render_arrives() { let mut doc = PdfDocument::parse(LARGE).expect("parses"); let mut cache = PageCache::new(8 << 20); let worker = RenderWorker::spawn(); let generation = cache.begin_document(); let mut pending = PendingPages::new(generation); assert_eq!( pending.state(0, cache.contains(0)), PageState::Absent, "an unrequested page is absent" ); let page = doc.page(0).expect("page 0"); pending.mark_requested(0); worker.request(RenderRequest { generation, page_index: 0, content: page.content_data.clone(), page_height: page.height(), color_spaces: page.color_spaces.clone(), ext_gstates: page.ext_gstates.clone(), }); assert_eq!( pending.state(0, cache.contains(0)), PageState::Loading, "a requested page shows a placeholder" ); let result = worker.recv_blocking().expect("a result"); cache.insert(result.generation, result.page_index, result.page); pending.mark_done(0); assert_eq!( pending.state(0, cache.contains(0)), PageState::Ready, "once rendered, the real page is drawn" ); } #[test] fn scrolling_evicts_pages_that_left_the_viewport() { let mut doc = PdfDocument::parse(LARGE).expect("parses"); let mut cache = PageCache::new(64 << 20); let generation = cache.begin_document(); for index in 0..30 { let Ok(page) = doc.page(index) else { continue }; let Ok(ops) = parse_content_stream(&page.content_data) else { continue; }; cache.insert( generation, index, CachedPage { commands: RecordingDevice::from_ops(&ops), images: Default::default(), page_height: page.height(), appearance_dirty: false, }, ); } assert!(cache.len() >= 20); // The user scrolls to pages 20..=22. cache.retain_around(20, 22, 2); assert!(cache.contains(20) && cache.contains(22)); assert!(cache.contains(18), "the margin is kept"); assert!(!cache.contains(5), "a distant page is released"); assert!(cache.len() <= 7, "window plus margin only"); } #[test] fn a_form_edit_does_not_discard_the_pages_parsed_commands() { let mut doc = PdfDocument::parse(LARGE).expect("parses"); let mut cache = PageCache::new(8 << 20); let generation = cache.begin_document(); let page = doc.page(0).expect("page 0"); let ops = parse_content_stream(&page.content_data).expect("parses"); let command_count = RecordingDevice::from_ops(&ops).len(); cache.insert( generation, 0, CachedPage { commands: RecordingDevice::from_ops(&ops), images: Default::default(), page_height: page.height(), appearance_dirty: false, }, ); // A form edit invalidates the drawn appearance, not the content stream. cache.invalidate_appearance(0); assert_eq!(cache.dirty_pages(), vec![0]); assert_eq!( cache.peek(0).expect("still cached").commands.len(), command_count, "re-parsing the content stream after a form edit is wasted work" ); }