use std::collections::BTreeMap;
use topodb::workload::{batches, WorkloadSpec};
use topodb::{
Db, DbOptions, IndexSpec, NodeId, Op, PropIndex, PropValue, Props, Scope, ScopeId, ScopeSet,
};
fn spec() -> IndexSpec {
IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
}
}
fn run_size_report(scales: &[usize]) {
for &memories in scales {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bench.redb");
let db = Db::open_with(&path, spec()).unwrap();
for batch in batches(&WorkloadSpec {
memories,
..Default::default()
}) {
db.submit(batch).unwrap();
}
drop(db);
let db = Db::open_with(&path, spec()).unwrap();
let report = db.storage_report().unwrap();
let file = std::fs::metadata(&path).unwrap().len();
println!("\n== {memories} memories == file: {file}");
let mut total = 0;
for r in &report {
println!("{} {} {} {}", r.table, r.rows, r.key_bytes, r.value_bytes);
total += r.key_bytes + r.value_bytes;
}
println!("logical total: {total}");
let edges = report.iter().find(|r| r.table == "edges").unwrap();
let out_adj = report.iter().find(|r| r.table == "out_adj").unwrap();
let in_adj = report.iter().find(|r| r.table == "in_adj").unwrap();
let edge_family = edges.key_bytes
+ edges.value_bytes
+ out_adj.key_bytes
+ out_adj.value_bytes
+ in_adj.key_bytes
+ in_adj.value_bytes;
println!("edges+out_adj+in_adj logical bytes: {edge_family}");
}
}
#[test]
#[ignore]
fn size_report() {
run_size_report(&[1_000usize, 10_000, 100_000]);
}
#[test]
#[ignore]
fn size_report_v3_gate4() {
run_size_report(&[1_000usize, 10_000]);
}
fn timed_opens(
path: &std::path::Path,
open_spec: &IndexSpec,
iters: usize,
) -> Vec<std::time::Duration> {
let mut out = Vec::with_capacity(iters);
for _ in 0..iters {
let start = std::time::Instant::now();
let db = Db::open_with(path, open_spec.clone()).unwrap();
out.push(start.elapsed());
drop(db);
}
out.sort();
out
}
fn print_open_stats(times: &[std::time::Duration]) {
for (i, t) in times.iter().enumerate() {
println!(" open[{i}] = {:.1} ms", t.as_secs_f64() * 1000.0);
}
let p95_idx = (times.len() * 95 / 100).min(times.len() - 1);
println!(
"open_min_ms={:.1} open_median_ms={:.1} open_p95_ms={:.1} open_max_ms={:.1}",
times[0].as_secs_f64() * 1000.0,
times[times.len() / 2].as_secs_f64() * 1000.0,
times[p95_idx].as_secs_f64() * 1000.0,
times[times.len() - 1].as_secs_f64() * 1000.0,
);
}
fn env_usize(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn fixture_spec(skip_fts: bool) -> IndexSpec {
let mut s = spec();
if skip_fts {
s.text.clear();
}
s
}
fn open_fixture_path(memories: usize, embed_pct: usize, skip_fts: bool) -> std::path::PathBuf {
let fts = if skip_fts { "nofts" } else { "fts" };
std::env::temp_dir().join(format!("topodb_v3_open_{memories}_{embed_pct}_{fts}.redb"))
}
#[test]
#[ignore]
fn build_open_fixture() {
let memories = env_usize("TOPODB_FIXTURE_MEMORIES", 250_000);
let embed_pct = env_usize("TOPODB_FIXTURE_EMBED_PCT", 0);
let skip_fts = env_usize("TOPODB_FIXTURE_SKIP_FTS", 0) == 1;
let budget = std::time::Duration::from_secs(env_usize("TOPODB_BUILD_BUDGET_SECS", 420) as u64);
let chunk = env_usize("TOPODB_BUILD_CHUNK", 5_000);
let path = open_fixture_path(memories, embed_pct, skip_fts);
let ops: Vec<_> = batches(&WorkloadSpec {
memories,
embed_pct: embed_pct as u8,
..Default::default()
})
.into_iter()
.flatten()
.collect();
let db = Db::open_with(&path, fixture_spec(skip_fts)).unwrap();
let done = db.current_seq().unwrap() as usize;
assert!(
done <= ops.len(),
"fixture has more ops than this workload — wrong TOPODB_FIXTURE_* env?"
);
let start = std::time::Instant::now();
let mut submitted = done;
for slice in ops[done..].chunks(chunk) {
if start.elapsed() > budget {
break;
}
db.submit(slice.to_vec()).unwrap();
submitted += slice.len();
println!(
" ops {submitted}/{} elapsed_secs={:.0}",
ops.len(),
start.elapsed().as_secs_f64()
);
}
println!(
"memories={memories} embed_pct={embed_pct} skip_fts={skip_fts} ops={submitted}/{} run_secs={:.1} complete={}",
ops.len(),
start.elapsed().as_secs_f64(),
submitted == ops.len()
);
}
#[test]
#[ignore]
fn open_report() {
let memories = env_usize("TOPODB_FIXTURE_MEMORIES", 250_000);
let embed_pct = env_usize("TOPODB_FIXTURE_EMBED_PCT", 0);
let skip_fts = env_usize("TOPODB_FIXTURE_SKIP_FTS", 0) == 1;
let path = open_fixture_path(memories, embed_pct, skip_fts);
let open_spec = fixture_spec(skip_fts);
let expected_ops: u64 = batches(&WorkloadSpec {
memories,
embed_pct: embed_pct as u8,
..Default::default()
})
.iter()
.map(|b| b.len() as u64)
.sum();
{
let db = Db::open_with(&path, open_spec.clone()).unwrap();
assert_eq!(
db.current_seq().unwrap(),
expected_ops,
"fixture incomplete — rerun build_open_fixture until complete=true"
);
}
println!("memories={memories} embed_pct={embed_pct} skip_fts={skip_fts}");
println!("file_bytes={}", std::fs::metadata(&path).unwrap().len());
print_open_stats(&timed_opens(&path, &open_spec, 10));
}
fn ram_fixture_path() -> std::path::PathBuf {
std::env::temp_dir().join("topodb_v3_ram_fixture.redb")
}
#[test]
#[ignore]
fn build_ram_fixture() {
let path = ram_fixture_path();
if path.exists() {
std::fs::remove_file(&path).unwrap();
}
let db = Db::open_with(&path, spec()).unwrap();
for batch in batches(&WorkloadSpec {
memories: 30_000,
..Default::default()
}) {
db.submit(batch).unwrap();
}
drop(db);
println!("built {}", path.display());
}
#[test]
#[ignore]
fn ram_report() {
let cache_mb: usize = std::env::var("TOPODB_RAM_CACHE_MB")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(64);
let path = ram_fixture_path();
let opts = DbOptions {
cache_size_bytes: Some(cache_mb * 1024 * 1024),
};
let db = Db::open_with_options(&path, spec(), opts).unwrap();
let report = db.storage_report().unwrap();
let total: u64 = report.iter().map(|r| r.key_bytes + r.value_bytes).sum();
println!("cache_mb={cache_mb} logical_total={total}");
std::thread::sleep(std::time::Duration::from_secs(3));
}
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
const FTS_WORDS: [&str; 16] = [
"agent", "memory", "graph", "scope", "recall", "vector", "search", "index", "temporal", "edge",
"node", "label", "batch", "snapshot", "project", "decision",
];
fn fts_sentence(r: &mut Rng) -> String {
(0..50 + r.below(451))
.map(|_| FTS_WORDS[r.below(FTS_WORDS.len())])
.collect::<Vec<_>>()
.join(" ")
}
fn fts_spec() -> IndexSpec {
spec()
}
#[test]
#[ignore]
fn fts_linearity_append_report() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fts_linearity.redb");
let db = Db::open_with(&path, fts_spec()).unwrap();
let scope = Scope::Id(ScopeId::from_u128(1));
let total = 100_000usize;
let checkpoints = [1_000usize, 10_000, 100_000];
let mut r = Rng(0xC0FFEE);
let mut memories_done = 0usize;
let mut window_start_memories = 0usize;
let mut window_start_time = std::time::Instant::now();
let mut next_checkpoint = 0usize;
let mut batch: Vec<Op> = Vec::with_capacity(200);
let mut batch_n = 0usize;
for i in 0..total {
let content = fts_sentence(&mut r);
let mut props = Props::new();
props.insert("content".into(), PropValue::Str(content));
batch.push(Op::CreateNode {
id: NodeId::new(),
scope,
label: "Memory".into(),
props,
});
batch_n += 1;
let is_last = i + 1 == total;
if batch_n == 200 || is_last {
let start = std::time::Instant::now();
db.submit(std::mem::take(&mut batch)).unwrap();
let elapsed = start.elapsed();
memories_done += batch_n;
println!(
"memories={memories_done}/{total} batch_n={batch_n} batch_elapsed_ms={:.2}",
elapsed.as_secs_f64() * 1000.0
);
batch_n = 0;
if next_checkpoint < checkpoints.len() && memories_done >= checkpoints[next_checkpoint]
{
let window_memories = memories_done - window_start_memories;
let window_elapsed = window_start_time.elapsed();
let per_doc_ms = window_elapsed.as_secs_f64() * 1000.0 / window_memories as f64;
println!(
"CHECKPOINT corpus={} window_memories={window_memories} window_elapsed_s={:.2} per_doc_ms={:.4}",
checkpoints[next_checkpoint],
window_elapsed.as_secs_f64(),
per_doc_ms
);
next_checkpoint += 1;
window_start_memories = memories_done;
window_start_time = std::time::Instant::now();
}
}
}
}
#[test]
#[ignore]
fn fts_edit_heavy_report() {
const BASE_DOCS: usize = 15_000;
const MARKER_DOCS: usize = 500;
const EDIT_BATCH: usize = 200;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fts_edit_heavy.redb");
let db = Db::open_with(&path, fts_spec()).unwrap();
let scope_id = ScopeId::from_u128(1);
let scope = Scope::Id(scope_id);
let scopes = ScopeSet::of(&[scope_id]);
let mut r = Rng(0xBADC0DE);
let mut ids = Vec::with_capacity(BASE_DOCS);
let mut contents = Vec::with_capacity(BASE_DOCS);
for _ in 0..BASE_DOCS {
ids.push(NodeId::new());
}
let build_start = std::time::Instant::now();
let mut batch: Vec<Op> = Vec::with_capacity(200);
for (i, &id) in ids.iter().enumerate() {
let mut content = fts_sentence(&mut r);
if i >= BASE_DOCS - MARKER_DOCS {
content.push_str(" zzmarker");
}
contents.push(content.clone());
let mut props = Props::new();
props.insert("content".into(), PropValue::Str(content));
batch.push(Op::CreateNode {
id,
scope,
label: "Memory".into(),
props,
});
if batch.len() == 200 || i + 1 == BASE_DOCS {
db.submit(std::mem::take(&mut batch)).unwrap();
}
}
println!(
"base_docs={BASE_DOCS} marker_docs={MARKER_DOCS} build_elapsed_s={:.2}",
build_start.elapsed().as_secs_f64()
);
let before = db.search_text(&scopes, "zzmarker", BASE_DOCS).unwrap();
println!("marker_hits_before_edits={}", before.len());
assert_eq!(
before.len(),
MARKER_DOCS,
"fixture setup: marker must start present in exactly MARKER_DOCS documents"
);
let edit_candidates = BASE_DOCS - MARKER_DOCS;
let checkpoints: Vec<usize> = [1_000usize, 2_000, 4_000, 8_000, 12_000]
.into_iter()
.filter(|&c| c <= edit_candidates)
.collect();
let total_edits = *checkpoints.last().unwrap();
let mut edits_done = 0usize;
let mut window_start = 0usize;
let mut window_time = std::time::Instant::now();
let mut next_checkpoint = 0usize;
let mut checkpoint_per_edit_us: Vec<f64> = Vec::new();
let mut idx = 0usize;
while edits_done < total_edits {
let batch_end = (idx + EDIT_BATCH).min(total_edits);
let mut ops = Vec::with_capacity(batch_end - idx);
for doc_i in idx..batch_end {
let mut props: BTreeMap<String, Option<PropValue>> = BTreeMap::new();
let new_content = format!("{} zzmarker", contents[doc_i]);
props.insert("content".to_string(), Some(PropValue::Str(new_content)));
ops.push(Op::SetNodeProps {
id: ids[doc_i],
props,
});
}
let n = batch_end - idx;
let start = std::time::Instant::now();
db.submit(ops).unwrap();
let elapsed = start.elapsed();
edits_done += n;
idx = batch_end;
println!(
"edits={edits_done}/{total_edits} batch_n={n} batch_elapsed_ms={:.3} per_edit_us={:.1}",
elapsed.as_secs_f64() * 1000.0,
elapsed.as_secs_f64() * 1e6 / n as f64
);
if next_checkpoint < checkpoints.len() && edits_done >= checkpoints[next_checkpoint] {
let window_n = edits_done - window_start;
let window_elapsed = window_time.elapsed();
println!(
"CHECKPOINT edits={} window_n={window_n} window_elapsed_ms={:.2} per_edit_us={:.1}",
checkpoints[next_checkpoint],
window_elapsed.as_secs_f64() * 1000.0,
window_elapsed.as_secs_f64() * 1e6 / window_n as f64
);
checkpoint_per_edit_us.push(window_elapsed.as_secs_f64() * 1e6 / window_n as f64);
next_checkpoint += 1;
window_start = edits_done;
window_time = std::time::Instant::now();
}
}
let after = db.search_text(&scopes, "zzmarker", BASE_DOCS).unwrap();
println!("marker_hits_after_edits={}", after.len());
assert_eq!(
after.len(),
MARKER_DOCS + total_edits,
"every edited doc must now carry the marker term"
);
let first = *checkpoint_per_edit_us
.first()
.expect("checkpoints are non-empty by construction");
let last = *checkpoint_per_edit_us
.last()
.expect("checkpoints are non-empty by construction");
let ratio = last / first;
println!("GATE6B first_window_per_edit_us={first:.1} last_window_per_edit_us={last:.1} ratio={ratio:.2}");
assert!(
ratio <= 1.5,
"GATE 6b FAIL: per-edit cost grew {ratio:.2}x from the 1k to the 12k checkpoint (gate: <= 1.5x)"
);
}
#[test]
#[ignore]
fn chunk_target_experiment_report() {
{
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("chunk_append.redb");
let db = Db::open_with(&path, fts_spec()).unwrap();
let scope = Scope::Id(ScopeId::from_u128(1));
let total = 10_000usize;
let mut r = Rng(0xC0FFEE);
let mut batch: Vec<Op> = Vec::with_capacity(200);
let start = std::time::Instant::now();
let mut last_window_start = std::time::Instant::now();
let mut done = 0usize;
for i in 0..total {
let content = fts_sentence(&mut r);
let mut props = Props::new();
props.insert("content".into(), PropValue::Str(content));
batch.push(Op::CreateNode {
id: NodeId::new(),
scope,
label: "Memory".into(),
props,
});
if batch.len() == 200 || i + 1 == total {
let n = batch.len();
db.submit(std::mem::take(&mut batch)).unwrap();
done += n;
if done == total - 2_000 {
last_window_start = std::time::Instant::now();
}
}
}
let total_elapsed = start.elapsed();
let last_window_elapsed = last_window_start.elapsed();
println!(
"APPEND total_docs={total} total_elapsed_s={:.2} overall_per_doc_ms={:.4} last_2k_per_doc_ms={:.4}",
total_elapsed.as_secs_f64(),
total_elapsed.as_secs_f64() * 1000.0 / total as f64,
last_window_elapsed.as_secs_f64() * 1000.0 / 2_000.0
);
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
let sanity = db.search_text(&scopes, "agent", 10).unwrap();
assert_eq!(
sanity.len(),
10,
"chunk-target search-latency probe must hit k=10 in a 10k-doc near-universal-term corpus"
);
let mut times: Vec<std::time::Duration> = Vec::with_capacity(200);
for _ in 0..200 {
let start = std::time::Instant::now();
db.search_text(&scopes, "agent", 10).unwrap();
times.push(start.elapsed());
}
times.sort();
let p95_idx = (times.len() * 95 / 100).min(times.len() - 1);
println!(
"SEARCH_LATENCY min_us={:.1} median_us={:.1} p95_us={:.1} max_us={:.1}",
times[0].as_secs_f64() * 1e6,
times[times.len() / 2].as_secs_f64() * 1e6,
times[p95_idx].as_secs_f64() * 1e6,
times[times.len() - 1].as_secs_f64() * 1e6
);
}
{
const BASE_DOCS: usize = 4_000;
const MARKER_DOCS: usize = 200;
const EDIT_BATCH: usize = 200;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("chunk_edit.redb");
let db = Db::open_with(&path, fts_spec()).unwrap();
let scope_id = ScopeId::from_u128(1);
let scope = Scope::Id(scope_id);
let mut r = Rng(0xBADC0DE);
let mut ids = Vec::with_capacity(BASE_DOCS);
let mut contents = Vec::with_capacity(BASE_DOCS);
for _ in 0..BASE_DOCS {
ids.push(NodeId::new());
}
let mut batch: Vec<Op> = Vec::with_capacity(200);
for (i, &id) in ids.iter().enumerate() {
let mut content = fts_sentence(&mut r);
if i >= BASE_DOCS - MARKER_DOCS {
content.push_str(" zzmarker");
}
contents.push(content.clone());
let mut props = Props::new();
props.insert("content".into(), PropValue::Str(content));
batch.push(Op::CreateNode {
id,
scope,
label: "Memory".into(),
props,
});
if batch.len() == 200 || i + 1 == BASE_DOCS {
db.submit(std::mem::take(&mut batch)).unwrap();
}
}
let edit_candidates = BASE_DOCS - MARKER_DOCS;
let checkpoints: Vec<usize> = [500usize, 1_500, 3_000]
.into_iter()
.filter(|&c| c <= edit_candidates)
.collect();
let total_edits = *checkpoints.last().unwrap();
let mut edits_done = 0usize;
let mut window_start = 0usize;
let mut window_time = std::time::Instant::now();
let mut next_checkpoint = 0usize;
let mut idx = 0usize;
while edits_done < total_edits {
let batch_end = (idx + EDIT_BATCH).min(total_edits);
let mut ops = Vec::with_capacity(batch_end - idx);
for doc_i in idx..batch_end {
let mut props: BTreeMap<String, Option<PropValue>> = BTreeMap::new();
let new_content = format!("{} zzmarker", contents[doc_i]);
props.insert("content".to_string(), Some(PropValue::Str(new_content)));
ops.push(Op::SetNodeProps {
id: ids[doc_i],
props,
});
}
let n = batch_end - idx;
db.submit(ops).unwrap();
edits_done += n;
idx = batch_end;
if next_checkpoint < checkpoints.len() && edits_done >= checkpoints[next_checkpoint] {
let window_n = edits_done - window_start;
let window_elapsed = window_time.elapsed();
println!(
"EDIT_HEAVY edits={} window_n={window_n} window_elapsed_ms={:.2} per_edit_us={:.1}",
checkpoints[next_checkpoint],
window_elapsed.as_secs_f64() * 1000.0,
window_elapsed.as_secs_f64() * 1e6 / window_n as f64
);
next_checkpoint += 1;
window_start = edits_done;
window_time = std::time::Instant::now();
}
}
}
}