@@ -28,6 +28,7 @@ static REQUEST_ID: AtomicU32 = AtomicU32::new(1);
2828
2929/// Number of iterations for statistical tests
3030const STAT_ITERATIONS : usize = 10 ;
31+ const PERFORMANCE_METRICS_SCHEMA_VERSION : u8 = 2 ;
3132const 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)
556591fn 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 ! ( "\n Collecting untimed refresh diagnostics..." ) ;
806+ println ! ( "\n Collecting 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) ]
13411397fn 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