use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use lumen::chunks::head::Head;
use lumen::json::{Meta, Timing};
use lumen::reader::LumenFile;
use lumen::validate::{self, Level};
use lumen::writer::Encoder;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let pointer = System.alloc(layout);
if !pointer.is_null() {
let live = LIVE.fetch_add(layout.size(), Ordering::SeqCst) + layout.size();
PEAK.fetch_max(live, Ordering::SeqCst);
}
pointer
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size(), Ordering::SeqCst);
System.dealloc(pointer, layout)
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
const WIDTH: u32 = 512;
const HEIGHT: u32 = 512;
const PIXELS: usize = (WIDTH * HEIGHT) as usize;
const LAYERS: u32 = 16;
const LAYERS_PER_CHUNK: u32 = 2;
fn peak_of(body: impl FnOnce()) -> u64 {
let before = LIVE.load(Ordering::SeqCst);
PEAK.store(before, Ordering::SeqCst);
body();
PEAK.load(Ordering::SeqCst).saturating_sub(before) as u64
}
#[test]
fn validation_peak_memory_does_not_grow_with_the_file() {
let bytes = build();
let file = LumenFile::open(&bytes, Level::Loose).expect("a readable file");
let chunk_indices: Vec<u32> = file.layr_chunks().iter().map(|c| c.index).collect();
let mut per_chunk: HashMap<u32, u64> = HashMap::new();
for entry in &file.layer_table().entries {
*per_chunk.entry(entry.first_layr).or_insert(0) += u64::from(entry.data_size);
}
let total: u64 = per_chunk.values().sum();
let largest = per_chunk.values().copied().max().unwrap_or(0);
let chunk_count = chunk_indices.len();
let layr_container: u64 = file
.layr_chunks()
.iter()
.map(|chunk| chunk.descriptor.stored_len())
.sum();
assert_eq!(chunk_count as u32, LAYERS / LAYERS_PER_CHUNK);
assert!(
chunk_count >= 4 && total >= 3 * largest,
"the file needs several comparably sized chunks for this to mean \
anything: {chunk_count} chunks, total {total}, largest {largest}"
);
let first = chunk_indices[0];
let one_chunk = peak_of(|| {
let file = LumenFile::open(&bytes, Level::Loose).expect("a readable file");
let data = file.layr_chunk_data(first).expect("a readable chunk");
assert_eq!(data.len() as u64, per_chunk[&first]);
});
let sequential = peak_of(|| {
let file = LumenFile::open(&bytes, Level::Loose).expect("a readable file");
for index in &chunk_indices {
let data = file.layr_chunk_data(*index).expect("a readable chunk");
assert!(!data.is_empty());
}
});
let validated = peak_of(|| {
validate::validate(&bytes, Level::Loose).expect("the file is valid");
});
println!(
"file {} bytes; LAYR containers {layr_container}; {chunk_count} chunks, \
{total} bytes decompressed, {largest} largest",
bytes.len()
);
println!(
"peak: one chunk {one_chunk}, all chunks sequentially {sequential}, full validation {validated}"
);
assert!(
validated <= sequential + largest,
"validation peaked at {validated} but walking the chunks one at a time \
peaked at {sequential}: the pass costs more than the walk, so it is \
holding on to something. One chunk is {largest}, all {chunk_count} \
total {total}."
);
}
fn build() -> Vec<u8> {
let mut encoder = Encoder::new(head(), meta());
encoder.set_layers_per_chunk(LAYERS_PER_CHUNK);
encoder.set_dictionary(false);
encoder.set_layer_hashes(true);
for index in 0..LAYERS {
let shift = index as usize;
let mask: Vec<u8> = (0..PIXELS)
.map(|i| (((i + shift) % (WIDTH as usize)) * 255 / (WIDTH as usize - 1)) as u8)
.collect();
encoder.push_layer(&mask).expect("a pushable layer");
}
encoder.finish().expect("a writable file")
}
fn head() -> Head {
Head {
head_version: 1,
encoder_name: "peak-memory".to_string(),
created_unix_sec: 0,
display_width_px: WIDTH,
display_height_px: HEIGHT,
build_width_um: 1,
build_depth_um: 1,
build_height_um: 1,
layer_height_um: 50,
total_layers: LAYERS,
}
}
fn meta() -> Meta {
Meta {
meta_version: Some(1),
timing: Timing {
layer_height_um: Some(50),
normal_exposure_ms: Some(2500),
bottom_exposure_ms: Some(30000),
bottom_layer_count: Some(1),
transition_layer_count: Some(1),
lift_slow_distance_um: Some(5000),
lift_slow_speed_um_min: Some(65_000),
retract_fast_distance_um: Some(5000),
retract_fast_speed_um_min: Some(150_000),
..Timing::default()
},
..Meta::default()
}
}