Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/lance-graph-benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lance-graph = { path = "../lance-graph", version = "0.2.0" }
lance = "13.0.0"
arrow-array = "58"
arrow-schema = "58"
datafusion = { version = "54", default-features = false }
criterion = { version = "0.5", features = ["async", "async_tokio", "html_reports"] }
futures = "0.3"
tempfile = "3"
Expand Down
24 changes: 24 additions & 0 deletions crates/lance-graph-benches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,27 @@ cargo bench -p lance-graph-benches --bench graph_execution

This crate is not published to crates.io and is excluded from releases.
It exists solely for performance testing during development.


## Query template reuse

Measure fresh parsing/planning/execution against reuse of an unoptimized scalar-parameter template on the
same 10,000-row Arrow snapshot. Both paths optimize and build a physical plan for each execution. The harness
checks identical Arrow results, row count and ID checksum before timing; catalog construction is excluded.

```bash
cargo bench -p lance-graph-benches --bench graph_execution -- query_templates --warm-up-time 1
```

For a stage breakdown on a separate fixed 1,000-row scan, run the optional core diagnostic:

```bash
cargo test --release -p lance-graph profile_query_planning_stages -- --ignored --nocapture
```

It reports elapsed means for 100 iterations of parsing, semantic analysis, graph logical planning,
DataFusion logical planning, optimization, physical planning, execution and cached binding. Physical timing
calls DataFusion's query planner directly so optimization is counted only once. It verifies 990 result rows
and their ID checksum. These local measurements do not replace the two SF1 suites required for performance
submissions; record runtime, hardware, revision and build profile with any reported numbers. The engine's
Python cache also includes dictionary conversion and lookup, which this Rust microbenchmark does not time.
72 changes: 71 additions & 1 deletion crates/lance-graph-benches/benches/graph_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,75 @@ fn bench_cypher_execution(c: &mut Criterion) {
group.finish();
}

criterion_group!(benches, bench_cypher_execution);
// A fixed snapshot isolates template reuse from dataset/catalog construction.
// The result count and checksum are checked outside the timed loops.
fn bench_query_templates(c: &mut Criterion) {
use datafusion::datasource::{DefaultTableSource, MemTable};
use datafusion::execution::context::SessionContext;
use lance_graph::{GraphSourceCatalog, InMemoryCatalog};
let rt = tokio::runtime::Runtime::new().unwrap();
let context = SessionContext::new();
let batch = make_people_batch(10_000);
let provider = Arc::new(MemTable::try_new(batch.schema(), vec![vec![batch]]).unwrap());
let catalog: Arc<dyn GraphSourceCatalog> = Arc::new(
InMemoryCatalog::new()
.with_node_source("Person", Arc::new(DefaultTableSource::new(provider))),
);
let config = GraphConfig::builder()
.node_table("Person", "person_id")
.build()
.unwrap();
let text = "MATCH (p:Person) WHERE p.age >= $minimum RETURN p.person_id AS id ORDER BY id";
let fresh = || {
CypherQuery::new(text)
.unwrap()
.with_config(config.clone())
.with_parameter("minimum", 50)
};
let mut template = fresh();
assert!(template.prepare_cached_plan(catalog.clone()).unwrap());
let expected = rt
.block_on(fresh().execute_with_catalog_and_context(catalog.clone(), context.clone()))
.unwrap();
let cached = rt
.block_on(template.execute_with_catalog_and_context(catalog.clone(), context.clone()))
.unwrap();
assert_eq!(cached, expected);
let expected_ids: Vec<i32> = (0..10_000).filter(|id| 20 + id % 60 >= 50).collect();
assert_eq!(cached.num_rows(), expected_ids.len());
let ids = cached
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap();
assert_eq!(
ids.values().iter().map(|id| i64::from(*id)).sum::<i64>(),
expected_ids.iter().map(|id| i64::from(*id)).sum::<i64>()
);
let mut group = c.benchmark_group("query_templates");
group.bench_function("cold_parse_plan_execute", |b| {
b.iter(|| {
black_box(
rt.block_on(
fresh().execute_with_catalog_and_context(catalog.clone(), context.clone()),
)
.unwrap(),
);
})
});
group.bench_function("cached_bind_plan_execute", |b| {
b.iter(|| {
let query = template.clone().with_parameter("minimum", black_box(50));
black_box(
rt.block_on(
query.execute_with_catalog_and_context(catalog.clone(), context.clone()),
)
.unwrap(),
);
})
});
group.finish();
}

criterion_group!(benches, bench_cypher_execution, bench_query_templates);
criterion_main!(benches);
Loading
Loading