Merge pull request 'docs(clawhdf5): document DType variants, fix unresolved doc links' (#17) from sdlc-docs/clawhdf5-types-20260514-165210 into main

This commit is contained in:
redclawsystems
2026-05-14 23:54:48 +00:00
commit 3f222f6956
3030 changed files with 89917 additions and 0 deletions
@@ -0,0 +1,493 @@
use clawhdf5_agent::bm25::BM25Index;
use clawhdf5_agent::consolidation::{
ConsolidationConfig, ConsolidationEngine, ImportanceScorer, ImportanceWeights, MemorySource,
};
use clawhdf5_agent::hybrid::{hybrid_search, rrf_hybrid_search};
use clawhdf5_agent::knowledge::KnowledgeCache;
use clawhdf5_agent::temporal::TemporalIndex;
use clawhdf5_agent::vector_search;
use clawhdf5_agent::{AgentMemory, HDF5Memory, MemoryConfig, MemoryEntry};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use tempfile::TempDir;
// ---------------------------------------------------------------------------
// Simple deterministic PRNG (LCG)
// ---------------------------------------------------------------------------
struct Rng(u32);
impl Rng {
fn new(seed: u32) -> Self {
Self(seed)
}
fn next_u32(&mut self) -> u32 {
self.0 = self.0.wrapping_mul(1103515245).wrapping_add(12345);
self.0 >> 16
}
fn next_f32(&mut self) -> f32 {
self.next_u32() as f32 / 65536.0 - 0.5
}
fn next_usize(&mut self, max: usize) -> usize {
self.next_u32() as usize % max
}
}
// ---------------------------------------------------------------------------
// Data generation helpers
// ---------------------------------------------------------------------------
const WORDS: &[&str] = &[
"the",
"quick",
"brown",
"fox",
"jumps",
"over",
"lazy",
"dog",
"rust",
"programming",
"memory",
"vector",
"search",
"index",
"data",
"system",
"agent",
"knowledge",
"graph",
"neural",
"network",
"machine",
"learning",
"deep",
"embedding",
"cosine",
"similarity",
"token",
"chunk",
"session",
"channel",
"hybrid",
"temporal",
"consolidation",
"episodic",
"semantic",
];
fn make_vec(rng: &mut Rng, dim: usize) -> Vec<f32> {
(0..dim).map(|_| rng.next_f32()).collect()
}
fn make_vecs(n: usize, dim: usize, seed: u32) -> Vec<Vec<f32>> {
let mut rng = Rng::new(seed);
(0..n).map(|_| make_vec(&mut rng, dim)).collect()
}
fn make_text(rng: &mut Rng, word_count: usize) -> String {
(0..word_count)
.map(|_| WORDS[rng.next_usize(WORDS.len())])
.collect::<Vec<_>>()
.join(" ")
}
fn make_texts(n: usize, word_count: usize, seed: u32) -> Vec<String> {
let mut rng = Rng::new(seed);
(0..n).map(|_| make_text(&mut rng, word_count)).collect()
}
// ---------------------------------------------------------------------------
// Vector search latency benchmarks
// ---------------------------------------------------------------------------
fn vector_search_latency(c: &mut Criterion) {
const DIM: usize = 384;
let query = make_vec(&mut Rng::new(99), DIM);
let mut group = c.benchmark_group("vector_search_latency");
group.sample_size(50);
for (label, n) in [("1k", 1_000usize), ("10k", 10_000), ("100k", 100_000)] {
let vectors = make_vecs(n, DIM, 42);
let tombstones = vec![0u8; n];
group.bench_with_input(
BenchmarkId::new("bench_cosine_search", label),
&n,
|b, _| {
b.iter(|| vector_search::cosine_similarity_batch(&query, &vectors, &tombstones));
},
);
}
group.finish();
}
// ---------------------------------------------------------------------------
// Hybrid search benchmarks
// ---------------------------------------------------------------------------
fn hybrid_search_benches(c: &mut Criterion) {
const DIM: usize = 384;
const N: usize = 1_000;
let vectors = make_vecs(N, DIM, 42);
let docs = make_texts(N, 50, 77);
let tombstones = vec![0u8; N];
let bm25 = BM25Index::build(&docs, &tombstones);
let query_vec = make_vec(&mut Rng::new(99), DIM);
let mut group = c.benchmark_group("hybrid_search");
group.sample_size(50);
// Weighted score fusion (vector + BM25)
group.bench_function("bench_hybrid_search_1k", |b| {
b.iter(|| {
hybrid_search(
&query_vec,
"rust memory search",
&vectors,
&docs,
&tombstones,
&bm25,
0.7,
0.3,
10,
)
});
});
// Reciprocal Rank Fusion
group.bench_function("bench_rrf_search_1k", |b| {
b.iter(|| {
rrf_hybrid_search(
&query_vec,
"rust memory search",
&vectors,
&docs,
&tombstones,
&bm25,
10,
)
});
});
group.finish();
}
// ---------------------------------------------------------------------------
// Knowledge graph benchmarks
// ---------------------------------------------------------------------------
fn build_knowledge_graph(n: usize) -> KnowledgeCache {
let mut kg = KnowledgeCache::new();
// Add n entities
for i in 0..n {
kg.add_entity(&format!("entity_{i}"), "node", -1);
}
// Add edges: each node connects to next 3 nodes (ring-like)
for i in 0..n {
let src = i as u64;
let tgt1 = ((i + 1) % n) as u64;
let tgt2 = ((i + 2) % n) as u64;
let tgt3 = ((i + 3) % n) as u64;
kg.add_relation(src, tgt1, "connects", 1.0);
kg.add_relation(src, tgt2, "relates", 0.8);
kg.add_relation(src, tgt3, "associated", 0.6);
}
kg
}
fn knowledge_graph_benches(c: &mut Criterion) {
let mut group = c.benchmark_group("knowledge_graph");
group.sample_size(50);
// BFS traversal benchmarks
{
let kg_100 = build_knowledge_graph(100);
group.bench_function("bench_bfs_100_entities", |b| {
b.iter(|| kg_100.bfs_neighbors(0, 3));
});
}
{
let kg_1000 = build_knowledge_graph(1_000);
group.bench_function("bench_bfs_1000_entities", |b| {
b.iter(|| kg_1000.bfs_neighbors(0, 3));
});
}
// Spreading activation benchmark
{
let kg_100 = build_knowledge_graph(100);
let seed_ids: Vec<u64> = vec![0, 1, 2];
group.bench_function("bench_spreading_activation_100", |b| {
b.iter(|| kg_100.spreading_activation(&seed_ids, 0.85, 0.01, 5));
});
}
// Fuzzy entity resolution benchmark
{
// Build a graph with 100 entities, then benchmark fuzzy matching
let mut kg_100 = build_knowledge_graph(100);
// Pre-populate with some named entities
for i in 0..100usize {
kg_100.add_alias(&format!("alias_{i}"), i as i64);
}
group.bench_function("bench_entity_resolution_100", |b| {
// Queries with slight typos to trigger fuzzy matching
let queries = [
("entty_42", "node"),
("entitty_7", "node"),
("entity_99x", "node"),
("enttiy_50", "node"),
];
let mut idx = 0usize;
b.iter(|| {
let (name, etype) = queries[idx % queries.len()];
idx += 1;
// Clone needed since resolve_or_create takes &mut self
let mut kg = kg_100.clone();
kg.resolve_or_create(name, etype, -1, 3)
});
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Consolidation benchmarks
// ---------------------------------------------------------------------------
fn consolidation_benches(c: &mut Criterion) {
const DIM: usize = 384;
let mut group = c.benchmark_group("consolidation");
group.sample_size(50);
// Consolidation cycle benchmarks
for (label, n) in [("100", 100usize), ("1000", 1_000)] {
group.bench_with_input(
BenchmarkId::new("bench_consolidation_cycle", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let mut engine = ConsolidationEngine::new(ConsolidationConfig {
working_capacity: n + 50,
episodic_capacity: n * 20,
..ConsolidationConfig::default()
});
let mut rng = Rng::new(42);
let now = 1_000_000.0f64;
for i in 0..n {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("memory record {i} with some content");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
}
engine
},
|mut engine| {
engine.consolidate(2_000_000.0);
},
criterion::BatchSize::LargeInput,
);
},
);
}
// Importance scoring benchmark
{
let mut rng = Rng::new(55);
// Build a set of existing records for surprise scoring
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
let now = 1_000_000.0f64;
for i in 0..50usize {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("existing record {i}");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
}
let records = engine.records().to_vec();
let weights = ImportanceWeights::default();
let query_embedding = make_vec(&mut rng, DIM);
let sample_text =
"This is a substantive memory about system architecture and deployment patterns";
group.bench_function("bench_importance_scoring", |b| {
b.iter(|| {
let surprise = ImportanceScorer::score_surprise(&query_embedding, &records);
let correction = ImportanceScorer::score_correction(&MemorySource::Correction);
let length = ImportanceScorer::score_length(sample_text);
ImportanceScorer::score_combined(surprise, correction, length, &weights)
});
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Temporal index benchmarks
// ---------------------------------------------------------------------------
fn temporal_benches(c: &mut Criterion) {
const N: usize = 10_000;
let mut group = c.benchmark_group("temporal");
group.sample_size(50);
// Build a pre-populated temporal index for range queries
let mut index = TemporalIndex::new();
for i in 0..N {
index.insert(i as u64, i as f64 * 10.0);
}
// Query middle third
let start_ts = (N as f64 * 10.0) / 3.0;
let end_ts = (N as f64 * 10.0) * 2.0 / 3.0;
group.bench_function("bench_temporal_range_query_10k", |b| {
b.iter(|| index.range_query(start_ts, end_ts));
});
// Insert benchmark: measure time to insert 10k timestamps one by one
group.bench_function("bench_temporal_insert_10k", |b| {
b.iter_batched(
|| TemporalIndex::new(),
|mut idx| {
for i in 0..N {
// Shuffle insertion order slightly using a simple offset pattern
let ts = ((i * 7) % N) as f64 * 10.0;
idx.insert(i as u64, ts);
}
idx
},
criterion::BatchSize::LargeInput,
);
});
group.finish();
}
// ---------------------------------------------------------------------------
// HDF5 write-scale benchmarks (Track 8.4 — memory footprint)
// ---------------------------------------------------------------------------
//
// Measures batch write throughput at 100, 1K, and 10K records.
// Actual file sizes are reported by the standalone `footprint_bench` binary.
//
// # WASM Note (cfg target_arch = "wasm32")
// These benchmarks require std filesystem access (HDF5 on-disk format).
// For wasm32 targets:
// - HDF5Memory would need an in-memory or IndexedDB-backed storage layer.
// - `TempDir` would be replaced by a virtual FS.
// - `criterion` is not available on wasm32; use `console_error_panic_hook`
// + manual timing via `web_sys::Performance` instead.
// The #[cfg(target_arch = "wasm32")] guard is not applied here because the
// entire bench harness is excluded from wasm32 builds by the `harness = false`
// Cargo configuration.
fn make_bench_entry(idx: usize, dim: usize) -> MemoryEntry {
let mut rng = Rng::new(idx as u32 + 7777);
MemoryEntry {
chunk: make_text(&mut rng, 30),
embedding: make_vec(&mut rng, dim),
source_channel: "footprint-bench".to_string(),
timestamp: 1_000_000.0 + idx as f64,
session_id: format!("sess_{}", idx / 50),
tags: String::new(),
}
}
fn hdf5_write_scale_benches(c: &mut Criterion) {
const DIM: usize = 384;
let mut group = c.benchmark_group("hdf5_write_scale");
group.sample_size(10); // fewer samples — these involve disk I/O
for (label, n) in [("100", 100usize), ("1k", 1_000), ("10k", 10_000)] {
let entries: Vec<MemoryEntry> = (0..n).map(|i| make_bench_entry(i, DIM)).collect();
group.bench_with_input(BenchmarkId::new("batch_write", label), &n, |b, _| {
b.iter_batched(
|| {
let dir = TempDir::new().expect("TempDir");
let mut cfg = MemoryConfig::new(dir.path().join("w.h5"), "bench", DIM);
cfg.wal_enabled = false;
cfg.compact_threshold = 0.0;
let mem = HDF5Memory::create(cfg).expect("HDF5Memory");
(dir, mem, entries.clone())
},
|(dir, mut mem, e)| {
mem.save_batch(e).expect("save_batch");
// Keep dir alive so the file isn't deleted during measurement
std::hint::black_box(dir);
},
criterion::BatchSize::LargeInput,
);
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Large-scale consolidation benchmarks (Track 8.5 extension)
// ---------------------------------------------------------------------------
fn large_consolidation_benches(c: &mut Criterion) {
const DIM: usize = 384;
let mut group = c.benchmark_group("consolidation_large");
group.sample_size(10);
for (label, n) in [("10k", 10_000usize)] {
group.bench_with_input(
BenchmarkId::new("bench_consolidation_cycle", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let mut engine = ConsolidationEngine::new(ConsolidationConfig {
working_capacity: n / 2,
episodic_capacity: n * 20,
..ConsolidationConfig::default()
});
let mut rng = Rng::new(99);
let now = 1_000_000.0f64;
for i in 0..n {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("memory record {i} with content");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
}
engine
},
|mut engine| {
engine.consolidate(2_000_000.0);
},
criterion::BatchSize::LargeInput,
);
},
);
}
group.finish();
}
// ---------------------------------------------------------------------------
// Criterion groups & main
// ---------------------------------------------------------------------------
criterion_group!(
memory_benches,
vector_search_latency,
hybrid_search_benches,
knowledge_graph_benches,
consolidation_benches,
large_consolidation_benches,
temporal_benches,
hdf5_write_scale_benches,
);
criterion_main!(memory_benches);