use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use aion_core::{AssistantSessionEvent, AssistantSessionId, ContentType, Payload};
use aion_store::StoreError;
use aion_store::assistant::{AssistantSessionRecord, AssistantSessionStore};
use aion_store_haematite::HaematiteStore;
use chrono::Utc;
const CHUNKS: usize = 2_000;
const CHUNK_TEXT: &str = "the quick brown fox jumps over the lazy dog";
fn unique_temp_dir(name: &str) -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos());
std::env::temp_dir().join(format!(
"aion-assistant-commit-broadcast-{name}-{}-{nanos}",
std::process::id()
))
}
fn percentile(samples: &mut [Duration], per_cent: usize) -> Duration {
if samples.is_empty() {
return Duration::ZERO;
}
samples.sort_unstable();
let rank = samples.len().saturating_mul(per_cent).saturating_add(99) / 100;
samples[rank.clamp(1, samples.len()) - 1]
}
fn rendered_rate(chunks: usize, elapsed: Duration) -> String {
let micros = elapsed.as_micros();
if micros == 0 {
return String::from("immeasurable (the run took under a microsecond)");
}
let Ok(chunks) = u128::try_from(chunks) else {
return String::from("immeasurable (the chunk count does not fit)");
};
let tenths = chunks.saturating_mul(10_000_000) / micros;
format!("{}.{}", tenths / 10, tenths % 10)
}
#[tokio::test(flavor = "multi_thread")]
async fn commit_before_broadcast_at_token_chunk_rate() -> Result<(), StoreError> {
let store = Arc::new(HaematiteStore::create(
unique_temp_dir("single-node"),
haematite::NodeCacheBudget::Unlimited,
)?);
let session_id = AssistantSessionId::new_v4();
let now = Utc::now();
store
.put_assistant_session(AssistantSessionRecord {
session_id,
subject: String::from("operator"),
harness: String::from("claude"),
account: None,
title: Some(String::from("the measurement")),
created_at: now,
updated_at: now,
turns: 0,
mcp_token_digest: None,
commands: Vec::new(),
})
.await?;
let (sender, mut watcher) =
tokio::sync::broadcast::channel::<(u64, Instant)>(CHUNKS.next_power_of_two());
let mut gaps: Vec<Duration> = Vec::with_capacity(CHUNKS);
let started = Instant::now();
for nth in 0..CHUNKS {
let event = AssistantSessionEvent::Delta {
turn_id: String::from("t-1"),
text: format!("{CHUNK_TEXT} {nth}"),
};
let bytes = serde_json::to_vec(&event)
.map_err(|error| StoreError::Serialization(error.to_string()))?;
let index = store
.append_assistant_transcript_event(
&session_id,
Utc::now(),
Payload::new(ContentType::Json, bytes),
)
.await?;
let committed = Instant::now();
drop(sender.send((index, committed)));
let (received, committed) = watcher.recv().await.map_err(|error| {
StoreError::Backend(format!(
"a broadcast frame was lost, so the gap cannot be measured: {error}"
))
})?;
assert_eq!(
received, index,
"the frame a watcher receives is the frame that was committed"
);
gaps.push(committed.elapsed());
}
let elapsed = started.elapsed();
let transcript = store.assistant_transcript(&session_id, None).await?;
assert_eq!(transcript.len(), CHUNKS, "every chunk must be durable");
let indices: Vec<u64> = transcript.iter().map(|event| event.index).collect();
let expected: Vec<u64> = (0..CHUNKS as u64).collect();
assert_eq!(
indices, expected,
"the transcript must be dense and ordered"
);
let rate = rendered_rate(CHUNKS, elapsed);
let p99 = percentile(&mut gaps, 99);
let p50 = percentile(&mut gaps, 50);
println!(
"assistant commit-before-broadcast measurement\n \
store mode: haematite, single node, 1 shard, NodeCacheBudget::Unlimited\n \
build profile: {profile}\n \
host parallelism: {cores}\n \
chunk payload: {payload} bytes of JSON\n \
chunks: {CHUNKS}\n \
wall clock: {elapsed:?}\n \
sustained chunks/s: {rate}\n \
commit→broadcast p50: {p50:?}\n \
commit→broadcast p99: {p99:?}",
profile = if cfg!(debug_assertions) {
"debug (unoptimized)"
} else {
"release"
},
cores = std::thread::available_parallelism().map_or_else(
|error| format!("unknown ({error})"),
|count| count.to_string()
),
payload = transcript
.first()
.map_or(0, |event| event.payload.bytes().len()),
);
Ok(())
}