Skip to content

Commit b96da55

Browse files
karthiknadigCopilot
andcommitted
test: separate cold and warm performance samples (#509)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 412b102 commit b96da55

1 file changed

Lines changed: 167 additions & 55 deletions

File tree

crates/pet/tests/e2e_performance.rs

Lines changed: 167 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ static REQUEST_ID: AtomicU32 = AtomicU32::new(1);
2828

2929
/// Number of iterations for statistical tests
3030
const STAT_ITERATIONS: usize = 10;
31+
const PERFORMANCE_METRICS_SCHEMA_VERSION: u8 = 2;
3132
const STDERR_TAIL_LINES: usize = 100;
3233

3334
/// Statistical metrics with percentile calculations
@@ -552,6 +553,40 @@ fn get_test_cache_dir() -> PathBuf {
552553
.join(format!("cache-{}", std::process::id()))
553554
}
554555

556+
fn benchmark_iteration_cache_dir(cache_root: &Path, workload: &str, iteration: usize) -> PathBuf {
557+
cache_root
558+
.join(workload)
559+
.join(format!("iteration-{}", iteration + 1))
560+
}
561+
562+
fn reset_cache_dir(cache_dir: &Path) {
563+
match std::fs::remove_dir_all(cache_dir) {
564+
Ok(()) => {}
565+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
566+
Err(error) => panic!("Failed to remove cache directory {cache_dir:?}: {error}"),
567+
}
568+
std::fs::create_dir_all(cache_dir)
569+
.unwrap_or_else(|error| panic!("Failed to create cache directory {cache_dir:?}: {error}"));
570+
}
571+
572+
fn assert_stable_inventory(
573+
expected: &mut Option<(usize, usize)>,
574+
actual: (usize, usize),
575+
workload: &str,
576+
iteration: usize,
577+
) {
578+
if let Some(expected) = expected {
579+
assert_eq!(
580+
actual,
581+
*expected,
582+
"{workload} inventory changed at iteration {}",
583+
iteration + 1
584+
);
585+
} else {
586+
*expected = Some(actual);
587+
}
588+
}
589+
555590
/// Get workspace directory (current project root)
556591
fn get_workspace_dir() -> PathBuf {
557592
env::var("GITHUB_WORKSPACE")
@@ -763,33 +798,42 @@ fn collect_refresh_diagnostics(
763798
phase_stats: &mut BTreeMap<String, StatisticalMetrics>,
764799
locator_stats: &mut BTreeMap<String, StatisticalMetrics>,
765800
probe_timeout_counts: &mut BTreeMap<String, usize>,
801+
expected_inventory: (usize, usize),
766802
) {
767-
let diagnostic_cache_dir = cache_dir.join("refresh-progress");
768-
let _ = std::fs::remove_dir_all(&diagnostic_cache_dir);
769-
std::fs::create_dir_all(&diagnostic_cache_dir)
770-
.expect("Failed to create refresh diagnostic cache dir");
803+
let diagnostic_cache_root = cache_dir.join("refresh-progress");
804+
reset_cache_dir(&diagnostic_cache_root);
771805

772-
println!("\nCollecting untimed refresh diagnostics...");
806+
println!("\nCollecting untimed cold-refresh diagnostics...");
773807
for iteration in 0..STAT_ITERATIONS {
808+
let diagnostic_cache_dir =
809+
benchmark_iteration_cache_dir(&diagnostic_cache_root, "cold", iteration);
810+
reset_cache_dir(&diagnostic_cache_dir);
774811
let mut client =
775812
PetClient::spawn_with_refresh_progress().expect("Failed to spawn diagnostic server");
776813
client
777814
.configure(json!({
778815
"workspaceDirectories": [workspace_dir],
779-
"cacheDirectory": diagnostic_cache_dir
816+
"cacheDirectory": diagnostic_cache_dir,
780817
}))
781818
.expect("Failed to configure diagnostic server");
782819
let (result, _) = client
783820
.refresh(None)
784821
.expect("Failed to run diagnostic refresh");
785822

786823
collect_refresh_progress(&client.get_refresh_progress(), phase_stats, locator_stats);
824+
let inventory = (client.get_environments().len(), client.get_managers().len());
825+
assert_eq!(
826+
inventory,
827+
expected_inventory,
828+
"Cold diagnostic inventory changed at iteration {}",
829+
iteration + 1,
830+
);
787831
record_interpreter_probe_timeouts(&client, probe_timeout_counts);
788832
println!(
789-
" Diagnostic iteration {}: refresh={}ms, envs={}",
833+
" Cold diagnostic iteration {}: refresh={}ms, envs={}",
790834
iteration + 1,
791835
result.duration,
792-
client.get_environments().len()
836+
inventory.0,
793837
);
794838
}
795839
}
@@ -853,6 +897,18 @@ fn refresh_progress_aggregation_separates_phases_and_locators() {
853897
assert_eq!(locators["Conda"].samples, vec![20]);
854898
}
855899

900+
#[test]
901+
fn benchmark_cache_directories_are_isolated_by_workload_and_iteration() {
902+
let root = Path::new("benchmark-cache");
903+
let first_cold = benchmark_iteration_cache_dir(root, "cold", 0);
904+
let second_cold = benchmark_iteration_cache_dir(root, "cold", 1);
905+
let first_warm = benchmark_iteration_cache_dir(root, "warm", 0);
906+
907+
assert_eq!(first_cold, root.join("cold").join("iteration-1"));
908+
assert_ne!(first_cold, second_cold);
909+
assert_ne!(first_cold, first_warm);
910+
}
911+
856912
// ============================================================================
857913
// Performance Tests
858914
// ============================================================================
@@ -1340,73 +1396,124 @@ fn test_refresh_warm_vs_cold_cache() {
13401396
#[allow(dead_code)]
13411397
fn test_performance_summary() {
13421398
let mut startup_stats = StatisticalMetrics::new();
1343-
let mut refresh_stats = StatisticalMetrics::new();
1344-
let mut time_to_first_env_stats = StatisticalMetrics::new();
1399+
let mut cold_refresh_stats = StatisticalMetrics::new();
1400+
let mut warm_refresh_stats = StatisticalMetrics::new();
1401+
let mut cold_time_to_first_env_stats = StatisticalMetrics::new();
1402+
let mut warm_time_to_first_env_stats = StatisticalMetrics::new();
13451403
let mut phase_stats = BTreeMap::new();
13461404
let mut locator_stats = BTreeMap::new();
13471405
let mut probe_timeout_counts: BTreeMap<String, usize> = BTreeMap::new();
13481406
let mut expected_inventory = None;
13491407

1350-
let cache_dir = get_test_cache_dir();
1351-
let _ = std::fs::remove_dir_all(&cache_dir);
1352-
std::fs::create_dir_all(&cache_dir).expect("Failed to create cache dir");
1353-
1408+
let cache_root = get_test_cache_dir();
1409+
reset_cache_dir(&cache_root);
13541410
let workspace_dir = get_workspace_dir();
13551411

13561412
println!("\n========================================");
1357-
println!(" PERFORMANCE SUMMARY ({} iterations)", STAT_ITERATIONS);
1413+
println!(
1414+
" COLD/WARM PERFORMANCE SUMMARY ({} pairs)",
1415+
STAT_ITERATIONS
1416+
);
13581417
println!("========================================\n");
13591418

1360-
for i in 0..STAT_ITERATIONS {
1361-
// Measure server startup (fresh server each iteration)
1362-
let spawn_start = Instant::now();
1363-
let mut client = PetClient::spawn().expect("Failed to spawn server");
1364-
1365-
let config = json!({
1366-
"workspaceDirectories": [workspace_dir.clone()],
1367-
"cacheDirectory": cache_dir.clone()
1368-
});
1419+
for iteration in 0..STAT_ITERATIONS {
1420+
let iteration_cache = benchmark_iteration_cache_dir(&cache_root, "measured", iteration);
1421+
reset_cache_dir(&iteration_cache);
13691422

1370-
client.configure(config).expect("Failed to configure");
1423+
let spawn_start = Instant::now();
1424+
let mut cold_client = PetClient::spawn().expect("Failed to spawn cold server");
1425+
cold_client
1426+
.configure(json!({
1427+
"workspaceDirectories": [workspace_dir.clone()],
1428+
"cacheDirectory": iteration_cache.clone(),
1429+
}))
1430+
.expect("Failed to configure cold server");
13711431
let startup_time = spawn_start.elapsed().as_millis();
13721432
startup_stats.add(startup_time);
13731433

1374-
// Measure full refresh
1375-
let (result, _) = client.refresh(None).expect("Failed to refresh");
1376-
refresh_stats.add(result.duration);
1377-
1378-
let inventory = (client.get_environments().len(), client.get_managers().len());
1379-
if let Some(expected) = expected_inventory {
1380-
assert_eq!(
1381-
inventory, expected,
1382-
"Environment and manager inventory changed after iteration 1"
1383-
);
1384-
} else {
1385-
expected_inventory = Some(inventory);
1434+
let (cold_result, _) = cold_client
1435+
.refresh(None)
1436+
.expect("Failed to run cold refresh");
1437+
cold_refresh_stats.add(cold_result.duration);
1438+
let cold_inventory = (
1439+
cold_client.get_environments().len(),
1440+
cold_client.get_managers().len(),
1441+
);
1442+
assert_stable_inventory(
1443+
&mut expected_inventory,
1444+
cold_inventory,
1445+
"Cold refresh",
1446+
iteration,
1447+
);
1448+
if let Some(ttfe) = cold_client.time_to_first_env() {
1449+
cold_time_to_first_env_stats.add(ttfe.as_millis());
13861450
}
1451+
record_interpreter_probe_timeouts(&cold_client, &mut probe_timeout_counts);
1452+
1453+
println!(
1454+
" Cold iteration {}: startup={}ms, refresh={}ms, envs={}",
1455+
iteration + 1,
1456+
startup_time,
1457+
cold_result.duration,
1458+
cold_inventory.0,
1459+
);
1460+
drop(cold_client);
13871461

1388-
if let Some(ttfe) = client.time_to_first_env() {
1389-
time_to_first_env_stats.add(ttfe.as_millis());
1462+
let mut warm_client = PetClient::spawn().expect("Failed to spawn warm server");
1463+
warm_client
1464+
.configure(json!({
1465+
"workspaceDirectories": [workspace_dir.clone()],
1466+
"cacheDirectory": iteration_cache,
1467+
}))
1468+
.expect("Failed to configure warm server");
1469+
let (warm_result, _) = warm_client
1470+
.refresh(None)
1471+
.expect("Failed to run warm refresh");
1472+
warm_refresh_stats.add(warm_result.duration);
1473+
let warm_inventory = (
1474+
warm_client.get_environments().len(),
1475+
warm_client.get_managers().len(),
1476+
);
1477+
assert_stable_inventory(
1478+
&mut expected_inventory,
1479+
warm_inventory,
1480+
"Warm refresh",
1481+
iteration,
1482+
);
1483+
if let Some(ttfe) = warm_client.time_to_first_env() {
1484+
warm_time_to_first_env_stats.add(ttfe.as_millis());
13901485
}
1391-
record_interpreter_probe_timeouts(&client, &mut probe_timeout_counts);
1486+
record_interpreter_probe_timeouts(&warm_client, &mut probe_timeout_counts);
13921487

13931488
println!(
1394-
" Iteration {}: startup={}ms, refresh={}ms, envs={}",
1395-
i + 1,
1396-
startup_time,
1397-
result.duration,
1398-
inventory.0
1489+
" Warm iteration {}: refresh={}ms, envs={}",
1490+
iteration + 1,
1491+
warm_result.duration,
1492+
warm_inventory.0,
13991493
);
14001494
}
14011495

14021496
let (env_count, manager_count) =
14031497
expected_inventory.expect("Performance summary must run at least one iteration");
1498+
for (label, count) in [
1499+
("startup", startup_stats.count()),
1500+
("cold refresh", cold_refresh_stats.count()),
1501+
("warm refresh", warm_refresh_stats.count()),
1502+
("cold time-to-first", cold_time_to_first_env_stats.count()),
1503+
("warm time-to-first", warm_time_to_first_env_stats.count()),
1504+
] {
1505+
assert_eq!(
1506+
count, STAT_ITERATIONS,
1507+
"Expected one {label} sample per benchmark pair"
1508+
);
1509+
}
14041510
collect_refresh_diagnostics(
14051511
&workspace_dir,
1406-
&cache_dir,
1512+
&cache_root,
14071513
&mut phase_stats,
14081514
&mut locator_stats,
14091515
&mut probe_timeout_counts,
1516+
(env_count, manager_count),
14101517
);
14111518

14121519
for phase in ["locators", "path", "globalVirtualEnvs", "workspaces"] {
@@ -1429,10 +1536,10 @@ fn test_performance_summary() {
14291536
println!(" STATISTICS ");
14301537
println!("----------------------------------------");
14311538
startup_stats.print_summary("Server startup");
1432-
refresh_stats.print_summary("Full refresh");
1433-
if time_to_first_env_stats.count() > 0 {
1434-
time_to_first_env_stats.print_summary("Time to first env");
1435-
}
1539+
cold_refresh_stats.print_summary("Cold full refresh");
1540+
warm_refresh_stats.print_summary("Warm full refresh");
1541+
cold_time_to_first_env_stats.print_summary("Cold time to first env");
1542+
warm_time_to_first_env_stats.print_summary("Warm time to first env");
14361543
for (phase, metrics) in &phase_stats {
14371544
metrics.print_summary(&format!("Phase {phase}"));
14381545
}
@@ -1447,17 +1554,22 @@ fn test_performance_summary() {
14471554
let locator_json = statistics_json(&locator_stats);
14481555

14491556
// Output as JSON for CI parsing
1450-
// Includes both P50 values at top level (for backwards compatibility) and full stats
1557+
// Existing top-level refresh fields remain warm-cache values for schema compatibility.
14511558
let json_output = serde_json::to_string_pretty(&json!({
1559+
"metrics_schema_version": PERFORMANCE_METRICS_SCHEMA_VERSION,
14521560
"server_startup_ms": startup_stats.p50().unwrap_or(0),
1453-
"full_refresh_ms": refresh_stats.p50().unwrap_or(0),
1454-
"time_to_first_env_ms": time_to_first_env_stats.p50(),
1561+
"full_refresh_ms": warm_refresh_stats.p50().unwrap_or(0),
1562+
"cold_refresh_ms": cold_refresh_stats.p50().unwrap_or(0),
1563+
"time_to_first_env_ms": warm_time_to_first_env_stats.p50(),
1564+
"cold_time_to_first_env_ms": cold_time_to_first_env_stats.p50(),
14551565
"environments_count": env_count,
14561566
"managers_count": manager_count,
14571567
"stats": {
14581568
"server_startup": startup_stats.to_json(),
1459-
"full_refresh": refresh_stats.to_json(),
1460-
"time_to_first_env": time_to_first_env_stats.to_json()
1569+
"full_refresh": warm_refresh_stats.to_json(),
1570+
"cold_refresh": cold_refresh_stats.to_json(),
1571+
"time_to_first_env": warm_time_to_first_env_stats.to_json(),
1572+
"cold_time_to_first_env": cold_time_to_first_env_stats.to_json(),
14611573
},
14621574
"phases": phase_json,
14631575
"locators": locator_json,

0 commit comments

Comments
 (0)