use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell as StdCell;
use std::path::Path;
use std::sync::Arc;
use pigeonhole::{Family, Options, Pigeonhole, Table};
use pigeonhole_engine::{
Engine, EngineOptions, FamilyOptions, ValueRef, WriteBatch as EngineBatch,
};
use pigeonhole_io::sim::SimVfs;
struct Counting;
thread_local! {
static ALLOCS: StdCell<u64> = const { StdCell::new(0) };
}
fn bump() {
let _ = ALLOCS.try_with(|n| n.set(n.get() + 1));
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
bump();
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
bump();
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
bump();
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
bump();
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn allocations<R>(f: impl FnOnce() -> R) -> (u64, R) {
let before = ALLOCS.with(StdCell::get);
let r = f();
(ALLOCS.with(StdCell::get) - before, r)
}
fn open(name: &str) -> (Pigeonhole, Table) {
let vfs = SimVfs::new(1);
let db = Pigeonhole::open(
format!("/db/{name}.phdb"),
Options::default()
.vfs(Arc::clone(&vfs) as _)
.shards(1)
.memtable_budget(8 << 20)
.wal_segment_size(256 << 10),
)
.expect("open");
let t = db
.table("t")
.expect("table")
.family("f", Family::default())
.family("g", Family::default())
.create_if_missing()
.expect("create");
(db, t)
}
#[test]
fn get_adds_no_allocation_to_the_engine_get() {
let (db, t) = open("get");
let mut o = EngineOptions::new(SimVfs::new(1));
o.create_if_missing = true;
o.shards = 1;
o.memtable_budget = 8 << 20;
let engine = Engine::open(Path::new("/db/engine.phdb"), o).expect("open engine");
let info = engine
.create_table("t", &[("f".into(), FamilyOptions::default())])
.expect("create");
let (table, f) = (info.id, info.families[0].id);
for (i, len) in [16usize, 100, 1000].into_iter().enumerate() {
let row = format!("row{i}");
let value = vec![i as u8 + 1; len];
t.mutate(row.as_bytes())
.put("f", b"q", &value)
.commit()
.expect("commit");
let mut wb = EngineBatch::new();
wb.put(
table,
f,
row.as_bytes(),
b"q",
None,
ValueRef::Bytes(&value),
)
.expect("put");
engine.commit(wb, None).expect("commit");
}
for i in 0..3 {
let row = format!("row{i}").into_bytes();
for q in [&b"q"[..], b"missing"] {
let _ = t.get(&row, "f", q).expect("get");
let _ = engine.get_latest(table, f, &row, q).expect("get");
let (engine_n, _) = allocations(|| engine.get_latest(table, f, &row, q).expect("get"));
let (public_n, len) = allocations(|| {
let c = t.get(&row, "f", q).expect("get");
let len = c
.as_ref()
.map_or(0, |c| c.value().len() as u64 + c.timestamp() % 2);
let owned = c.as_ref().map(|c| c.to_owned());
len + owned.map_or(0, |o| o.value().len() as u64)
});
assert_eq!(len > 0, q == b"q");
assert_eq!(
public_n,
engine_n,
"row{i}/{}: Table::get made {public_n} allocator calls, the engine get {engine_n}",
String::from_utf8_lossy(q)
);
}
}
drop(t);
db.close().expect("close");
engine.close().expect("close engine");
}
fn scan_allocations(t: &Table) -> (u64, usize) {
let mut it = t.scan_prefix(b"").iter().expect("iter");
assert!(it.next_ref().expect("next").is_some());
allocations(|| {
let mut cells = 0;
while let Some(row) = it.next_ref().expect("next") {
for e in row.iter() {
cells += e.cell.value().len().min(1) + e.qualifier.len().min(1);
}
}
cells
})
}
#[test]
fn lending_scan_allocations_do_not_grow_with_cells() {
let rows = 50usize;
let mut per_row = Vec::new();
for cells in [1usize, 32] {
let (db, t) = open(&format!("scan{cells}"));
for r in 0..rows {
let mut m = t.mutate(format!("row{r:04}").as_bytes());
for c in 0..cells {
m = m.put("f", format!("q{c:04}").as_bytes(), &[7u8; 24]);
}
m.commit().expect("commit");
}
let (n, seen) = scan_allocations(&t);
assert_eq!(seen, (rows - 1) * cells * 2);
eprintln!(
"{cells} cells per row: {n} allocations for {} rows",
rows - 1
);
per_row.push(n);
drop(t);
db.close().expect("close");
}
assert!(
per_row[1] <= per_row[0],
"allocations grow with cells per row: {per_row:?} (1 vs 32 cells, {rows} rows)"
);
}
fn row_read_allocations(t: &Table, cells: usize) -> u64 {
let row = format!("wide{cells:04}");
let mut m = t.mutate(row.as_bytes());
for c in 0..cells {
m = m.put("f", format!("q{c:04}").as_bytes(), &[7u8; 24]);
}
m.commit().expect("commit");
let read = || {
let r = t.row(row.as_bytes()).read().expect("read").expect("row");
r.iter()
.map(|e| e.cell.value().len() + e.qualifier.len())
.sum::<usize>()
};
let _ = read();
let (n, seen) = allocations(read);
assert!(seen > 0);
n
}
#[test]
fn row_read_allocations_per_cell() {
let (db, t) = open("row_read");
let one = row_read_allocations(&t, 1);
let wide = row_read_allocations(&t, 33);
let per_cell = (wide - one) as f64 / 32.0;
eprintln!(
"row read: {one} allocator calls for 1 cell, {wide} for 33 ({per_cell:.2} per extra cell)"
);
assert!(
one <= ROW_READ_BUDGET.0,
"{one} allocator calls for a 1-cell row"
);
assert!(
per_cell <= ROW_READ_BUDGET.1,
"{per_cell:.2} allocator calls per extra cell"
);
drop(t);
db.close().expect("close");
}
const ROW_READ_BUDGET: (u64, f64) = (22, 0.5);