Skip to content

Commit 1462635

Browse files
authored
fix: rest api calculates stage running time correctly (#1675)
* fix: rest api calculates stage running time correctly * move current time method to utils * update calculation * add tests * return 0 if stages not running * address review comments
1 parent df4b391 commit 1462635

2 files changed

Lines changed: 151 additions & 19 deletions

File tree

ballista/core/src/utils.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ pub fn get_time_before(interval_seconds: u64) -> u64 {
349349
.as_secs()
350350
}
351351

352+
/// current time since UNIX EPOCH
353+
pub fn get_current_time() -> u128 {
354+
SystemTime::now()
355+
.duration_since(UNIX_EPOCH)
356+
.unwrap()
357+
.as_millis()
358+
}
359+
352360
#[cfg(test)]
353361
mod tests {
354362
use super::*;

ballista/scheduler/src/api/handlers.rs

Lines changed: 143 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use crate::scheduler_server::event::QueryStageSchedulerEvent;
1414
use crate::state::execution_graph::ExecutionStage;
1515
use crate::state::execution_graph_dot::ExecutionGraphDot;
16+
use crate::state::execution_stage::TaskInfo;
1617
use crate::{api::SchedulerErrorResponse, scheduler_server::SchedulerServer};
1718
use axum::extract::Query;
1819
use axum::{
@@ -28,10 +29,11 @@ use ballista_core::serde::protobuf::{
2829
use ballista_core::serde::scheduler::{
2930
ExecutorOperatingSystemSpecification, ExecutorSpecification,
3031
};
32+
use ballista_core::utils::get_current_time;
3133
use datafusion::DATAFUSION_VERSION;
3234
use datafusion::physical_plan::display::DisplayableExecutionPlan;
3335
use datafusion::physical_plan::displayable;
34-
use datafusion::physical_plan::metrics::{MetricValue, MetricsSet, Time};
36+
use datafusion::physical_plan::metrics::{MetricsSet, Time};
3537
use datafusion_proto::logical_plan::AsLogicalPlan;
3638
use 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

740765
fn 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

Comments
 (0)