use harness_core::{Block, Context, Guide, Memory, MemoryEntry, MemoryError, Task, Turn, TurnRole};
use harness_loop::MemoryGuide;
use std::sync::Arc;
use std::time::{Duration, Instant};
struct Stats {
n: usize,
min: Duration,
median: Duration,
mean: Duration,
}
impl std::fmt::Display for Stats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"min {:>10.3?} median {:>10.3?} mean {:>10.3?} (n={})",
self.min, self.median, self.mean, self.n
)
}
}
fn summarise(mut samples: Vec<Duration>) -> Stats {
samples.sort();
let n = samples.len();
let total: Duration = samples.iter().sum();
Stats {
n,
min: samples[0],
median: samples[n / 2],
mean: total / n as u32,
}
}
fn bench_async<F, Fut>(rt: &tokio::runtime::Runtime, warmup: usize, iters: usize, mut f: F) -> Stats
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = ()>,
{
for _ in 0..warmup {
rt.block_on(f());
}
let mut samples = Vec::with_capacity(iters);
for _ in 0..iters {
let t = Instant::now();
rt.block_on(f());
samples.push(t.elapsed());
}
summarise(samples)
}
struct NullMemory {
canned: Vec<MemoryEntry>,
}
#[async_trait::async_trait]
impl Memory for NullMemory {
async fn recall(&self, _q: &str, k: usize) -> Result<Vec<MemoryEntry>, MemoryError> {
Ok(self.canned.iter().take(k).cloned().collect())
}
async fn write(&self, _e: MemoryEntry) -> Result<(), MemoryError> {
Ok(())
}
}
const ASCII_QUERY: &str = "how should the billing service handle retries on payment webhooks";
const CJK_QUERY: &str = "结算服务在支付回调重试的时候应该怎么处理幂等问题";
const CJK_SUBSTRING: &str = "支付回调重试";
fn ascii_entry(i: usize) -> MemoryEntry {
MemoryEntry::new(format!(
"note {i}: the billing service retries payment webhooks with an idempotency key derived \
from the provider event id; the deploy runbook for release {i} covers the rollback path"
))
.with_source("bench")
.with_tags([
"bench",
if i.is_multiple_of(3) {
"billing"
} else {
"misc"
},
])
}
fn cjk_entry(i: usize) -> MemoryEntry {
MemoryEntry::new(format!(
"笔记 {i}:结算服务在处理支付回调重试时使用幂等键,键由渠道事件号推导;\
第 {i} 次发布的运维手册里写了回滚步骤和值班联系人"
))
.with_source("bench")
.with_tags(["bench", "结算"])
}
fn tmpdir() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let d = std::env::temp_dir().join(format!("harness-bench-mem-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
fn file_memory_with(
dir: &std::path::Path,
name: &str,
entries: Vec<MemoryEntry>,
) -> Arc<dyn Memory> {
use std::io::Write;
let path = dir.join(format!("{name}.jsonl"));
let mut f = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
for mut e in entries {
if e.id.is_empty() {
e.id = format!("{:016x}", fastrand_ish());
}
if e.created_ms == 0 {
e.created_ms = 1_700_000_000_000;
}
writeln!(f, "{}", serde_json::to_string(&e).unwrap()).unwrap();
}
f.flush().unwrap();
drop(f);
Arc::new(harness_context::FileMemory::open(&path).unwrap())
}
fn fastrand_ish() -> u64 {
use std::cell::Cell;
thread_local!(static S: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
S.with(|s| {
let mut x = s.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
s.set(x);
x
})
}
fn ctx_with_user_turn(query: &str) -> Context {
let mut c = Context::new(Task {
description: query.to_string(),
source: None,
deadline: None,
});
c.history.push(Turn {
role: TurnRole::User,
blocks: vec![Block::Text(query.to_string())],
});
c
}
fn injected_chars(ctx: &Context) -> usize {
ctx.guides
.iter()
.map(|b| match b {
Block::Text(t) => t.chars().count(),
_ => 0,
})
.sum()
}
fn main() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let dir = tmpdir();
println!("\n=== MemoryGuide (harness-loop) — per-turn injection cost ===");
println!("machine: {}", machine());
println!("path measured: Guide::apply_before_iter (the every-turn one)\n");
{
let canned: Vec<MemoryEntry> = (0..5).map(ascii_entry).collect();
let mem: Arc<dyn Memory> = Arc::new(NullMemory { canned });
let guide = MemoryGuide::new(mem).with_top_k(5);
let w = harness_context::default_world(".");
let mut probe = ctx_with_user_turn(ASCII_QUERY);
rt.block_on(guide.apply_before_iter(&mut probe, &w))
.unwrap();
let chars = injected_chars(&probe);
let s = bench_async(&rt, 200, 2_000, || {
let guide = &guide;
let w = &w;
async move {
let mut c = ctx_with_user_turn(ASCII_QUERY);
guide.apply_before_iter(&mut c, w).await.unwrap();
std::hint::black_box(&c);
}
});
println!("guide-only (O(1) stub backend, top_k=5) {s} rendered {chars} chars");
}
for &n in &[10usize, 1_000, 50_000] {
let mem = file_memory_with(
&dir,
&format!("ascii-{n}"),
(0..n).map(ascii_entry).collect(),
);
let guide = MemoryGuide::new(mem).with_top_k(5);
let w = harness_context::default_world(".");
let mut probe = ctx_with_user_turn(ASCII_QUERY);
rt.block_on(guide.apply_before_iter(&mut probe, &w))
.unwrap();
let chars = injected_chars(&probe);
let iters = if n >= 50_000 { 20 } else { 200 };
let s = bench_async(&rt, 3, iters, || {
let guide = &guide;
let w = &w;
async move {
let mut c = ctx_with_user_turn(ASCII_QUERY);
guide.apply_before_iter(&mut c, w).await.unwrap();
std::hint::black_box(&c);
}
});
println!("FileMemory ascii n={n:<6} {s} rendered {chars} chars");
}
for &n in &[1_000usize, 50_000] {
let mem = file_memory_with(
&dir,
&format!("filt-{n}"),
(0..n).map(ascii_entry).collect(),
);
let guide = MemoryGuide::new(mem)
.with_top_k(5)
.with_min_score(0.2)
.with_excluded_tags(["secret"]);
let w = harness_context::default_world(".");
let mut probe = ctx_with_user_turn(ASCII_QUERY);
rt.block_on(guide.apply_before_iter(&mut probe, &w))
.unwrap();
let chars = injected_chars(&probe);
let iters = if n >= 50_000 { 20 } else { 200 };
let s = bench_async(&rt, 3, iters, || {
let guide = &guide;
let w = &w;
async move {
let mut c = ctx_with_user_turn(ASCII_QUERY);
guide.apply_before_iter(&mut c, w).await.unwrap();
std::hint::black_box(&c);
}
});
println!("FileMemory + filters n={n:<6} {s} rendered {chars} chars");
}
for &n in &[1_000usize, 50_000] {
let mem = file_memory_with(&dir, &format!("cjk-{n}"), (0..n).map(cjk_entry).collect());
let guide = MemoryGuide::new(mem).with_top_k(5);
let w = harness_context::default_world(".");
for (label, q) in [("sentence", CJK_QUERY), ("substring", CJK_SUBSTRING)] {
let mut probe = ctx_with_user_turn(q);
rt.block_on(guide.apply_before_iter(&mut probe, &w))
.unwrap();
let chars = injected_chars(&probe);
let iters = if n >= 50_000 { 20 } else { 200 };
let s = bench_async(&rt, 3, iters, || {
let guide = &guide;
let w = &w;
async move {
let mut c = ctx_with_user_turn(q);
guide.apply_before_iter(&mut c, w).await.unwrap();
std::hint::black_box(&c);
}
});
println!("FileMemory CJK {label:<9} n={n:<6} {s} rendered {chars} chars");
}
}
let _ = std::fs::remove_dir_all(&dir);
println!();
}
fn machine() -> String {
std::process::Command::new("sysctl")
.args(["-n", "machdep.cpu.brand_string"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".into())
}