use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use djvu_rs::Pixmap;
use djvu_rs::djvu_encode::{
EncodeQuality, encode_djvm_layered_shared, encode_djvm_layered_shared_streaming,
};
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
struct Counting;
impl Counting {
#[inline]
fn grew(by: usize) {
let live = LIVE.fetch_add(by, Ordering::Relaxed) + by;
PEAK.fetch_max(live, Ordering::Relaxed);
}
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
Self::grew(layout.size());
}
p
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc_zeroed(layout) };
if !p.is_null() {
Self::grew(layout.size());
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new_size) };
if !p.is_null() {
if new_size >= layout.size() {
Self::grew(new_size - layout.size());
} else {
LIVE.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
}
p
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
fn peak_bytes_of<T>(f: impl FnOnce() -> T) -> usize {
let base = LIVE.load(Ordering::Relaxed);
PEAK.store(base, Ordering::Relaxed);
let out = f();
let peak = PEAK.load(Ordering::Relaxed);
drop(out);
peak.saturating_sub(base)
}
const PAGE_W: u32 = 240;
const PAGE_H: u32 = 320;
const PIXMAP_BYTES: usize = (PAGE_W as usize) * (PAGE_H as usize) * 4;
fn synthetic_page() -> Pixmap {
let mut pm = Pixmap::white(PAGE_W, PAGE_H);
for line in 0..12u32 {
let y0 = 16 + line * 24;
for word in 0..9u32 {
let x0 = 12 + word * 25;
for dy in 0..10u32 {
for dx in 0..17u32 {
if dy == 0 || dy == 9 || dx == 0 || dx == 16 {
pm.set_rgb(x0 + dx, y0 + dy, 0, 0, 0);
}
}
}
}
}
pm
}
const SWEEP: [usize; 3] = [12, 48, 96];
const SHARED_DICT_THRESHOLD: usize = 2;
fn encode_streaming(pages: usize) -> usize {
encode_djvm_layered_shared_streaming(
pages,
|_i| -> Result<Pixmap, std::io::Error> { Ok(synthetic_page()) },
EncodeQuality::Quality,
300,
None,
SHARED_DICT_THRESHOLD,
false,
None,
Some(2),
)
.expect("streaming encode succeeds")
.len()
}
fn encode_eager(pages: usize) -> usize {
let pixmaps: Vec<Pixmap> = (0..pages).map(|_| synthetic_page()).collect();
encode_djvm_layered_shared(
&pixmaps,
EncodeQuality::Quality,
300,
None,
SHARED_DICT_THRESHOLD,
)
.expect("eager encode succeeds")
.len()
}
fn slope(lo: (usize, usize), hi: (usize, usize)) -> f64 {
let (lo_pages, lo_peak) = lo;
let (hi_pages, hi_peak) = hi;
(hi_peak as f64 - lo_peak as f64) / (hi_pages as f64 - lo_pages as f64)
}
#[test]
fn streaming_encode_peak_stays_flat_as_page_count_grows() {
let _ = encode_streaming(2);
let mut streaming = Vec::new();
for &pages in &SWEEP {
let peak = peak_bytes_of(|| encode_streaming(pages));
println!("streaming {pages:>3} pages: peak {:>10} B", peak);
streaming.push((pages, peak));
}
let mut eager = Vec::new();
for &pages in &SWEEP[..2] {
let peak = peak_bytes_of(|| encode_eager(pages));
println!("eager {pages:>3} pages: peak {:>10} B", peak);
eager.push((pages, peak));
}
let streaming_slope = slope(streaming[0], streaming[2]);
let eager_slope = slope(eager[0], eager[1]);
println!(
"one pixmap {PIXMAP_BYTES} B | streaming slope {streaming_slope:.0} B/page \
({:.1}% of a pixmap) | eager slope {eager_slope:.0} B/page ({:.1}% of a pixmap)",
100.0 * streaming_slope / PIXMAP_BYTES as f64,
100.0 * eager_slope / PIXMAP_BYTES as f64,
);
assert!(
eager_slope > 0.5 * PIXMAP_BYTES as f64,
"control failed: the eager path should add about one pixmap ({PIXMAP_BYTES} B) of peak \
per page, but measured {eager_slope:.0} B/page — the allocator counter is not seeing \
pixmap residency, so this file is not guarding anything"
);
assert!(
streaming_slope < 0.25 * PIXMAP_BYTES as f64,
"streaming peak is growing with the page count: {streaming_slope:.0} B/page, over 25% of \
one page's pixmap ({PIXMAP_BYTES} B). The bounded-window guarantee of \
`encode_djvm_layered_shared_streaming` (encoder peak-memory step 4) is broken — most \
likely the source closure's pixmaps are being collected before phase 1 instead of being \
dropped after it. Peaks: {streaming:?}"
);
assert!(
streaming_slope < 0.2 * eager_slope,
"streaming ({streaming_slope:.0} B/page) is no longer decisively flatter than eager \
({eager_slope:.0} B/page)"
);
}