use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::summary::stale_assets_for_many_under;
use crate::core::session_assets::{session_asset_staleness_with_catalog, session_plan_under};
use crate::core::update_check::CatalogHashes;
use crate::session_manager::{ManagedSessionId, ManagedSessionState, SessionRecord};
const AGENTS: usize = 42;
const SKILLS: usize = 52;
const AGENT_BYTES: usize = 23_541;
const SKILL_BYTES: usize = 7_401;
const SESSIONS: usize = 32;
const REPS: usize = 7;
fn write_sized(path: &Path, head: &str, bytes: usize) {
let mut body = String::with_capacity(bytes + head.len());
body.push_str(head);
body.push('\n');
while body.len() < bytes {
body.push_str("filler line for realistic file size measurement\n");
}
std::fs::write(path, body).unwrap();
}
fn build_fleet(base: &Path, sessions: usize) -> Vec<SessionRecord> {
let fw = crate::core::paths::FrameworkPaths::under(base);
let agent_source = fw.agent_source_dir();
let skill_source = fw.skill_source_dir();
std::fs::create_dir_all(&agent_source).unwrap();
std::fs::create_dir_all(&skill_source).unwrap();
for i in 0..AGENTS {
write_sized(
&agent_source.join(format!("bench-agent-{i:02}.md")),
&format!("# bench agent {i}"),
AGENT_BYTES,
);
}
for i in 0..SKILLS {
write_sized(
&skill_source.join(format!("bench-skill-{i:02}.md")),
&format!("# bench skill {i}"),
SKILL_BYTES,
);
}
crate::core::agent_deployer::deploy_agents_filtered(
&agent_source,
&fw.agent_deploy_dir(),
&fw.skill_deploy_dir(),
|_| true,
)
.unwrap();
(0..sessions)
.map(|i| {
let ws = base.join(format!("bench-ws-{i:03}"));
std::fs::create_dir_all(&ws).unwrap();
let ws_fw = crate::core::paths::FrameworkPaths::for_managed_workspace_under(base, &ws);
crate::core::skill_tiers::deploy_all_skill_tiers(
&skill_source,
&fw.user_skill_source_dir(),
&ws_fw.claude_skills_dir(),
|_| true,
)
.unwrap();
let mut r = super::tests::make_record(None);
r.state = ManagedSessionState::Active;
r.workspace_path = Some(ws);
r
})
.collect()
}
async fn stale_assets_per_session_agent_read(
records: Vec<SessionRecord>,
base: PathBuf,
) -> HashMap<ManagedSessionId, bool> {
let inputs = tokio::task::spawn_blocking(move || {
let mut cache: HashMap<(PathBuf, PathBuf), Arc<CatalogHashes>> = HashMap::new();
let mut out = Vec::with_capacity(records.len());
for record in records {
let (fw, plan) = session_plan_under(&record, &base);
let key = (plan.agent_source.clone(), plan.skill_source.clone());
let catalog = cache
.entry(key)
.or_insert_with(|| {
Arc::new(CatalogHashes::compute(
&plan.agent_source,
&plan.skill_source,
&fw.skill_deploy_dir(),
))
})
.clone();
out.push((record.id, fw, plan, catalog));
}
out
})
.await
.unwrap_or_default();
let mut probes = tokio::task::JoinSet::new();
for (id, fw, plan, catalog) in inputs {
probes.spawn_blocking(move || {
(
id,
session_asset_staleness_with_catalog(&fw, &plan, &catalog).stale,
)
});
}
let mut result = HashMap::with_capacity(probes.len());
while let Some(res) = probes.join_next().await {
if let Ok((id, stale)) = res {
result.insert(id, stale);
}
}
result
}
fn median(mut xs: Vec<Duration>) -> Duration {
xs.sort_unstable();
xs[xs.len() / 2]
}
#[tokio::test(flavor = "multi_thread")]
#[serial_test::serial]
#[ignore = "benchmark: run explicitly with --ignored --nocapture"]
async fn bench_stale_assets_for_many() {
let base = super::tests::fake_base();
let records = build_fleet(base.path(), SESSIONS);
let scope = base.path().to_path_buf();
let _ = stale_assets_for_many_under(records.clone(), scope.clone()).await;
let _ = stale_assets_per_session_agent_read(records.clone(), scope.clone()).await;
let mut old_timings = Vec::with_capacity(REPS);
let mut new_timings = Vec::with_capacity(REPS);
let (mut old_reads, mut new_reads) = (0usize, 0usize);
for rep in 0..REPS {
let run_old = || async {
crate::core::update_check::reset_deployed_read_log();
let t = Instant::now();
let out = stale_assets_per_session_agent_read(records.clone(), scope.clone()).await;
let elapsed = t.elapsed();
let reads = crate::core::update_check::deployed_reads_under(&scope);
(out, elapsed, reads)
};
let run_new = || async {
crate::core::update_check::reset_deployed_read_log();
let t = Instant::now();
let out = stale_assets_for_many_under(records.clone(), scope.clone()).await;
let elapsed = t.elapsed();
let reads = crate::core::update_check::deployed_reads_under(&scope);
(out, elapsed, reads)
};
let ((old, old_elapsed, o_reads), (new, new_elapsed, n_reads)) = if rep % 2 == 0 {
let a = run_old().await;
let b = run_new().await;
(a, b)
} else {
let b = run_new().await;
let a = run_old().await;
(a, b)
};
old_reads = o_reads;
new_reads = n_reads;
assert_eq!(old.len(), SESSIONS, "every session must get a verdict");
assert_eq!(
old, new,
"the shared deployed-agent read must produce the IDENTICAL verdict \
map as the per-session read it replaces — a faster wrong answer is \
strictly worse than the slow right one (#4322)"
);
println!(
" rep {rep} ({}): per-session {:>9.3} ms ({old_reads} reads) | \
shared {:>9.3} ms ({new_reads} reads)",
if rep % 2 == 0 {
"old-first"
} else {
"new-first"
},
old_elapsed.as_secs_f64() * 1000.0,
new_elapsed.as_secs_f64() * 1000.0,
);
old_timings.push(old_elapsed);
new_timings.push(new_elapsed);
}
let old_med = median(old_timings);
let new_med = median(new_timings);
println!(
"BENCH sessions={SESSIONS} agents={AGENTS} skills={SKILLS} \
per_session_median={:.3}ms shared_median={:.3}ms speedup={:.2}x \
reads {old_reads} -> {new_reads}",
old_med.as_secs_f64() * 1000.0,
new_med.as_secs_f64() * 1000.0,
old_med.as_secs_f64() / new_med.as_secs_f64(),
);
}