Contents
mentatd Performance Guide
Running Benchmarks
Serialization Micro-benchmarks (no database required)
# Run all serialization benchmarks
cargo bench -p mentatd --bench serialization
# Run only EDN benchmarks
cargo bench -p mentatd --bench serialization -- edn_
# Run only Transit+JSON benchmarks
cargo bench -p mentatd --bench serialization -- transit_json_
# Run only Transit+MessagePack benchmarks
cargo bench -p mentatd --bench serialization -- transit_msgpack_
# Run format comparison (all three formats, same data)
cargo bench -p mentatd --bench serialization -- format_comparison
# Run output size comparison
cargo bench -p mentatd --bench serialization -- output_size
Cache Micro-benchmarks (no database required)
# Run all cache benchmarks
cargo bench -p mentatd --bench cache
# Run only cache hit benchmarks
cargo bench -p mentatd --bench cache -- cache_hit
# Run only cache invalidation benchmarks
cargo bench -p mentatd --bench cache -- cache_invalidate
Load Tests (requires running mentatd + PostgreSQL)
# Start mentatd
cargo run -p mentatd --release
# In another terminal, run load tests
./mentatd/benches/load_test.sh localhost 8484
# Custom host/port
./mentatd/benches/load_test.sh 192.168.1.10 8484
Criterion reports are generated in target/criterion/ with HTML charts.
Serialization Format Comparison
mentatd supports three response formats, selected via the Accept header:
| Format | Header | Use Case |
|---|---|---|
| EDN | application/edn (default) |
Human-readable, REPL use |
| Transit+JSON | application/transit+json |
Web clients, debugging |
| Transit+MessagePack | application/transit+msgpack |
Production, max throughput |
Expected Characteristics
- EDN: Simplest format, minimal overhead. Good baseline.
- Transit+JSON: ~10-20% overhead vs EDN for serialization, but parseable by standard JSON parsers with Transit decoding layer.
- Transit+MessagePack: Smallest payload size (binary), fastest to parse on the client side. Slightly more serialization overhead than EDN due to binary encoding, but significantly less data over the wire.
Query Cache
The built-in LRU query cache (mentatd/src/cache.rs) provides:
- Cache hits: Near-zero latency (mutex lock + LRU lookup)
- Cache misses: Full PostgreSQL round-trip
- Invalidation: Entire cache cleared after every transaction
- TTL: Entries expire after configurable duration (default 300s)
Cache Configuration
In mentatd.toml:
[cache]
enabled = true
capacity = 1000 # Maximum number of cached query results
ttl_secs = 300 # Time-to-live in seconds
Cache Tuning
- High read, low write: Increase
capacityto 5000-10000 - High write (frequent transactions): Cache may provide less benefit since
it is fully invalidated on each transaction. Consider reducing
capacityto save memory. - Memory constraint: Each cached entry stores the raw JSON string from
PostgreSQL. For large result sets, monitor memory usage and reduce
capacityaccordingly.
PostgreSQL Tuning
mentatd performance is dominated by PostgreSQL query execution time. Key settings to tune:
# postgresql.conf
# Memory
shared_buffers = 256MB # 25% of available RAM
effective_cache_size = 768MB # 75% of available RAM
work_mem = 16MB # Per-operation memory
# Connections
max_connections = 100 # Match mentatd pool size
# Query planning
random_page_cost = 1.1 # SSD storage
effective_io_concurrency = 200 # SSD storage
# WAL
wal_buffers = 16MB
checkpoint_completion_target = 0.9
Connection Pool
mentatd uses deadpool-postgres for connection pooling. Configure in
mentatd.toml:
[database]
pool_size = 16 # Number of PostgreSQL connections
Rule of thumb: pool_size = number of CPU cores * 2. Too many connections
cause contention; too few cause queuing.
Performance Targets
These are approximate targets for a typical deployment (4-core server, PostgreSQL on SSD, ~1M datoms):
| Operation | Target Latency | Notes |
|---|---|---|
| Health check | < 1ms | No DB access |
| List databases | < 10ms | Simple pg_database query |
| Connect | < 10ms | Database existence check |
| Simple query (1-2 patterns) | < 50ms | Depends on data size |
| Complex query (5+ patterns) | < 500ms | Depends on join complexity |
| Transaction (10 datoms) | < 50ms | Single PG function call |
| Transaction (100 datoms) | < 200ms | Batch insert |
Monitoring
mentatd exposes Prometheus metrics at /metrics:
# Request counts
mentatd_requests_total
mentatd_errors_total
# Query performance
mentatd_queries_total
mentatd_query_duration_seconds (histogram)
# Cache performance
mentatd_cache_hits_total
mentatd_cache_misses_total
# Transactions
mentatd_transactions_total
# Connection pool
mentatd_connection_pool_size
Key Metrics to Watch
- Cache hit ratio:
cache_hits / (cache_hits + cache_misses). Target > 80% for read-heavy workloads. - Query p99 latency: From the
mentatd_query_duration_secondshistogram. Spikes indicate PostgreSQL contention or complex queries. - Error rate:
errors / requests. Should be < 1% in normal operation. - Pool saturation: If
connection_pool_sizeequalspool_sizeconfig and latency is rising, increase the pool or optimize queries.
Profiling
For deeper analysis, use Rust profiling tools:
# CPU profiling with flamegraph
cargo install flamegraph
cargo flamegraph -p mentatd --bench serialization
# Memory profiling with DHAT
# Add to Cargo.toml: dhat = { version = "0.3", optional = true }
# Run with: cargo run --features dhat -p mentatd