Skip to main content

SochDB End-to-End Profiling

Overviewโ€‹

This document describes the end-to-end profiling infrastructure for SochDB's HNSW vector index, covering the entire pipeline from the Python SDK through FFI to the Rust core.

Versions

This page reflects the pure-Python ctypes SDK (sochdb 0.5.9) and the SochDB core engine (2.0.3). The profiling helpers (enable_profiling(), disable_profiling(), dump_profiling()) live in the 0.5.9 SDK's sochdb.vector module; the native PyO3 package (also named sochdb, version 2.0.3) does not expose them.

Architectureโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Python SDK Layer โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ 10_e2e_profiling.py โ”‚ โ”‚
โ”‚ โ”‚ - PrecisionTimer (nanosecond resolution) โ”‚ โ”‚
โ”‚ โ”‚ - ProfiledVectorIndex wrapper โ”‚ โ”‚
โ”‚ โ”‚ - Memory tracking via tracemalloc โ”‚ โ”‚
โ”‚ โ”‚ - Per-operation timing: numpy, dtype, validation, FFI ptr creation โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ–ผ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ sochdb.vector (FFI bindings, 0.5.9 SDK) โ”‚ โ”‚
โ”‚ โ”‚ - ctypes bindings to libsochdb_index.dylib โ”‚ โ”‚
โ”‚ โ”‚ - insert_batch โ†’ hnsw_insert_batch โ”‚ โ”‚
โ”‚ โ”‚ - Profiling control: enable_profiling() / disable_profiling() โ”‚ โ”‚
โ”‚ โ”‚ / dump_profiling() โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ”‚ FFI Boundary
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Rust FFI Layer (ffi.rs) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ hnsw_insert_batch() โ”‚ โ”‚
โ”‚ โ”‚ - Profiled: ffi.slice_from_raw, ffi.id_conversion โ”‚ โ”‚
โ”‚ โ”‚ - Calls: index.insert_batch_contiguous() โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ HNSW Core (hnsw.rs) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ insert_batch_contiguous_bulk() โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ Phase 1: Parallel Quantization (hnsw.phase1.quantize_parallel) โ”‚ โ”‚
โ”‚ โ”‚ - rayon parallel iterator โ”‚ โ”‚
โ”‚ โ”‚ - Creates HnswNode with QuantizedVector โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ Phase 2: Map Insert (hnsw.phase2.map_insert) โ”‚ โ”‚
โ”‚ โ”‚ - Insert nodes into DashMap โ”‚ โ”‚
โ”‚ โ”‚ - Update entry point โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ Phase 3: Connection Building (hnsw.phase3.*) โ”‚ โ”‚
โ”‚ โ”‚ - connect_total: Overall connection time โ”‚ โ”‚
โ”‚ โ”‚ - search_layer: Graph navigation โ”‚ โ”‚
โ”‚ โ”‚ - neighbor_select: RNG heuristic โ† MAIN BOTTLENECK โ”‚ โ”‚
โ”‚ โ”‚ - add_connections: Bidirectional edge creation โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Profiling Infrastructureโ€‹

Python-Side Profilingโ€‹

File: sochdb-python-sdk/examples/10_e2e_profiling.py

# Key components
class PrecisionTimer:
"""Nanosecond-precision timer using time.perf_counter_ns()"""

class TimingStats:
"""Statistical aggregation: count, total, mean, std, min, max, p50, p95, p99"""

class ProfiledVectorIndex:
"""Wrapper that instruments all VectorIndex operations"""

def insert_batch_profiled(vectors, ids):
# Times: numpy_ascontiguous, dtype_conversion, data_validation,
# ffi_ptr_creation, ffi_call_overhead, batch_total

Output: profiling_results.json

Profiling control hooksโ€‹

The sochdb.vector module (pure-Python sochdb 0.5.9 SDK) exposes three no-argument helpers that toggle the Rust-side profiling collector across the FFI boundary:

from sochdb.vector import enable_profiling, disable_profiling, dump_profiling

enable_profiling() # turn on the Rust-side profiling collector
# ... run your inserts / searches ...
dump_profiling() # write /tmp/sochdb_profile.json and print a summary
disable_profiling() # turn the collector back off

These call the FFI exports sochdb_profiling_enable(), sochdb_profiling_dump(), and sochdb_profiling_disable() respectively. Enabling at runtime is equivalent to setting SOCHDB_PROFILING=1 before launching the process.

Rust-Side Profilingโ€‹

File: sochdb-index/src/profiling.rs

// Enabling profiling
SOCHDB_PROFILING=1 // Environment variable

// Key components
pub struct Timer { /* start, stop, record */ }
pub struct OpStats { count, total_ns, min_ns, max_ns, item_count }
pub struct ProfileCollector { /* global singleton with thread-safe stats */ }

// FFI exports
pub extern "C" fn sochdb_profiling_enable();
pub extern "C" fn sochdb_profiling_disable();
pub extern "C" fn sochdb_profiling_dump();

Output: /tmp/sochdb_profile.json

Running Profilingโ€‹

Basic Usageโ€‹

cd sochdb-python-sdk

# Enable profiling and run
export SOCHDB_PROFILING=1
export SOCHDB_LIB_PATH=/path/to/target/release/libsochdb_index.dylib
python3 examples/10_e2e_profiling.py

# Analyze results
python3 examples/11_profiling_analysis.py

Customizable Parametersโ€‹

Edit 10_e2e_profiling.py:

# Configuration
NUM_VECTORS = 1000 # Number of vectors to insert
DIMENSION = 768 # Vector dimension (e.g., OpenAI embeddings)
EF_CONSTRUCTION = 200 # HNSW ef parameter (quality vs speed)
MAX_CONNECTIONS = 16 # HNSW M parameter

Profiling Results (1000 vectors, 768d)โ€‹

Summary Metricsโ€‹

MetricValue
Total Insert Time13,533 ms (13.5s)
Throughput74 vectors/sec
Latency13.5 ms/vector
Peak Memory9.45 MB
Search Time1.7 ms

Time Breakdownโ€‹

PhaseTime (ms)PercentagePer-Vector
Neighbor Selection10,36576.6%11.07 ms
Add Connections1,90814.1%2.04 ms
Search Layer1,0767.9%1.15 ms
Quantization0.99<0.01%1.05 ยตs
Map Insert0.11<0.01%0.12 ยตs
FFI Overhead0.003<0.01%0.003 ยตs

Bottleneck Analysisโ€‹

CRITICAL BOTTLENECK: Neighbor Selection (RNG Heuristic) - 77%

The select_neighbors_heuristic function implements the Relative Neighborhood
Graph (RNG) pruning which is essential for HNSW quality but computationally
expensive.

For each of N vectors inserted:
- Search returns ~ef candidates (200)
- RNG checks each candidate against all previously selected neighbors
- Each check requires a full 768-dimensional distance calculation
- Complexity: O(N ร— ef ร— m ร— D) where D is dimension

With N=1000, ef=200, m=16, D=768:
- Approximately 1000 ร— 200 ร— 16 = 3.2M distance calculations
- Each 768d cosine distance = 3 SIMD operations (dot, norm_a, norm_b)

Optimization Recommendationsโ€‹

1. Reduce ef_constructionโ€‹

# Current: ef_construction = 200
# Recommended: ef_construction = 100-128
# Trade-off: ~5% recall reduction, 2x faster insert

index = VectorIndex(
dimension=768,
ef_construction=100, # Reduced from 200
max_connections=16,
)

2. Use Batch Distance Computationโ€‹

The RNG heuristic currently does scalar distance calculations. Batching candidates and using SIMD would provide 4-8x speedup on distance computation.

3. Pre-compute Distance Cacheโ€‹

Cache pairwise distances between candidates during search to avoid recomputation in neighbor selection.

4. Approximate RNG for Early Insertsโ€‹

Use simpler heuristics (top-k by distance) when graph is sparse, switch to full RNG when graph is denser.

5. Parallel Neighbor Selectionโ€‹

The RNG check for each candidate can be parallelized since it's independent.

Files Createdโ€‹

FilePurpose
sochdb-python-sdk/examples/10_e2e_profiling.pyMain profiling script
sochdb-python-sdk/examples/11_profiling_analysis.pyAnalysis and visualization
sochdb-python-sdk/src/sochdb/vector.pyenable_profiling / disable_profiling / dump_profiling FFI hooks
sochdb-index/src/profiling.rsRust profiling infrastructure
profiling_results.jsonPython-side profiling output
/tmp/sochdb_profile.jsonRust-side profiling output

JSON Output Formatโ€‹

Python Profile (profiling_results.json)โ€‹

{
"timestamp": "2025-12-29T00:33:55",
"config": {
"num_vectors": 1000,
"dimension": 768,
"ef_construction": 200,
"max_connections": 16
},
"summary": {
"total_insert_time_ms": 13533.17,
"vectors_per_second": 73.89,
"peak_memory_mb": 9.45
},
"python_layer": {
"timings": {
"numpy_ascontiguous": { "total_ms": 0.008 },
"dtype_conversion": { "total_ms": 0.013 },
"ffi_ptr_creation": { "total_ms": 0.032 },
"batch_total": { "total_ms": 13533.05 }
}
}
}

Rust Profile (/tmp/sochdb_profile.json)โ€‹

{
"total_elapsed_ms": 13537.17,
"operations": {
"hnsw.phase3.neighbor_select": {
"count": 1,
"total_ms": 10365.00,
"mean_us": 10365003.96,
"item_count": 936,
"per_item_us": 11073.72
},
"hnsw.phase3.add_connections": { ... },
"hnsw.phase3.search_layer": { ... },
"hnsw.phase1.quantize_parallel": { ... },
"hnsw.phase2.map_insert": { ... }
}
}

Conclusionโ€‹

The profiling reveals that 77% of insertion time is spent in the RNG neighbor selection heuristic, which is the core HNSW algorithm for maintaining graph quality. This is expected behavior for high-dimensional vectors.

Key insights:

  • Python/FFI overhead is negligible (<0.001%)
  • Quantization and map operations are very fast (<0.01%)
  • The bottleneck is algorithmic, not I/O or memory
  • Optimization should focus on the neighbor selection loop

For faster inserts at the cost of some recall:

  1. Reduce ef_construction from 200 to 100
  2. Use lower precision (F16) for distance calculations
  3. Implement batched SIMD distance in the RNG loop