use std::alloc::{GlobalAlloc, Layout, System};
use std::hint::black_box;
use std::sync::atomic::{AtomicUsize, Ordering};
use truecalc_workbook::{
Address, Cell, CellInput, EngineFlavor, RecalcContext, Value, Workbook, Worksheet,
};
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocations_during<T>(body: impl FnOnce() -> T) -> usize {
let before = ALLOCATIONS.load(Ordering::Relaxed);
black_box(body());
ALLOCATIONS.load(Ordering::Relaxed) - before
}
fn scan_workbook(authored: u32, scanned: u32, cols: u32) -> Workbook {
let mut sheet = Worksheet::new("Sheet1");
for row in 1..=authored {
for col in 1..=cols {
sheet.cells_mut().insert(
Address::new(row, col).unwrap().to_a1(),
Cell::literal(Value::Number(f64::from(row))).unwrap(),
);
}
}
let last = Address::new(1, cols).unwrap().to_a1();
let last_col = last.trim_end_matches('1');
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(sheet).unwrap();
wb.set(
"Sheet1",
Address::new(1, 20).unwrap(),
CellInput::Formula(format!("=SUM(A$1:{last_col}{scanned})")),
)
.unwrap();
wb
}
fn allocations_to_recalc(authored: u32, scanned: u32, cols: u32) -> usize {
let ctx = RecalcContext::new(0, "UTC", 0).unwrap();
let mut wb = scan_workbook(authored, scanned, cols);
allocations_during(|| wb.recalc(&ctx))
}
const MAX_ALLOCATIONS_PER_BLOCK_ELEMENT: f64 = 0.50;
const MAX_ALLOCATIONS_PER_COLUMN_ELEMENT: f64 = 2.50;
const MAX_ALLOCATIONS_PER_EMPTY_READ: f64 = 0.50;
const SHEET_SIZES: [u32; 3] = [1_000, 2_000, 4_000];
const EMPTY_ROWS: u32 = 100;
const BLOCK_COLS: u32 = 2;
#[test]
fn cell_reads_do_not_allocate_per_element_or_per_empty_cell() {
black_box(allocations_to_recalc(64, 64, BLOCK_COLS));
let small = allocations_to_recalc(1_000, 1_000, BLOCK_COLS);
let large = allocations_to_recalc(2_000, 2_000, BLOCK_COLS);
let per_block_element = (large - small) as f64 / f64::from(1_000 * BLOCK_COLS);
eprintln!("allocations per populated block element: {per_block_element:.2}");
assert!(
per_block_element <= MAX_ALLOCATIONS_PER_BLOCK_ELEMENT,
"scanning one more populated range element cost {per_block_element:.2} \
allocations (budget {MAX_ALLOCATIONS_PER_BLOCK_ELEMENT:.2}); a range \
element must not allocate an owned map key or re-fold the sheet names"
);
let small = allocations_to_recalc(1_000, 1_000, 1);
let large = allocations_to_recalc(2_000, 2_000, 1);
let per_column_element = (large - small) as f64 / 1_000.0;
eprintln!("allocations per populated column element: {per_column_element:.2}");
assert!(
per_column_element <= MAX_ALLOCATIONS_PER_COLUMN_ELEMENT,
"scanning one more element of a single-column range cost \
{per_column_element:.2} allocations (budget \
{MAX_ALLOCATIONS_PER_COLUMN_ELEMENT:.2}); only the Nx1 column wrapper \
should remain"
);
let mut measured: Vec<(u32, f64)> = Vec::new();
for authored in SHEET_SIZES {
let one_k = allocations_to_recalc(authored, authored + EMPTY_ROWS, BLOCK_COLS);
let two_k = allocations_to_recalc(authored, authored + 2 * EMPTY_ROWS, BLOCK_COLS);
let extra_reads = f64::from(EMPTY_ROWS * BLOCK_COLS);
let per_empty_read = (two_k - one_k) as f64 / extra_reads;
eprintln!(
"allocations per empty-cell read on a {} cell sheet: {per_empty_read:.2}",
authored * BLOCK_COLS
);
measured.push((authored * BLOCK_COLS, per_empty_read));
}
for &(cells, per_empty_read) in &measured {
assert!(
per_empty_read <= MAX_ALLOCATIONS_PER_EMPTY_READ,
"reading one more empty cell on a {cells}-cell sheet cost \
{per_empty_read:.2} allocations (budget \
{MAX_ALLOCATIONS_PER_EMPTY_READ:.2}); an empty-cell read must look \
the sheet's spill anchors up, not scan every authored cell: \
{measured:?}"
);
}
}