Benchmarks
This document presents comprehensive performance measurements for Uni across various workloads including ingestion, querying, graph traversal, and vector search. All benchmarks are reproducible using the included benchmark suite.
Executive Summary
| Workload |
Performance |
Context |
| Ingestion |
1.8M vertices/sec |
Batched L0 writes |
| Point Lookup |
2.9ms |
Indexed property access |
| 1-Hop Traversal |
4.7ms |
CSR adjacency cache |
| Vector KNN (k=10) |
1.8ms |
HNSW index |
| Hybrid Query |
215ms |
Vector + Graph + Filter |
Test Environment
Hardware Configuration
┌─────────────────────────────────────────────────────────────────────────────┐
│ BENCHMARK HARDWARE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Development Machine: │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ CPU: AMD Ryzen 9 5900X (12 cores, 24 threads) │ │
│ │ Memory: 64 GB DDR4-3600 │ │
│ │ Storage: Samsung 980 PRO NVMe (7,000 MB/s read) │ │
│ │ OS: Ubuntu 22.04, kernel 5.15 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ Cloud VM (Comparable): │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Instance: AWS c6i.4xlarge (16 vCPU, 32 GB RAM) │ │
│ │ Storage: gp3 EBS (16,000 IOPS, 1,000 MB/s) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Dataset Characteristics
| Dataset |
Vertices |
Edges |
Properties |
Vector Dim |
| Small |
10K |
50K |
5 per vertex |
128 |
| Medium |
100K |
500K |
5 per vertex |
384 |
| Large |
1M |
5M |
5 per vertex |
768 |
| XLarge |
10M |
50M |
5 per vertex |
768 |
Raw Write Throughput
Measuring writes to the in-memory L0 buffer:
┌─────────────────────────────────────────────────────────────────────────────┐
│ INGESTION THROUGHPUT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Batch Size: 1,000 vertices │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Time (µs) │ │
│ │ 800 ┤ │ │
│ │ │ │ │
│ │ 600 ┤ ┌───┐ │ │
│ │ │ │███│ │ │
│ │ 400 ┤ │███│ ┌───┐ │ │
│ │ │ │███│ │███│ │ │
│ │ 200 ┤ │███│ │███│ ┌───┐ ┌───┐ │ │
│ │ │ │███│ │███│ │███│ │███│ │ │
│ │ 0 ┼────┴───┴────┴───┴────┴───┴────┴───┴──── │ │
│ │ Insert Insert Insert Insert │ │
│ │ (cold) (warm) (batch) (batch+WAL) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Results (1K vertices): │
│ ├── Cold insert (first batch): ~720 µs │
│ ├── Warm insert (cached): ~420 µs │
│ ├── Batch insert (no WAL sync): ~180 µs │
│ └── Batch insert (WAL sync): ~550 µs │
│ │
│ Throughput: 1.8M vertices/sec (batch, no sync) │
│ 550K vertices/sec (batch, sync WAL) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FLUSH LATENCY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Flush 10K vertices to Lance: │
│ │
│ Phase Time (ms) Percentage │
│ ──────────────────────────────────────────── │
│ Serialize 12.4 19.7% │
│ Arrow conversion 18.2 28.9% │
│ Lance write 28.1 44.6% │
│ Index update 4.3 6.8% │
│ ──────────────────────────────────────────── │
│ Total 63.0 ms 100% │
│ │
│ Throughput: ~160K vertices/sec (to persistent storage) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Scaling with Data Size
| Data Size |
L0 Insert |
L0→L1 Flush |
L1→L2 Compact |
| 1K vertices |
550 µs |
6.3 ms |
N/A |
| 10K vertices |
5.2 ms |
63 ms |
180 ms |
| 100K vertices |
52 ms |
640 ms |
2.1 s |
| 1M vertices |
520 ms |
6.4 s |
25 s |
Point Lookups
Single vertex retrieval by indexed property:
┌─────────────────────────────────────────────────────────────────────────────┐
│ POINT LOOKUP LATENCY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper {id: 'paper_12345'}) RETURN p │
│ │
│ Index Type P50 P90 P99 P99.9 │
│ ───────────────────────────────────────────────────────── │
│ Hash index 2.1 ms 2.8 ms 4.2 ms 8.1 ms │
│ BTree index 2.4 ms 3.1 ms 4.8 ms 9.3 ms │
│ No index (scan) 85 ms 120 ms 180 ms 250 ms │
│ │
│ Dataset: 1M vertices │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Range Queries
┌─────────────────────────────────────────────────────────────────────────────┐
│ RANGE QUERY PERFORMANCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper) WHERE p.year >= 2020 AND p.year <= 2023 │
│ RETURN p.title │
│ │
│ Selectivity Rows Returned Index Time Scan Time Speedup │
│ ───────────────────────────────────────────────────────────────── │
│ 1% (10K) 10,000 12 ms 85 ms 7.1x │
│ 5% (50K) 50,000 35 ms 95 ms 2.7x │
│ 10% (100K) 100,000 58 ms 102 ms 1.8x │
│ 50% (500K) 500,000 210 ms 180 ms 0.9x (scan wins)│
│ │
│ Dataset: 1M vertices │
│ Takeaway: Index wins for selectivity < 30% │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Aggregation Queries
┌─────────────────────────────────────────────────────────────────────────────┐
│ AGGREGATION PERFORMANCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper) RETURN p.venue, COUNT(*) AS count │
│ ORDER BY count DESC │
│ │
│ Dataset Size Groups Aggregate Sort Total │
│ ───────────────────────────────────────────────────────── │
│ 100K 50 28 ms 2 ms 30 ms │
│ 1M 50 185 ms 3 ms 188 ms │
│ 1M 10K 320 ms 45 ms 365 ms │
│ 1M 100K 580 ms 120 ms 700 ms │
│ │
│ Takeaway: Hash aggregation scales linearly with input size │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Single-Hop Traversal
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1-HOP TRAVERSAL LATENCY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper)-[:CITES]->(cited) │
│ WHERE p.id = 'paper_12345' │
│ RETURN cited.title │
│ │
│ Cache State P50 P90 P99 Notes │
│ ───────────────────────────────────────────────────────── │
│ Cold cache 8.2 ms 12.1 ms 18.5 ms Load from Lance │
│ Warm cache 4.7 ms 5.8 ms 8.1 ms CSR in memory │
│ Hot path 2.1 ms 2.8 ms 4.2 ms Repeated query │
│ │
│ Average degree: 5 edges per vertex │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Multi-Hop Traversal
┌─────────────────────────────────────────────────────────────────────────────┐
│ MULTI-HOP TRAVERSAL SCALING │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (start)-[:CITES*N]->(end) RETURN DISTINCT end │
│ │
│ Latency (ms) │
│ 40 ┤ ┌───┐ │
│ │ │ │ │
│ 35 ┤ │ │ │
│ │ │ │ │
│ 30 ┤ │ │ │
│ │ │ │ │
│ 25 ┤ │ │ │
│ │ │ │ │
│ 20 ┤ ┌───┐ │ │ │
│ │ │ │ │ │ │
│ 15 ┤ ┌───┐ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ 10 ┤ ┌───┐ │ │ │ │ │ │ │
│ │ ┌───┐ │ │ │ │ │ │ │ │ │
│ 5 ┤ ┌───┐ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ 0 ┼──────┴───┴────┴───┴────┴───┴────┴───┴────┴───┴────┴───┴── │
│ 1-hop 2-hop 3-hop 4-hop 5-hop 6-hop │
│ │
│ Hops Vertices Visited Latency Throughput │
│ ───────────────────────────────────────────────── │
│ 1 5 4.7 ms 1.1K v/s │
│ 2 25 6.7 ms 3.7K v/s │
│ 3 125 9.0 ms 13.9K v/s │
│ 4 625 15.2 ms 41.1K v/s │
│ 5 3,125 22.8 ms 137K v/s │
│ 6 15,625 38.5 ms 406K v/s │
│ │
│ Note: Assuming avg degree = 5, no duplicates │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Traversal with Filters
┌─────────────────────────────────────────────────────────────────────────────┐
│ FILTERED TRAVERSAL PERFORMANCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper)-[:CITES]->(cited:Paper) │
│ WHERE p.year > 2020 AND cited.year > 2018 │
│ RETURN cited.title │
│ │
│ Strategy Latency Notes │
│ ───────────────────────────────────────────────── │
│ Filter-then-traverse 12.5 ms Filter p first, then traverse │
│ Traverse-then-filter 28.3 ms Traverse all, filter cited │
│ Dual pushdown 8.2 ms Push both filters down │
│ │
│ Dataset: 1M papers, 5M citations │
│ Filter selectivity: p.year > 2020 = 30%, cited.year > 2018 = 60% │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
KNN Search (HNSW)
┌─────────────────────────────────────────────────────────────────────────────┐
│ VECTOR KNN LATENCY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: CALL db.idx.vector.query('Paper', 'embedding', $vec, 10) │
│ │
│ Dataset: 1M vectors, 768 dimensions, HNSW index │
│ │
│ k P50 P90 P99 Recall@k │
│ ───────────────────────────────────────────────── │
│ 10 1.8 ms 2.4 ms 3.8 ms 0.95 │
│ 50 2.9 ms 3.8 ms 5.2 ms 0.94 │
│ 100 4.2 ms 5.5 ms 7.8 ms 0.93 │
│ 500 12.5 ms 16.2 ms 22.1 ms 0.91 │
│ │
│ HNSW Parameters: M=32, ef_construction=200, ef_search=100 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HNSW vs IVF_PQ
┌─────────────────────────────────────────────────────────────────────────────┐
│ INDEX COMPARISON (k=10) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Dataset: 1M vectors, 768 dimensions │
│ │
│ Index Type Build Time Memory Latency Recall │
│ ───────────────────────────────────────────────────────── │
│ HNSW 45 min 2.4 GB 1.8 ms 0.95 │
│ IVF_PQ 12 min 180 MB 3.2 ms 0.88 │
│ Brute Force N/A 2.9 GB 85 ms 1.00 │
│ │
│ Recommendation: │
│ • HNSW: Best recall, moderate memory │
│ • IVF_PQ: Low memory, acceptable recall │
│ • Brute Force: Only for small datasets (<100K) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Vector Search Scaling
┌─────────────────────────────────────────────────────────────────────────────┐
│ SCALING WITH DATASET SIZE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HNSW Index, k=10, 768 dimensions │
│ │
│ Dataset Size Latency (P50) Memory (Index) Build Time │
│ ───────────────────────────────────────────────────────── │
│ 100K 0.8 ms 240 MB 4 min │
│ 1M 1.8 ms 2.4 GB 45 min │
│ 10M 3.2 ms 24 GB 8 hours │
│ │
│ Observation: Latency scales O(log n) due to HNSW structure │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Vector + Graph Queries
┌─────────────────────────────────────────────────────────────────────────────┐
│ HYBRID QUERY BREAKDOWN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: CALL db.idx.vector.query('Paper', 'embedding', $vec, 20) │
│ YIELD node AS paper │
│ MATCH (paper)-[:CITES]->(cited) │
│ WHERE cited.year > 2020 │
│ RETURN cited.title │
│ │
│ Phase Time Rows In Rows Out │
│ ───────────────────────────────────────────────────── │
│ Vector Search 2.1 ms 1M 20 │
│ Traverse 45 ms 20 ~100 │
│ Filter 8 ms ~100 ~60 │
│ Project 2 ms ~60 ~60 │
│ ───────────────────────────────────────────────────── │
│ Total ~57 ms │
│ │
│ Note: Traversal dominates due to cold cache for cited papers │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Complex Hybrid Query
┌─────────────────────────────────────────────────────────────────────────────┐
│ COMPLEX HYBRID QUERY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: // Find papers similar to query, get their authors' other papers │
│ CALL db.idx.vector.query('Paper', 'embedding', $vec, 10) │
│ YIELD node AS similar │
│ MATCH (similar)-[:AUTHORED_BY]->(author) │
│ MATCH (author)<-[:AUTHORED_BY]-(other:Paper) │
│ WHERE other.year > 2020 │
│ RETURN DISTINCT other.title, author.name │
│ LIMIT 50 │
│ │
│ Execution Timeline: │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 0ms 50ms 100ms 150ms 200ms 250ms │ │
│ │ ├───────────┼───────────┼───────────┼───────────┼───────────┤ │ │
│ │ │▓▓▓│ Vector (2ms) │ │
│ │ │▓▓▓▓▓▓▓▓▓▓▓▓│ Traverse1 (35ms)│ │
│ │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ Traverse2 (120ms)│
│ │ │▓▓▓▓▓▓▓▓│ Filter (45ms) │ │
│ │ │▓│ Project (8ms) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ Total: ~215 ms │
│ │
│ Bottleneck: Second traversal (author → other papers) │
│ Optimization: Add LIMIT earlier, warm adjacency cache │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Read Throughput
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONCURRENT READ THROUGHPUT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Query: MATCH (p:Paper) WHERE p.year = 2023 RETURN p.title LIMIT 10 │
│ Dataset: 1M vertices │
│ │
│ QPS │
│ 1200 ┤ ┌───────────────────── │
│ │ ┌─────┘ │
│ 1000 ┤ ┌─────┘ │
│ │ ┌─────┘ │
│ 800 ┤ ┌─────┘ │
│ │ ┌─────┘ │
│ 600 ┤ ┌─────┘ │
│ │┌─────┘ │
│ 400 ┤│ │
│ ││ │
│ 200 ┤│ │
│ ││ │
│ 0 ┼┴─────────────────────────────────────────────────────────────── │
│ 1 2 4 8 12 16 24 32 (concurrent readers) │
│ │
│ Threads QPS Avg Latency P99 Latency │
│ ───────────────────────────────────────────────── │
│ 1 180 5.5 ms 8.2 ms │
│ 4 650 6.1 ms 12.5 ms │
│ 8 920 8.7 ms 18.2 ms │
│ 16 1,050 15.2 ms 35.1 ms │
│ 32 1,120 28.5 ms 65.2 ms │
│ │
│ Note: Scales well up to ~16 readers, then contention increases │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Write Impact on Reads
┌─────────────────────────────────────────────────────────────────────────────┐
│ READ LATENCY UNDER WRITE LOAD │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Write Rate (vertices/sec) Read P50 Read P99 Notes │
│ ───────────────────────────────────────────────────────── │
│ 0 (no writes) 5.5 ms 8.2 ms Baseline │
│ 1,000 5.6 ms 9.1 ms Minimal impact │
│ 10,000 5.8 ms 11.5 ms Slight increase │
│ 50,000 6.2 ms 15.2 ms L0 buffer growing │
│ 100,000 7.5 ms 22.1 ms Frequent flushes │
│ │
│ Takeaway: Single-writer model ensures reads remain consistent │
│ L0 flushes cause brief latency spikes │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Uni includes native implementations of common graph algorithms, optimized for the CSR adjacency cache.
| Algorithm |
Complexity |
Notes |
| PageRank |
O(E) per iter |
Parallel execution |
| WCC |
O(V + E) |
Union-Find with path compression |
| Louvain |
O(E) per iter |
Multi-level community detection |
| Label Propagation |
O(E) per iter |
Fast community detection |
| Triangle Count |
O(E^1.5) |
SIMD-optimized set intersection |
| Betweenness |
O(VE) |
Sampling-based approximation available |
Running Benchmarks
Built-in Benchmark Suite
# Run all benchmarks
cargo bench
# Run specific benchmark
cargo bench -- ingestion
cargo bench -- traversal
cargo bench -- vector_search
# Run with specific dataset size
BENCH_SIZE=large cargo bench
# Generate HTML report
cargo bench -- --save-baseline main
open target/criterion/report/index.html
Custom Benchmarks
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
use uni::prelude::*;
fn benchmark_traversal(c: &mut Criterion) {
let storage = setup_test_storage();
let mut group = c.benchmark_group("traversal");
for hops in [1, 2, 3, 4, 5] {
group.bench_with_input(
BenchmarkId::new("multi_hop", hops),
&hops,
|b, &hops| {
b.iter(|| {
let query = format!(
"MATCH (p:Paper)-[:CITES*{}]->(end) \
WHERE p.id = 'seed_paper' \
RETURN DISTINCT end.id",
hops
);
executor.execute(&query).unwrap()
})
},
);
}
group.finish();
}
criterion_group!(benches, benchmark_traversal);
criterion_main!(benches);
Profiling
# CPU profiling with perf
perf record cargo bench -- vector_search
perf report
# Memory profiling
RUST_BACKTRACE=1 cargo bench -- --profile-time 30
# Flame graphs
cargo flamegraph --bench storage_bench -- --bench
Query Optimization
| Scenario |
Current |
Optimized |
Improvement |
| Missing index |
85ms |
2.9ms |
29x |
| Full projection |
12ms |
5ms |
2.4x |
| Late LIMIT |
180ms |
45ms |
4x |
| Cold cache |
8.2ms |
4.7ms |
1.7x |
Configuration Tuning
// Optimized for throughput
let config = StorageConfig {
batch_size: 8192,
adjacency_cache_size: 2_000_000,
property_cache_size: 500_000,
max_l0_size: 256 * 1024 * 1024, // 256 MB
wal_sync_mode: WalSyncMode::Periodic { interval_ms: 100 },
};
// Optimized for latency
let config = StorageConfig {
batch_size: 2048,
adjacency_cache_size: 5_000_000,
property_cache_size: 1_000_000,
max_l0_size: 64 * 1024 * 1024, // 64 MB
wal_sync_mode: WalSyncMode::Sync,
};
Next Steps