Skip to content

Commit 67df8a6

Browse files
Move acceptance scale checks to Criterion benches
1 parent ae3e415 commit 67df8a6

14 files changed

Lines changed: 747 additions & 143 deletions
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
mod common;
2+
3+
use common::scale::{empty, params};
4+
use contextdb_core::Value;
5+
use contextdb_engine::Database;
6+
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
7+
use std::time::{Duration, Instant};
8+
use uuid::Uuid;
9+
10+
fn seed_fixture() -> Database {
11+
let db = Database::open_memory();
12+
db.execute(
13+
"CREATE TABLE observations (id UUID PRIMARY KEY, obs_type TEXT NOT NULL, source TEXT NOT NULL, embedding VECTOR(3))",
14+
&empty(),
15+
)
16+
.unwrap();
17+
db.execute(
18+
"CREATE TABLE entities (id UUID PRIMARY KEY, entity_type TEXT NOT NULL, name TEXT NOT NULL)",
19+
&empty(),
20+
)
21+
.unwrap();
22+
db.execute(
23+
"CREATE TABLE decisions (id UUID PRIMARY KEY, description TEXT NOT NULL, status TEXT NOT NULL)",
24+
&empty(),
25+
)
26+
.unwrap();
27+
28+
let gate = Uuid::from_u128(1);
29+
let parking = Uuid::from_u128(2);
30+
db.execute(
31+
"INSERT INTO entities (id, entity_type, name) VALUES ($id, 'LOCATION', 'main-gate')",
32+
&params(vec![("id", Value::Uuid(gate))]),
33+
)
34+
.unwrap();
35+
db.execute(
36+
"INSERT INTO entities (id, entity_type, name) VALUES ($id, 'LOCATION', 'parking-lot')",
37+
&params(vec![("id", Value::Uuid(parking))]),
38+
)
39+
.unwrap();
40+
41+
let dec_gate = Uuid::from_u128(10);
42+
let dec_park = Uuid::from_u128(11);
43+
db.execute(
44+
"INSERT INTO decisions (id, description, status) VALUES ($id, 'Alert on unknown person at gate', 'active')",
45+
&params(vec![("id", Value::Uuid(dec_gate))]),
46+
)
47+
.unwrap();
48+
db.execute(
49+
"INSERT INTO decisions (id, description, status) VALUES ($id, 'Log vehicle plates in parking', 'superseded')",
50+
&params(vec![("id", Value::Uuid(dec_park))]),
51+
)
52+
.unwrap();
53+
54+
db.execute(
55+
"INSERT INTO GRAPH (source_id, target_id, edge_type) VALUES ($src, $tgt, 'BASED_ON')",
56+
&params(vec![
57+
("src", Value::Uuid(dec_gate)),
58+
("tgt", Value::Uuid(gate)),
59+
]),
60+
)
61+
.unwrap();
62+
db.execute(
63+
"INSERT INTO GRAPH (source_id, target_id, edge_type) VALUES ($src, $tgt, 'BASED_ON')",
64+
&params(vec![
65+
("src", Value::Uuid(dec_park)),
66+
("tgt", Value::Uuid(parking)),
67+
]),
68+
)
69+
.unwrap();
70+
71+
let obs_g1 = Uuid::from_u128(20);
72+
let obs_g2 = Uuid::from_u128(21);
73+
let obs_p1 = Uuid::from_u128(22);
74+
db.execute(
75+
"INSERT INTO observations (id, obs_type, source, embedding) VALUES ($id, 'person_detected', 'cam-gate', [0.9, 0.1, 0.0])",
76+
&params(vec![("id", Value::Uuid(obs_g1))]),
77+
)
78+
.unwrap();
79+
db.execute(
80+
"INSERT INTO observations (id, obs_type, source, embedding) VALUES ($id, 'person_detected', 'cam-gate', [0.95, 0.05, 0.0])",
81+
&params(vec![("id", Value::Uuid(obs_g2))]),
82+
)
83+
.unwrap();
84+
db.execute(
85+
"INSERT INTO observations (id, obs_type, source, embedding) VALUES ($id, 'vehicle_detected', 'cam-parking', [0.0, 0.95, 0.05])",
86+
&params(vec![("id", Value::Uuid(obs_p1))]),
87+
)
88+
.unwrap();
89+
90+
db.execute(
91+
"INSERT INTO GRAPH (source_id, target_id, edge_type) VALUES ($src, $tgt, 'OBSERVED_ON')",
92+
&params(vec![
93+
("src", Value::Uuid(obs_g1)),
94+
("tgt", Value::Uuid(gate)),
95+
]),
96+
)
97+
.unwrap();
98+
db.execute(
99+
"INSERT INTO GRAPH (source_id, target_id, edge_type) VALUES ($src, $tgt, 'OBSERVED_ON')",
100+
&params(vec![
101+
("src", Value::Uuid(obs_g2)),
102+
("tgt", Value::Uuid(gate)),
103+
]),
104+
)
105+
.unwrap();
106+
db.execute(
107+
"INSERT INTO GRAPH (source_id, target_id, edge_type) VALUES ($src, $tgt, 'OBSERVED_ON')",
108+
&params(vec![
109+
("src", Value::Uuid(obs_p1)),
110+
("tgt", Value::Uuid(parking)),
111+
]),
112+
)
113+
.unwrap();
114+
db
115+
}
116+
117+
fn run_query_and_assert(db: &Database) {
118+
let result = db
119+
.execute(
120+
"WITH similar_obs AS (\
121+
SELECT id FROM observations \
122+
ORDER BY embedding <=> $query_vec \
123+
LIMIT 5\
124+
), \
125+
reached AS (\
126+
SELECT b_id FROM GRAPH_TABLE(\
127+
edges MATCH (a)-[:OBSERVED_ON]->{1,1}(entity)<-[:BASED_ON]-(b) \
128+
WHERE a.id IN (SELECT id FROM similar_obs) \
129+
COLUMNS (b.id AS b_id)\
130+
)\
131+
) \
132+
SELECT d.id, d.description \
133+
FROM decisions d \
134+
INNER JOIN reached r ON d.id = r.b_id \
135+
WHERE d.status = 'active'",
136+
&params(vec![("query_vec", Value::Vector(vec![1.0, 0.0, 0.0]))]),
137+
)
138+
.unwrap();
139+
assert_eq!(result.rows.len(), 1);
140+
let desc = match &result.rows[0][1] {
141+
Value::Text(value) => value,
142+
other => panic!("expected Text for description, got: {other:?}"),
143+
};
144+
assert!(desc.contains("gate"), "expected gate decision, got: {desc}");
145+
assert!(
146+
!desc.contains("parking"),
147+
"superseded parking decision should not appear"
148+
);
149+
}
150+
151+
fn timed_query_and_assert(db: &Database) {
152+
let started = Instant::now();
153+
run_query_and_assert(db);
154+
let elapsed = started.elapsed();
155+
assert!(
156+
elapsed < Duration::from_millis(50),
157+
"query took {}ms, expected < 50ms",
158+
elapsed.as_millis()
159+
);
160+
}
161+
162+
fn bench_three_paradigm_recall_under_50ms(c: &mut Criterion) {
163+
let db = seed_fixture();
164+
run_query_and_assert(&db);
165+
c.bench_function("three_paradigm_recall_under_50ms", |b| {
166+
b.iter_batched(
167+
seed_fixture,
168+
|db| timed_query_and_assert(&db),
169+
BatchSize::LargeInput,
170+
)
171+
});
172+
}
173+
174+
criterion_group!(benches, bench_three_paradigm_recall_under_50ms);
175+
criterion_main!(benches);

benches/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(dead_code)]
22

33
pub mod process;
4+
pub mod scale;
45
pub mod sync;
56
pub mod workloads;

benches/common/scale.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use contextdb_core::Value;
2+
use std::collections::HashMap;
3+
4+
pub fn empty() -> HashMap<String, Value> {
5+
HashMap::new()
6+
}
7+
8+
pub fn params(pairs: Vec<(&str, Value)>) -> HashMap<String, Value> {
9+
pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
10+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
mod common;
2+
3+
use common::scale::{empty, params};
4+
use contextdb_core::Value;
5+
use contextdb_engine::Database;
6+
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
7+
use std::time::{Duration, Instant};
8+
use uuid::Uuid;
9+
10+
const ROWS: i64 = 5_000;
11+
12+
fn seed_entities() -> Database {
13+
let db = Database::open_memory();
14+
db.execute(
15+
"CREATE TABLE entities (id UUID PRIMARY KEY, entity_type TEXT, name TEXT, created_at TIMESTAMP)",
16+
&empty(),
17+
)
18+
.unwrap();
19+
db.execute(
20+
"CREATE INDEX idx_entity_type ON entities (entity_type, created_at DESC, id DESC)",
21+
&empty(),
22+
)
23+
.unwrap();
24+
for i in 0..ROWS {
25+
let entity_type = if i % 2 == 0 { "Service" } else { "Database" };
26+
let name = if i % 5 == 0 {
27+
format!("pay-entity-{i}")
28+
} else {
29+
format!("misc-entity-{i}")
30+
};
31+
db.execute(
32+
"INSERT INTO entities (id, entity_type, name, created_at) VALUES ($id, $et, $n, $ts)",
33+
&params(vec![
34+
("id", Value::Uuid(Uuid::new_v4())),
35+
("et", Value::Text(entity_type.into())),
36+
("n", Value::Text(name)),
37+
("ts", Value::Timestamp(1_700_000_000_000 + i)),
38+
]),
39+
)
40+
.unwrap();
41+
}
42+
db
43+
}
44+
45+
fn run_filtered_query(db: &Database, rows_examined_limit: u64) {
46+
db.__reset_rows_examined();
47+
let result = db
48+
.execute(
49+
"SELECT id, entity_type, name FROM entities \
50+
WHERE entity_type = $et AND name LIKE $pat \
51+
ORDER BY created_at DESC, id DESC",
52+
&params(vec![
53+
("et", Value::Text("Service".into())),
54+
("pat", Value::Text("%pay%".into())),
55+
]),
56+
)
57+
.unwrap();
58+
assert_eq!(result.rows.len(), 500);
59+
assert_eq!(result.trace.physical_plan, "IndexScan");
60+
assert!(result.trace.sort_elided);
61+
assert!(
62+
db.__rows_examined() <= rows_examined_limit,
63+
"IndexScan must not examine the full 5K table; got {}",
64+
db.__rows_examined()
65+
);
66+
}
67+
68+
fn seed_and_assert() {
69+
let db = seed_entities();
70+
run_filtered_query(&db, 3000);
71+
}
72+
73+
fn timed_queries_and_assert(db: &Database) {
74+
let mut samples = Vec::new();
75+
for _ in 0..20 {
76+
let started = Instant::now();
77+
run_filtered_query(db, 3000);
78+
samples.push(started.elapsed());
79+
}
80+
samples.sort();
81+
let p95 = samples[(samples.len() * 95 / 100).min(samples.len() - 1)];
82+
assert!(
83+
p95 <= Duration::from_millis(150),
84+
"p95 {p95:?} exceeds 150ms budget"
85+
);
86+
}
87+
88+
fn bench_cg02_entity_list_filter_under_budget(c: &mut Criterion) {
89+
seed_and_assert();
90+
c.bench_function("cg02_entity_list_filter_under_budget", |b| {
91+
b.iter_batched(
92+
seed_entities,
93+
|db| timed_queries_and_assert(&db),
94+
BatchSize::LargeInput,
95+
)
96+
});
97+
}
98+
99+
criterion_group!(benches, bench_cg02_entity_list_filter_under_budget);
100+
criterion_main!(benches);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
mod common;
2+
3+
use common::scale::{empty, params};
4+
use contextdb_core::Value;
5+
use contextdb_engine::Database;
6+
use criterion::{Criterion, criterion_group, criterion_main};
7+
use std::time::{Duration, Instant};
8+
use tempfile::TempDir;
9+
use uuid::Uuid;
10+
11+
const ROWS: i64 = 100_000;
12+
const SAMPLE: i64 = 42_000;
13+
14+
fn insert_rows(path: &std::path::Path) -> Uuid {
15+
let db = Database::open(path).unwrap();
16+
db.execute(
17+
"CREATE TABLE t (id UUID PRIMARY KEY, bucket INTEGER)",
18+
&empty(),
19+
)
20+
.unwrap();
21+
db.execute("CREATE INDEX idx_bucket ON t (bucket)", &empty())
22+
.unwrap();
23+
let mut sampled_id = None;
24+
for i in 0..ROWS {
25+
if i % 1_000 == 0 {
26+
db.execute("BEGIN", &empty()).unwrap();
27+
}
28+
let id = Uuid::new_v4();
29+
if i == SAMPLE {
30+
sampled_id = Some(id);
31+
}
32+
db.execute(
33+
"INSERT INTO t (id, bucket) VALUES ($id, $b)",
34+
&params(vec![
35+
("id", Value::Uuid(id)),
36+
("b", Value::Int64(i % 10_000)),
37+
]),
38+
)
39+
.unwrap();
40+
if i % 1_000 == 999 {
41+
db.execute("COMMIT", &empty()).unwrap();
42+
}
43+
}
44+
sampled_id.unwrap()
45+
}
46+
47+
fn assert_reopened(path: &std::path::Path, sampled_id: Uuid) {
48+
let db = Database::open(path).unwrap();
49+
let count = db.execute("SELECT COUNT(*) FROM t", &empty()).unwrap();
50+
assert_eq!(count.rows, vec![vec![Value::Int64(ROWS)]]);
51+
let r = db
52+
.execute("SELECT id FROM t WHERE bucket = 42", &empty())
53+
.unwrap();
54+
assert_eq!(r.trace.physical_plan, "IndexScan");
55+
assert_eq!(r.rows.len(), 10);
56+
assert!(db.__rows_examined() <= 50, "got {}", db.__rows_examined());
57+
let sampled = db
58+
.execute(
59+
"SELECT id, bucket FROM t WHERE id = $id",
60+
&params(vec![("id", Value::Uuid(sampled_id))]),
61+
)
62+
.unwrap();
63+
assert_eq!(sampled.rows.len(), 1);
64+
assert_eq!(sampled.rows[0][0], Value::Uuid(sampled_id));
65+
assert_eq!(sampled.rows[0][1], Value::Int64(SAMPLE % 10_000));
66+
}
67+
68+
fn seed_reopen_and_assert() {
69+
let tmp = TempDir::new().unwrap();
70+
let path = tmp.path().join("db");
71+
let sampled_id = insert_rows(&path);
72+
assert_reopened(&path, sampled_id);
73+
}
74+
75+
fn timed_seed_reopen_and_assert() {
76+
let tmp = TempDir::new().unwrap();
77+
let path = tmp.path().join("db");
78+
let insert_started = Instant::now();
79+
let sampled_id = insert_rows(&path);
80+
let insert_elapsed = insert_started.elapsed();
81+
assert!(
82+
insert_elapsed < Duration::from_secs(900),
83+
"100K file-backed INSERTs must finish in <900s, took {insert_elapsed:?}"
84+
);
85+
let reopen_started = Instant::now();
86+
assert_reopened(&path, sampled_id);
87+
let reopen_elapsed = reopen_started.elapsed();
88+
assert!(
89+
reopen_elapsed < Duration::from_secs(10),
90+
"100K-row reopen must complete in <10s; took {reopen_elapsed:?}"
91+
);
92+
}
93+
94+
fn bench_file_backed_100k_rows_open_upper_bound(c: &mut Criterion) {
95+
seed_reopen_and_assert();
96+
c.bench_function("file_backed_100k_rows_open_upper_bound", |b| {
97+
b.iter(timed_seed_reopen_and_assert)
98+
});
99+
}
100+
101+
criterion_group!(benches, bench_file_backed_100k_rows_open_upper_bound);
102+
criterion_main!(benches);

0 commit comments

Comments
 (0)