@@ -494,43 +494,45 @@ impl App {
494494 return ;
495495 }
496496 }
497- // Per-subsystem elapsed: measured to that subsystem's last success,
498- // so a skipped tick diffs cumulative counters over the true span.
499- let since = |at : Option < Instant > | at. map ( |a| snap. taken_at . duration_since ( a) . as_secs_f64 ( ) ) ;
500-
501- if let Some ( stats) = snap. stats {
502- self . rdma_rows = match since ( self . prev_stats_at ) {
503- Some ( e) => compute_throughputs ( & self . prev_stats , & stats, e) ,
504- None => compute_throughputs ( & stats, & stats, 1.0 ) , // baseline
497+ // Per-subsystem elapsed: each sample carries its own read-adjacent
498+ // timestamp, measured to that subsystem's last success, so neither
499+ // another subsystem's latency nor a skipped tick can skew the span.
500+ let since =
501+ |now : Instant , at : Option < Instant > | at. map ( |a| now. duration_since ( a) . as_secs_f64 ( ) ) ;
502+
503+ if let Some ( s) = snap. stats {
504+ self . rdma_rows = match since ( s. taken_at , self . prev_stats_at ) {
505+ Some ( e) => compute_throughputs ( & self . prev_stats , & s. data , e) ,
506+ None => compute_throughputs ( & s. data , & s. data , 1.0 ) , // baseline
505507 } ;
506- self . prev_stats = stats ;
507- self . prev_stats_at = Some ( snap . taken_at ) ;
508+ self . prev_stats = s . data ;
509+ self . prev_stats_at = Some ( s . taken_at ) ;
508510 }
509511
510- if let Some ( nvlink ) = snap. nvlink {
511- self . nvlink_rows = match since ( self . prev_nvlink_at ) {
512- Some ( e) => compute_nvlink_throughputs ( & self . prev_nvlink , & nvlink , e) ,
513- None => compute_nvlink_throughputs ( & nvlink , & nvlink , 1.0 ) , // baseline
512+ if let Some ( s ) = snap. nvlink {
513+ self . nvlink_rows = match since ( s . taken_at , self . prev_nvlink_at ) {
514+ Some ( e) => compute_nvlink_throughputs ( & self . prev_nvlink , & s . data , e) ,
515+ None => compute_nvlink_throughputs ( & s . data , & s . data , 1.0 ) , // baseline
514516 } ;
515- self . prev_nvlink = nvlink ;
516- self . prev_nvlink_at = Some ( snap . taken_at ) ;
517+ self . prev_nvlink = s . data ;
518+ self . prev_nvlink_at = Some ( s . taken_at ) ;
517519 }
518520
519- if let Some ( xgmi ) = snap. xgmi {
520- self . xgmi_rows = match since ( self . prev_xgmi_at ) {
521- Some ( e) => compute_xgmi_throughputs ( & self . prev_xgmi , & xgmi , e) ,
522- None => compute_xgmi_throughputs ( & xgmi , & xgmi , 1.0 ) , // baseline
521+ if let Some ( s ) = snap. xgmi {
522+ self . xgmi_rows = match since ( s . taken_at , self . prev_xgmi_at ) {
523+ Some ( e) => compute_xgmi_throughputs ( & self . prev_xgmi , & s . data , e) ,
524+ None => compute_xgmi_throughputs ( & s . data , & s . data , 1.0 ) , // baseline
523525 } ;
524- self . prev_xgmi = xgmi ;
525- self . prev_xgmi_at = Some ( snap . taken_at ) ;
526+ self . prev_xgmi = s . data ;
527+ self . prev_xgmi_at = Some ( s . taken_at ) ;
526528 }
527529
528- if let Some ( ifstats ) = snap. ifstats {
529- if let Some ( e) = since ( self . prev_ifstats_at ) {
530- self . net_rate = net:: compute_net_rate ( & self . prev_ifstats , & ifstats , e) ;
530+ if let Some ( s ) = snap. ifstats {
531+ if let Some ( e) = since ( s . taken_at , self . prev_ifstats_at ) {
532+ self . net_rate = net:: compute_net_rate ( & self . prev_ifstats , & s . data , e) ;
531533 }
532- self . prev_ifstats = ifstats ;
533- self . prev_ifstats_at = Some ( snap . taken_at ) ;
534+ self . prev_ifstats = s . data ;
535+ self . prev_ifstats_at = Some ( s . taken_at ) ;
534536 }
535537
536538 if let Some ( processes) = snap. processes {
@@ -2288,12 +2290,16 @@ mod apply_snapshot_tests {
22882290 }
22892291 }
22902292
2293+ fn sample < T > ( data : T , taken_at : Instant ) -> Option < crate :: sampler:: Sample < T > > {
2294+ Some ( crate :: sampler:: Sample { data, taken_at } )
2295+ }
2296+
22912297 fn snapshot ( stats : Vec < PortStat > , taken_at : Instant ) -> Snapshot {
22922298 Snapshot {
2293- stats : Some ( stats) ,
2294- ifstats : Some ( Vec :: new ( ) ) ,
2295- nvlink : Some ( Vec :: new ( ) ) ,
2296- xgmi : Some ( Vec :: new ( ) ) ,
2299+ stats : sample ( stats, taken_at ) ,
2300+ ifstats : sample ( Vec :: new ( ) , taken_at ) ,
2301+ nvlink : sample ( Vec :: new ( ) , taken_at ) ,
2302+ xgmi : sample ( Vec :: new ( ) , taken_at ) ,
22972303 processes : Some ( Vec :: new ( ) ) ,
22982304 taken_at,
22992305 }
@@ -2311,14 +2317,30 @@ mod apply_snapshot_tests {
23112317 } ;
23122318 Snapshot {
23132319 stats : None , // e.g. kernel without rdma netlink support
2314- ifstats : Some ( Vec :: new ( ) ) ,
2315- nvlink : Some ( vec ! [ gpu] ) ,
2316- xgmi : Some ( Vec :: new ( ) ) ,
2320+ ifstats : sample ( Vec :: new ( ) , taken_at ) ,
2321+ nvlink : sample ( vec ! [ gpu] , taken_at ) ,
2322+ xgmi : sample ( Vec :: new ( ) , taken_at ) ,
23172323 processes : Some ( Vec :: new ( ) ) ,
23182324 taken_at,
23192325 }
23202326 }
23212327
2328+ #[ test]
2329+ fn rates_use_per_subsystem_read_timestamps ( ) {
2330+ // A slow pass must not skew a subsystem read later in it: the GPU
2331+ // sample's own timestamp, not the pass start, drives its elapsed.
2332+ let mut app = App :: new ( ) ;
2333+ let t0 = Instant :: now ( ) ;
2334+ let mut first = gpu_snapshot ( 0 , t0) ;
2335+ // First pass was slow: GPU counters actually read 2s after pass start.
2336+ first. nvlink . as_mut ( ) . unwrap ( ) . taken_at = t0 + Duration :: from_secs ( 2 ) ;
2337+ app. apply_snapshot ( first) ;
2338+ // Second pass, fast: read at t0+4s -> true GPU window is 2s, not 4s.
2339+ app. apply_snapshot ( gpu_snapshot ( 1_000_000_000 , t0 + Duration :: from_secs ( 4 ) ) ) ;
2340+ // 1e9 bytes * 8 / 2 s / 1e9 = 4.0 Gbps (2.0 would mean pass-start skew)
2341+ assert ! ( ( app. throughputs[ 0 ] . tx_gbps - 4.0 ) . abs( ) < 1e-9 ) ;
2342+ }
2343+
23222344 #[ test]
23232345 fn first_snapshot_populates_rows_with_zero_rates ( ) {
23242346 let mut app = App :: new ( ) ;
0 commit comments