1313use crate :: scheduler_server:: event:: QueryStageSchedulerEvent ;
1414use crate :: state:: execution_graph:: ExecutionStage ;
1515use crate :: state:: execution_graph_dot:: ExecutionGraphDot ;
16+ use crate :: state:: execution_stage:: TaskInfo ;
1617use crate :: { api:: SchedulerErrorResponse , scheduler_server:: SchedulerServer } ;
1718use axum:: extract:: Query ;
1819use axum:: {
@@ -28,10 +29,11 @@ use ballista_core::serde::protobuf::{
2829use ballista_core:: serde:: scheduler:: {
2930 ExecutorOperatingSystemSpecification , ExecutorSpecification ,
3031} ;
32+ use ballista_core:: utils:: get_current_time;
3133use datafusion:: DATAFUSION_VERSION ;
3234use datafusion:: physical_plan:: display:: DisplayableExecutionPlan ;
3335use datafusion:: physical_plan:: displayable;
34- use datafusion:: physical_plan:: metrics:: { MetricValue , MetricsSet , Time } ;
36+ use datafusion:: physical_plan:: metrics:: { MetricsSet , Time } ;
3537use datafusion_proto:: logical_plan:: AsLogicalPlan ;
3638use datafusion_proto:: physical_plan:: AsExecutionPlan ;
3739#[ cfg( feature = "graphviz-support" ) ]
@@ -539,11 +541,9 @@ pub async fn get_query_stages<
539541 . as_ref ( )
540542 . map ( |m| get_combined_count ( m. as_slice ( ) , "output_rows" ) )
541543 . unwrap_or ( 0 ) ;
542- summary. elapsed_compute = running_stage
543- . stage_metrics
544- . as_ref ( )
545- . map ( |m| get_elapsed_compute_nanos ( m. as_slice ( ) ) )
546- . unwrap_or_default ( ) ;
544+ summary. elapsed_compute = get_running_stage_time ( & running_stage
545+ . task_infos , get_current_time ( ) )
546+ ;
547547 summary. tasks = running_stage
548548 . task_infos
549549 . iter ( )
@@ -595,7 +595,7 @@ pub async fn get_query_stages<
595595 "output_rows" ,
596596 ) ;
597597 summary. elapsed_compute =
598- get_elapsed_compute_nanos ( & completed_stage. stage_metrics ) ;
598+ get_finished_stage_time ( & completed_stage. task_infos ) ;
599599
600600 summary. tasks = completed_stage
601601 . task_infos
@@ -722,19 +722,44 @@ fn format_job_status(status: &Option<Status>, elapsed_ms: u64) -> (String, Strin
722722 }
723723}
724724
725- fn get_elapsed_compute_nanos ( metrics : & [ MetricsSet ] ) -> String {
726- let nanos : usize = metrics
725+ fn get_running_stage_time ( task_infos : & [ Option < TaskInfo > ] , current_time : u128 ) -> String {
726+ let min_start = task_infos
727727 . iter ( )
728- . flat_map ( |vec| {
729- vec. iter ( ) . map ( |metric| match metric. as_ref ( ) . value ( ) {
730- MetricValue :: ElapsedCompute ( time) => time. value ( ) ,
731- _ => 0 ,
732- } )
733- } )
734- . sum ( ) ;
735- let t = Time :: new ( ) ;
736- t. add_duration ( Duration :: from_nanos ( nanos as u64 ) ) ;
737- t. to_string ( )
728+ . flat_map ( |t| t. as_ref ( ) . map ( |t| t. start_exec_time ) )
729+ . filter ( |t| * t > 0 )
730+ . min ( ) ;
731+
732+ match ( min_start, current_time) {
733+ ( Some ( start) , end) if end > start => {
734+ let t = Time :: new ( ) ;
735+ t. add_duration ( Duration :: from_millis ( ( end - start) as u64 ) ) ;
736+ t. to_string ( )
737+ }
738+ _ => "0" . to_string ( ) ,
739+ }
740+ }
741+
742+ fn get_finished_stage_time ( task_infos : & [ TaskInfo ] ) -> String {
743+ let min_start = task_infos
744+ . iter ( )
745+ . map ( |t| t. start_exec_time )
746+ . filter ( |t| * t > 0 )
747+ . min ( ) ;
748+
749+ let max_end = task_infos
750+ . iter ( )
751+ . map ( |t| t. end_exec_time )
752+ . filter ( |t| * t > 0 )
753+ . max ( ) ;
754+
755+ match ( min_start, max_end) {
756+ ( Some ( start) , Some ( end) ) if end > start => {
757+ let t = Time :: new ( ) ;
758+ t. add_duration ( Duration :: from_millis ( ( end - start) as u64 ) ) ;
759+ t. to_string ( )
760+ }
761+ _ => "0" . to_string ( ) ,
762+ }
738763}
739764
740765fn get_partition_counts ( metrics : & [ MetricsSet ] , partition_id : usize ) -> ( usize , usize ) {
@@ -878,3 +903,102 @@ pub async fn get_scheduler_metrics<
878903 . unwrap ( ) ,
879904 }
880905}
906+
907+ #[ cfg( test) ]
908+ mod tests {
909+ use super :: * ;
910+ use crate :: state:: execution_stage:: TaskInfo ;
911+ use ballista_core:: serde:: protobuf:: task_status;
912+
913+ fn make_task_info ( start : u128 , end : u128 ) -> TaskInfo {
914+ TaskInfo {
915+ task_id : 0 ,
916+ scheduled_time : 0 ,
917+ launch_time : 0 ,
918+ start_exec_time : start,
919+ end_exec_time : end,
920+ finish_time : 0 ,
921+ task_status : task_status:: Status :: Running ( Default :: default ( ) ) ,
922+ }
923+ }
924+
925+ // --- get_finished_stage_time ---
926+
927+ #[ test]
928+ fn test_finished_empty_slice_returns_zero ( ) {
929+ assert_eq ! ( get_finished_stage_time( & [ ] ) , "0" ) ;
930+ }
931+
932+ #[ test]
933+ fn test_finished_all_zero_timestamps_returns_zero ( ) {
934+ let tasks = vec ! [ make_task_info( 0 , 0 ) , make_task_info( 0 , 0 ) ] ;
935+ assert_eq ! ( get_finished_stage_time( & tasks) , "0" ) ;
936+ }
937+
938+ #[ test]
939+ fn test_finished_single_task_elapsed ( ) {
940+ // 600 - 100 = 500 ms → "500.00ms"
941+ let tasks = vec ! [ make_task_info( 100 , 600 ) ] ;
942+ assert_eq ! ( get_finished_stage_time( & tasks) , "500.00ms" ) ;
943+ }
944+
945+ #[ test]
946+ fn test_finished_picks_earliest_start_and_latest_end ( ) {
947+ // min start = 100, max end = 900 → 800 ms
948+ let tasks = vec ! [
949+ make_task_info( 100 , 500 ) ,
950+ make_task_info( 200 , 900 ) ,
951+ make_task_info( 300 , 700 ) ,
952+ ] ;
953+ assert_eq ! ( get_finished_stage_time( & tasks) , "800.00ms" ) ;
954+ }
955+
956+ #[ test]
957+ fn test_finished_end_before_start_returns_zero ( ) {
958+ let tasks = vec ! [ make_task_info( 900 , 100 ) ] ;
959+ assert_eq ! ( get_finished_stage_time( & tasks) , "0" ) ;
960+ }
961+
962+ // --- get_running_stage_time ---
963+
964+ #[ test]
965+ fn test_running_empty_slice_returns_zero ( ) {
966+ assert_eq ! ( get_running_stage_time( & [ ] , 1000 ) , "0" ) ;
967+ }
968+
969+ #[ test]
970+ fn test_running_all_none_returns_zero ( ) {
971+ let tasks: Vec < Option < TaskInfo > > = vec ! [ None , None ] ;
972+ assert_eq ! ( get_running_stage_time( & tasks, 1000 ) , "0" ) ;
973+ }
974+
975+ #[ test]
976+ fn test_running_future_start_returns_zero ( ) {
977+ // start_exec_time beyond current time → elapsed clamped to 0
978+ let tasks = vec ! [ Some ( make_task_info( u128 :: MAX , 0 ) ) ] ;
979+ assert_eq ! ( get_running_stage_time( & tasks, 1000 ) , "0" ) ;
980+ }
981+
982+ #[ test]
983+ fn test_running_past_start_returns_nonzero ( ) {
984+ let now = 4_000 ;
985+ let start = 1_000 ;
986+ let tasks = vec ! [ Some ( make_task_info( start, 0 ) ) ] ;
987+ assert_eq ! ( get_running_stage_time( & tasks, now) , "3.00s" ) ;
988+ }
989+
990+ #[ test]
991+ fn test_running_mixed_some_none_uses_earliest_some ( ) {
992+ let now = 3_000 ;
993+ let earlier = 1_000 ;
994+ let later = 2_000 ;
995+ let tasks = vec ! [
996+ None ,
997+ Some ( make_task_info( later, 0 ) ) ,
998+ Some ( make_task_info( earlier, 0 ) ) ,
999+ None ,
1000+ ] ;
1001+ let result = get_running_stage_time ( & tasks, now) ;
1002+ assert_eq ! ( result, "2.00s" ) ;
1003+ }
1004+ }
0 commit comments