Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6cf7dc0

Browse files
committed
chore(bench): add index acceleration and query implementations harness
1 parent 92a6fe7 commit 6cf7dc0

5 files changed

Lines changed: 879 additions & 2 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# lora-database Benchmarks
2+
3+
Benchmarks are split by intent:
4+
5+
| Target | Purpose |
6+
| --- | --- |
7+
| `query_implementations` | Coverage-oriented query-language suite. Add representative benches here when a tested query implementation changes or lands. |
8+
| `scale` | Same query families across larger graph sizes. |
9+
| `realistic` | End-to-end domain-shaped workloads that combine several operators. |
10+
| `perf_smoke` | Short CI canary for large regressions. |
11+
| `wal` | Durability and recovery overhead. |
12+
| `concurrent` | Concurrent read/write workload behavior. |
13+
| `concurrency_guard` | Focused guardrail suite for snapshot, OCC, and WAL concurrency changes. |
14+
| `engine`, `advanced`, `temporal_spatial` | Older deep-dive suites kept for historical comparison and detailed performance docs. Prefer `query_implementations` for new query-feature coverage. |
15+
16+
Run the coverage suite:
17+
18+
```bash
19+
cargo bench -p lora-database --bench query_implementations
20+
```
21+
22+
Run every registered database benchmark:
23+
24+
```bash
25+
cargo bench -p lora-database --benches
26+
```

crates/lora-database/benches/concurrency_guard.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,61 @@ fn bench_concurrency_guard(c: &mut Criterion) {
116116
});
117117
}
118118

119+
// Large materialized read paths. These catch whether intra-query read
120+
// parallelism is actually paying off instead of just adding scheduling
121+
// overhead to normal `execute()`.
122+
{
123+
let db = build_node_graph(Scale::LARGE);
124+
group.bench_function("read_scan_50k", |b| {
125+
b.iter(|| {
126+
black_box(db.service.execute("MATCH (n) RETURN n.id", opts()).unwrap());
127+
});
128+
});
129+
130+
group.bench_function("read_label_project_50k", |b| {
131+
b.iter(|| {
132+
black_box(
133+
db.service
134+
.execute("MATCH (n:Node) RETURN n.id, n.name, n.value", opts())
135+
.unwrap(),
136+
);
137+
});
138+
});
139+
140+
group.bench_function("read_scan_filter_project_50k", |b| {
141+
b.iter(|| {
142+
black_box(
143+
db.service
144+
.execute("MATCH (n:Node) WHERE n.value >= 0 RETURN n.id", opts())
145+
.unwrap(),
146+
);
147+
});
148+
});
149+
150+
group.bench_function("read_scan_filter_half_project_50k", |b| {
151+
b.iter(|| {
152+
black_box(
153+
db.service
154+
.execute("MATCH (n:Node) WHERE n.value >= 50 RETURN n.id", opts())
155+
.unwrap(),
156+
);
157+
});
158+
});
159+
160+
group.bench_function("read_map_projection_50k", |b| {
161+
b.iter(|| {
162+
black_box(
163+
db.service
164+
.execute(
165+
"MATCH (n:Node) RETURN n { .id, .name, .value } AS node",
166+
opts(),
167+
)
168+
.unwrap(),
169+
);
170+
});
171+
});
172+
}
173+
119174
// Live read stream: pins an Arc snapshot and drops after one row.
120175
{
121176
let db = build_node_graph(Scale::SMALL);
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//! Index-acceleration benchmarks.
2+
//!
3+
//! Pairs every read query against two seeded copies of the same graph —
4+
//! one with a property/text/point index, one without — so the runtime
5+
//! delta directly attributes to the cost-based rewrite picking up the
6+
//! index. Covers the four operators added in v0.8 plus their rel-side
7+
//! mirrors:
8+
//!
9+
//! * `NodeByPropertyRangeScan` ← `WHERE n.prop > X`
10+
//! * `NodeByTextScan` ← `WHERE n.prop STARTS WITH …`
11+
//! * `RelByPropertyRangeScan` ← `MATCH ()-[r:T]->() WHERE r.prop > X`
12+
//! * `RelByTextScan` ← `MATCH ()-[r:T]->() WHERE r.prop STARTS WITH …`
13+
//!
14+
//! Run with:
15+
//! `cargo bench -p lora-database --bench index_acceleration`
16+
//!
17+
//! Each scenario seeds once and is reused across iterations; only the
18+
//! query is measured. Set `LORA_BENCH_NODES` / `LORA_BENCH_RELS` in the
19+
//! environment to override the defaults (10k / 50k) for a quick local
20+
//! sweep, e.g. `LORA_BENCH_NODES=2000 LORA_BENCH_RELS=8000 cargo bench …`.
21+
22+
mod fixtures;
23+
24+
use std::env;
25+
use std::hint::black_box;
26+
use std::time::Duration;
27+
28+
use criterion::{criterion_group, criterion_main, Criterion};
29+
use fixtures::BenchDb;
30+
use lora_database::{ExecuteOptions, ResultFormat};
31+
32+
const DEFAULT_NODES: usize = 10_000;
33+
const DEFAULT_RELS: usize = 50_000;
34+
const SEED_BATCH: usize = 2_000;
35+
36+
fn opts() -> Option<ExecuteOptions> {
37+
Some(ExecuteOptions {
38+
format: ResultFormat::Rows,
39+
})
40+
}
41+
42+
fn env_usize(key: &str, default: usize) -> usize {
43+
env::var(key)
44+
.ok()
45+
.and_then(|v| v.parse().ok())
46+
.unwrap_or(default)
47+
}
48+
49+
fn bench_config() -> Criterion {
50+
// Rel-side scenarios at default scale need ~3 ms per iter; 4 s of
51+
// measurement keeps the 30-sample target reachable without a
52+
// warning. Override with `--measurement-time` for shorter runs.
53+
Criterion::default()
54+
.warm_up_time(Duration::from_millis(500))
55+
.measurement_time(Duration::from_millis(4_000))
56+
.sample_size(30)
57+
}
58+
59+
/// Seed `n` `:Person` nodes and `m` `:KNOWS` relationships connecting
60+
/// random pairs. Properties are chosen so:
61+
///
62+
/// * `n.age` spans 0..100 → range queries hit ~half the corpus when
63+
/// unindexed, a bounded slice when indexed.
64+
/// * `n.name` is `'p_<i>'` → STARTS WITH 'p_5' matches ~1/10 of the
65+
/// corpus, exercising the trigram path under load.
66+
/// * `r.since` spans 1990..2030 → range queries split the rel set.
67+
/// * `r.note` is `'note_<i>'` → STARTS WITH 'note_5' matches ~1/10.
68+
///
69+
/// The src/dst pairings are deterministic (seeded by index), so two
70+
/// builds of the same `(nodes, rels)` produce identical edges — the
71+
/// indexed and non-indexed databases see exactly the same data.
72+
fn seed_graph(db: &BenchDb, nodes: usize, rels: usize) {
73+
let mut i = 0;
74+
while i < nodes {
75+
let end = (i + SEED_BATCH).min(nodes);
76+
db.run(&format!(
77+
"UNWIND range({i}, {}) AS i \
78+
CREATE (:Person {{id: i, age: i % 100, name: 'p_' + toString(i)}})",
79+
end - 1
80+
));
81+
i = end;
82+
}
83+
84+
let mut j = 0;
85+
while j < rels {
86+
let end = (j + SEED_BATCH).min(rels);
87+
db.run(&format!(
88+
"UNWIND range({j}, {}) AS i \
89+
MATCH (a:Person {{id: i % {nodes}}}), (b:Person {{id: (i * 7 + 3) % {nodes}}}) \
90+
CREATE (a)-[:KNOWS {{since: 1990 + i % 41, note: 'note_' + toString(i % 100), idx: i}}]->(b)",
91+
end - 1
92+
));
93+
j = end;
94+
}
95+
}
96+
97+
/// Build two databases with identical data but different index
98+
/// catalogs: `(without_index, with_index)`. Index DDL runs *after*
99+
/// the seed so the index is built once over the existing corpus
100+
/// rather than incrementally per CREATE.
101+
fn build_pair<F: Fn(&BenchDb)>(nodes: usize, rels: usize, install_index: F) -> (BenchDb, BenchDb) {
102+
let plain = BenchDb::with_capacity_hint(nodes, rels);
103+
seed_graph(&plain, nodes, rels);
104+
105+
let indexed = BenchDb::with_capacity_hint(nodes, rels);
106+
seed_graph(&indexed, nodes, rels);
107+
install_index(&indexed);
108+
109+
(plain, indexed)
110+
}
111+
112+
fn run(db: &BenchDb, query: &str) {
113+
black_box(db.service.execute(query, opts()).unwrap());
114+
}
115+
116+
fn bench_node_range(c: &mut Criterion) {
117+
let nodes = env_usize("LORA_BENCH_NODES", DEFAULT_NODES);
118+
let rels = env_usize("LORA_BENCH_RELS", DEFAULT_RELS);
119+
120+
let (plain, indexed) = build_pair(nodes, rels, |db| {
121+
db.run("CREATE INDEX person_age FOR (n:Person) ON (n.age)");
122+
});
123+
124+
let query = "MATCH (n:Person) WHERE n.age > 95 RETURN n.id";
125+
126+
let mut group = c.benchmark_group("index_acceleration/node_range");
127+
group.bench_function("without_index", |b| b.iter(|| run(&plain, query)));
128+
group.bench_function("with_index", |b| b.iter(|| run(&indexed, query)));
129+
group.finish();
130+
}
131+
132+
fn bench_node_text(c: &mut Criterion) {
133+
let nodes = env_usize("LORA_BENCH_NODES", DEFAULT_NODES);
134+
let rels = env_usize("LORA_BENCH_RELS", DEFAULT_RELS);
135+
136+
let (plain, indexed) = build_pair(nodes, rels, |db| {
137+
db.run("CREATE TEXT INDEX person_name FOR (n:Person) ON (n.name)");
138+
});
139+
140+
let query = "MATCH (n:Person) WHERE n.name STARTS WITH 'p_99' RETURN n.id";
141+
142+
let mut group = c.benchmark_group("index_acceleration/node_text");
143+
group.bench_function("without_index", |b| b.iter(|| run(&plain, query)));
144+
group.bench_function("with_index", |b| b.iter(|| run(&indexed, query)));
145+
group.finish();
146+
}
147+
148+
fn bench_rel_range(c: &mut Criterion) {
149+
let nodes = env_usize("LORA_BENCH_NODES", DEFAULT_NODES);
150+
let rels = env_usize("LORA_BENCH_RELS", DEFAULT_RELS);
151+
152+
let (plain, indexed) = build_pair(nodes, rels, |db| {
153+
db.run("CREATE INDEX knows_since FOR ()-[r:KNOWS]-() ON (r.since)");
154+
});
155+
156+
let query = "MATCH ()-[r:KNOWS]->() WHERE r.since > 2025 RETURN r.idx";
157+
158+
let mut group = c.benchmark_group("index_acceleration/rel_range");
159+
group.bench_function("without_index", |b| b.iter(|| run(&plain, query)));
160+
group.bench_function("with_index", |b| b.iter(|| run(&indexed, query)));
161+
group.finish();
162+
}
163+
164+
fn bench_rel_text(c: &mut Criterion) {
165+
let nodes = env_usize("LORA_BENCH_NODES", DEFAULT_NODES);
166+
let rels = env_usize("LORA_BENCH_RELS", DEFAULT_RELS);
167+
168+
let (plain, indexed) = build_pair(nodes, rels, |db| {
169+
db.run("CREATE TEXT INDEX knows_note FOR ()-[r:KNOWS]-() ON (r.note)");
170+
});
171+
172+
let query = "MATCH ()-[r:KNOWS]->() WHERE r.note STARTS WITH 'note_9' RETURN r.idx";
173+
174+
let mut group = c.benchmark_group("index_acceleration/rel_text");
175+
group.bench_function("without_index", |b| b.iter(|| run(&plain, query)));
176+
group.bench_function("with_index", |b| b.iter(|| run(&indexed, query)));
177+
group.finish();
178+
}
179+
180+
criterion_group! {
181+
name = index_acceleration;
182+
config = bench_config();
183+
targets =
184+
bench_node_range,
185+
bench_node_text,
186+
bench_rel_range,
187+
bench_rel_text,
188+
}
189+
criterion_main!(index_acceleration);

crates/lora-database/benches/perf_smoke.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
//! This is a deliberately tiny Criterion suite used as a CI "canary": it is
44
//! meant to detect obvious, large performance regressions (≥3× slower) in
55
//! core engine paths. It is **not** a source of truth for performance
6-
//! numbers — see `engine`, `scale`,
7-
//! `advanced`, and `temporal_spatial` for that.
6+
//! numbers — see `query_implementations`, `scale`, `realistic`, and the
7+
//! WAL/concurrency suites for that.
88
//!
99
//! Run locally with:
1010
//! `cargo bench -p lora-database --bench perf_smoke`

0 commit comments

Comments
 (0)