Skip to content

Commit 6209781

Browse files
authored
perf: add criterion benchmarks for hot paths (#330)
* perf: add criterion benchmarks for token estimation, skill matching, context building Closes #318 * style: add #[must_use] to cosine_similarity
1 parent 5d319c7 commit 6209781

File tree

8 files changed

+222
-1
lines changed

8 files changed

+222
-1
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/zeph-core/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ zeph-memory.workspace = true
3232
zeph-skills = { workspace = true, features = ["qdrant"] }
3333
zeph-tools.workspace = true
3434

35+
[[bench]]
36+
name = "context_building"
37+
harness = false
38+
3539
[dev-dependencies]
40+
criterion.workspace = true
3641
serial_test.workspace = true
3742
tempfile.workspace = true
3843

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
2+
use std::hint::black_box;
3+
use zeph_memory::estimate_tokens;
4+
5+
fn generate_messages(count: usize, avg_len: usize) -> Vec<String> {
6+
let base = "This is a simulated message with typical content for an AI conversation. ";
7+
(0..count)
8+
.map(|i| {
9+
let content = base.repeat(avg_len / base.len() + 1);
10+
format!("[user]: message {i} {}", &content[..avg_len])
11+
})
12+
.collect()
13+
}
14+
15+
fn should_compact_check(c: &mut Criterion) {
16+
let mut group = c.benchmark_group("should_compact");
17+
18+
for count in [20, 50, 100] {
19+
let messages = generate_messages(count, 200);
20+
group.bench_with_input(BenchmarkId::new("messages", count), &messages, |b, msgs| {
21+
b.iter(|| {
22+
let total: usize = msgs.iter().map(|m| estimate_tokens(m)).sum();
23+
black_box(total > 4000)
24+
});
25+
});
26+
}
27+
28+
group.finish();
29+
}
30+
31+
fn trim_budget_scan(c: &mut Criterion) {
32+
let mut group = c.benchmark_group("trim_budget_scan");
33+
34+
for count in [20, 50, 100] {
35+
let messages = generate_messages(count, 200);
36+
let budget = 2000usize;
37+
38+
group.bench_with_input(BenchmarkId::new("messages", count), &messages, |b, msgs| {
39+
b.iter(|| {
40+
let mut total = 0usize;
41+
let mut keep_from = msgs.len();
42+
for i in (0..msgs.len()).rev() {
43+
let tokens = estimate_tokens(&msgs[i]);
44+
if total + tokens > budget {
45+
break;
46+
}
47+
total += tokens;
48+
keep_from = i;
49+
}
50+
black_box((keep_from, total))
51+
});
52+
});
53+
}
54+
55+
group.finish();
56+
}
57+
58+
fn history_formatting(c: &mut Criterion) {
59+
let mut group = c.benchmark_group("history_formatting");
60+
61+
for count in [10, 30, 50] {
62+
let messages = generate_messages(count, 200);
63+
64+
group.bench_with_input(BenchmarkId::new("messages", count), &messages, |b, msgs| {
65+
b.iter(|| {
66+
let text: String = msgs
67+
.iter()
68+
.map(|m| m.as_str())
69+
.collect::<Vec<_>>()
70+
.join("\n\n");
71+
black_box(text)
72+
});
73+
});
74+
}
75+
76+
group.finish();
77+
}
78+
79+
criterion_group!(
80+
benches,
81+
should_compact_check,
82+
trim_budget_scan,
83+
history_formatting
84+
);
85+
criterion_main!(benches);

crates/zeph-memory/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ tracing.workspace = true
1616
uuid = { workspace = true, features = ["v4"] }
1717
zeph-llm.workspace = true
1818

19+
[[bench]]
20+
name = "token_estimation"
21+
harness = false
22+
1923
[dev-dependencies]
2024
anyhow.workspace = true
25+
criterion.workspace = true
2126
testcontainers.workspace = true
2227
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
2328
tokio-stream.workspace = true
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
2+
use std::hint::black_box;
3+
use zeph_memory::estimate_tokens;
4+
5+
fn generate_text(size: usize) -> String {
6+
let paragraph = "The quick brown fox jumps over the lazy dog. \
7+
This sentence contains various English words and punctuation marks.\n";
8+
paragraph.repeat(size / paragraph.len() + 1)[..size].to_string()
9+
}
10+
11+
fn token_estimation(c: &mut Criterion) {
12+
let mut group = c.benchmark_group("estimate_tokens");
13+
14+
for size in [1_000, 10_000, 100_000] {
15+
let input = generate_text(size);
16+
group.throughput(Throughput::Bytes(size as u64));
17+
group.bench_with_input(BenchmarkId::new("ascii", size), &input, |b, input| {
18+
b.iter(|| estimate_tokens(black_box(input)));
19+
});
20+
}
21+
22+
group.finish();
23+
}
24+
25+
fn token_estimation_unicode(c: &mut Criterion) {
26+
let mut group = c.benchmark_group("estimate_tokens_unicode");
27+
28+
let pattern = "Привет мир! 你好世界! こんにちは世界! 🌍🌎🌏 ";
29+
for size in [1_000, 10_000, 100_000] {
30+
let input = pattern.repeat(size / pattern.len() + 1);
31+
let input = &input[..input.floor_char_boundary(size)];
32+
let input = input.to_string();
33+
let actual_len = input.len();
34+
group.throughput(Throughput::Bytes(actual_len as u64));
35+
group.bench_with_input(
36+
BenchmarkId::new("unicode", actual_len),
37+
&input,
38+
|b, input| {
39+
b.iter(|| estimate_tokens(black_box(input)));
40+
},
41+
);
42+
}
43+
44+
group.finish();
45+
}
46+
47+
fn token_estimation_batch(c: &mut Criterion) {
48+
let mut group = c.benchmark_group("estimate_tokens_batch");
49+
50+
let messages: Vec<String> = (0..50)
51+
.map(|i| format!("Message {i}: {}", generate_text(200)))
52+
.collect();
53+
54+
group.bench_function("50_messages_sum", |b| {
55+
b.iter(|| black_box(messages.iter().map(|m| estimate_tokens(m)).sum::<usize>()));
56+
});
57+
58+
group.finish();
59+
}
60+
61+
criterion_group!(
62+
benches,
63+
token_estimation,
64+
token_estimation_unicode,
65+
token_estimation_batch
66+
);
67+
criterion_main!(benches);

crates/zeph-skills/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ uuid = { workspace = true, optional = true, features = ["v5"] }
2525
zeph-llm.workspace = true
2626
zeph-memory = { workspace = true, optional = true }
2727

28+
[[bench]]
29+
name = "matcher"
30+
harness = false
31+
2832
[dev-dependencies]
2933
anyhow.workspace = true
34+
criterion.workspace = true
3035
tempfile.workspace = true
3136
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
3237

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
2+
use std::hint::black_box;
3+
use zeph_skills::matcher::cosine_similarity;
4+
5+
fn generate_vector(dim: usize, seed: f32) -> Vec<f32> {
6+
(0..dim).map(|i| ((i as f32 + seed) * 0.1).sin()).collect()
7+
}
8+
9+
fn cosine_similarity_bench(c: &mut Criterion) {
10+
let mut group = c.benchmark_group("cosine_similarity");
11+
12+
for dim in [128, 384, 768, 1536] {
13+
let a = generate_vector(dim, 1.0);
14+
let b = generate_vector(dim, 2.0);
15+
group.bench_with_input(BenchmarkId::new("dim", dim), &dim, |bench, _| {
16+
bench.iter(|| cosine_similarity(black_box(&a), black_box(&b)));
17+
});
18+
}
19+
20+
group.finish();
21+
}
22+
23+
fn cosine_ranking(c: &mut Criterion) {
24+
let mut group = c.benchmark_group("cosine_ranking");
25+
26+
for count in [10, 50, 100] {
27+
let query = generate_vector(384, 0.0);
28+
let candidates: Vec<Vec<f32>> =
29+
(0..count).map(|i| generate_vector(384, i as f32)).collect();
30+
31+
group.bench_with_input(BenchmarkId::new("candidates", count), &count, |b, _| {
32+
b.iter(|| {
33+
let mut scored: Vec<(usize, f32)> = candidates
34+
.iter()
35+
.enumerate()
36+
.map(|(i, emb)| (i, cosine_similarity(&query, emb)))
37+
.collect();
38+
scored.sort_unstable_by(|a, b| {
39+
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
40+
});
41+
black_box(scored)
42+
});
43+
});
44+
}
45+
46+
group.finish();
47+
}
48+
49+
criterion_group!(benches, cosine_similarity_bench, cosine_ranking);
50+
criterion_main!(benches);

crates/zeph-skills/src/matcher.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ impl SkillMatcherBackend {
142142
}
143143
}
144144

145-
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
145+
#[must_use]
146+
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
146147
if a.len() != b.len() || a.is_empty() {
147148
return 0.0;
148149
}

0 commit comments

Comments
 (0)