This is an automated email from the ASF dual-hosted git repository.

milenkovicm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new cd53c729 ballista client will collect few metrics (#1251)
cd53c729 is described below

commit cd53c7291a151d739ff8f07d3c53365eed8398a4
Author: Marko Milenković <[email protected]>
AuthorDate: Tue Apr 29 09:52:45 2025 +0100

    ballista client will collect few metrics (#1251)
---
 ballista/client/tests/context_checks.rs            | 117 +++++++++++++++++++++
 .../core/src/execution_plans/distributed_query.rs  |  62 ++++++++---
 ballista/scheduler/src/state/execution_graph.rs    |   9 +-
 3 files changed, 170 insertions(+), 18 deletions(-)

diff --git a/ballista/client/tests/context_checks.rs 
b/ballista/client/tests/context_checks.rs
index d27350f7..f60559ac 100644
--- a/ballista/client/tests/context_checks.rs
+++ b/ballista/client/tests/context_checks.rs
@@ -21,6 +21,7 @@ mod supported {
 
     use crate::common::{remote_context, standalone_context};
     use ballista_core::config::BallistaConfig;
+    use datafusion::physical_plan::collect;
     use datafusion::prelude::*;
     use datafusion::{assert_batches_eq, prelude::SessionContext};
     use rstest::*;
@@ -66,6 +67,122 @@ mod supported {
 
         Ok(())
     }
+
+    // tests if client will collect statistics for
+    // collect/show operation
+    #[rstest]
+    #[case::standalone(standalone_context())]
+    #[case::remote(remote_context())]
+    #[tokio::test]
+    async fn should_collect_client_statistics_for_show(
+        #[future(awt)]
+        #[case]
+        ctx: SessionContext,
+        test_data: String,
+    ) -> datafusion::error::Result<()> {
+        ctx.register_parquet(
+            "test",
+            &format!("{test_data}/alltypes_plain.parquet"),
+            Default::default(),
+        )
+        .await?;
+
+        let plan = ctx
+            .sql("select string_col, timestamp_col from test where id > 4")
+            .await?
+            .create_physical_plan()
+            .await?;
+
+        let result = collect(plan.clone(), ctx.task_ctx()).await?;
+
+        let expected = [
+            "+------------+---------------------+",
+            "| string_col | timestamp_col       |",
+            "+------------+---------------------+",
+            "| 31         | 2009-03-01T00:01:00 |",
+            "| 30         | 2009-04-01T00:00:00 |",
+            "| 31         | 2009-04-01T00:01:00 |",
+            "+------------+---------------------+",
+        ];
+
+        assert_batches_eq!(expected, &result);
+
+        let metrics = plan.metrics().unwrap();
+        let rows = metrics.output_rows().unwrap();
+
+        assert_eq!(3, rows);
+        assert!(
+            plan.metrics()
+                .unwrap()
+                .sum_by_name("transferred_bytes")
+                .unwrap()
+                .as_usize()
+                > 0
+        );
+
+        Ok(())
+    }
+
+    // tests if client will collect statistics for
+    // insert operation
+    #[rstest]
+    #[case::standalone(standalone_context())]
+    #[case::remote(remote_context())]
+    #[tokio::test]
+    #[cfg(not(windows))] // test is failing at windows, can't debug it
+    async fn should_collect_client_statistics_for_insert(
+        #[future(awt)]
+        #[case]
+        ctx: SessionContext,
+        test_data: String,
+    ) -> datafusion::error::Result<()> {
+        ctx.register_parquet(
+            "test",
+            &format!("{test_data}/alltypes_plain.parquet"),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+        let write_dir = tempfile::tempdir().expect("temporary directory to be 
created");
+        let write_dir_path = write_dir
+            .path()
+            .to_str()
+            .expect("path to be converted to str");
+
+        ctx.sql(&format!("CREATE EXTERNAL TABLE written_table (id INTEGER, 
string_col STRING, timestamp_col BIGINT) STORED AS PARQUET LOCATION '{}'" , 
write_dir_path)).await.unwrap().show().await.unwrap();
+
+        let plan = ctx
+            .sql("INSERT INTO written_table select  id, string_col, 
timestamp_col from test")
+            .await?
+            .create_physical_plan()
+            .await?;
+
+        let result = collect(plan.clone(), ctx.task_ctx()).await.unwrap();
+
+        // INSERT operation should return only single row
+        assert_eq!(1, plan.metrics().unwrap().output_rows().unwrap());
+        assert!(
+            plan.metrics()
+                .unwrap()
+                .sum_by_name("transferred_bytes")
+                .unwrap()
+                .as_usize()
+                > 0
+        );
+
+        let expected = [
+            "+-------+",
+            "| count |",
+            "+-------+",
+            "| 8     |",
+            "+-------+",
+        ];
+
+        assert_batches_eq!(expected, &result);
+
+        Ok(())
+    }
+
     #[rstest]
     #[case::standalone(standalone_context())]
     #[case::remote(remote_context())]
diff --git a/ballista/core/src/execution_plans/distributed_query.rs 
b/ballista/core/src/execution_plans/distributed_query.rs
index 59503814..7ccb2727 100644
--- a/ballista/core/src/execution_plans/distributed_query.rs
+++ b/ballista/core/src/execution_plans/distributed_query.rs
@@ -17,6 +17,7 @@
 
 use crate::client::BallistaClient;
 use crate::config::BallistaConfig;
+use crate::serde::protobuf::SuccessfulJob;
 use crate::serde::protobuf::{
     execute_query_params::Query, execute_query_result, job_status,
     scheduler_grpc_client::SchedulerGrpcClient, ExecuteQueryParams, 
GetJobStatusParams,
@@ -30,6 +31,9 @@ use datafusion::error::{DataFusionError, Result};
 use datafusion::execution::context::TaskContext;
 use datafusion::logical_expr::LogicalPlan;
 use datafusion::physical_expr::EquivalenceProperties;
+use datafusion::physical_plan::metrics::{
+    ExecutionPlanMetricsSet, MetricBuilder, MetricsSet,
+};
 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
 use datafusion::physical_plan::{
     DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
@@ -64,7 +68,12 @@ pub struct DistributedQueryExec<T: 'static + AsLogicalPlan> {
     plan_repr: PhantomData<T>,
     /// Session id
     session_id: String,
+    /// Plan properties
     properties: PlanProperties,
+    /// Execution metrics, currently exposes:
+    /// - row count
+    /// - transferred_bytes
+    metrics: ExecutionPlanMetricsSet,
 }
 
 impl<T: 'static + AsLogicalPlan> DistributedQueryExec<T> {
@@ -83,6 +92,7 @@ impl<T: 'static + AsLogicalPlan> DistributedQueryExec<T> {
             plan_repr: PhantomData,
             session_id,
             properties,
+            metrics: ExecutionPlanMetricsSet::new(),
         }
     }
 
@@ -102,6 +112,7 @@ impl<T: 'static + AsLogicalPlan> DistributedQueryExec<T> {
             plan_repr: PhantomData,
             session_id,
             properties,
+            metrics: ExecutionPlanMetricsSet::new(),
         }
     }
 
@@ -168,6 +179,7 @@ impl<T: 'static + AsLogicalPlan> ExecutionPlan for 
DistributedQueryExec<T> {
             properties: Self::compute_properties(
                 self.plan.schema().as_ref().clone().into(),
             ),
+            metrics: ExecutionPlanMetricsSet::new(),
         }))
     }
 
@@ -209,6 +221,9 @@ impl<T: 'static + AsLogicalPlan> ExecutionPlan for 
DistributedQueryExec<T> {
             session_id: Some(self.session_id.clone()),
         };
 
+        let metric_row_count = 
MetricBuilder::new(&self.metrics).output_rows(partition);
+        let metric_total_bytes =
+            MetricBuilder::new(&self.metrics).counter("transferred_bytes", 
partition);
         let stream = futures::stream::once(
             execute_query(
                 self.scheduler_url.clone(),
@@ -218,7 +233,17 @@ impl<T: 'static + AsLogicalPlan> ExecutionPlan for 
DistributedQueryExec<T> {
             )
             .map_err(|e| ArrowError::ExternalError(Box::new(e))),
         )
-        .try_flatten();
+        .try_flatten()
+        .inspect(move |batch| {
+            metric_total_bytes.add(
+                batch
+                    .as_ref()
+                    .map(|b| b.get_array_memory_size())
+                    .unwrap_or(0),
+            );
+
+            metric_row_count.add(batch.as_ref().map(|b| 
b.num_rows()).unwrap_or(0));
+        });
 
         let schema = self.schema();
         Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
@@ -230,6 +255,10 @@ impl<T: 'static + AsLogicalPlan> ExecutionPlan for 
DistributedQueryExec<T> {
         // This implies that we cannot infer the statistics at this stage.
         Ok(Statistics::new_unknown(&self.schema()))
     }
+
+    fn metrics(&self) -> Option<MetricsSet> {
+        Some(self.metrics.clone_inner())
+    }
 }
 
 async fn execute_query(
@@ -285,14 +314,14 @@ async fn execute_query(
         match status {
             None => {
                 if has_status_change {
-                    info!("Job {} still in initialization ...", job_id);
+                    info!("Job {} is in initialization ...", job_id);
                 }
                 wait_future.await;
                 prev_status = status;
             }
             Some(job_status::Status::Queued(_)) => {
                 if has_status_change {
-                    info!("Job {} still queued...", job_id);
+                    info!("Job {} is queued...", job_id);
                 }
                 wait_future.await;
                 prev_status = status;
@@ -309,17 +338,22 @@ async fn execute_query(
                 error!("{}", msg);
                 break Err(DataFusionError::Execution(msg));
             }
-            Some(job_status::Status::Successful(successful)) => {
-                let streams =
-                    successful
-                        .partition_location
-                        .into_iter()
-                        .map(move |partition| {
-                            let f = fetch_partition(partition, 
max_message_size)
-                                .map_err(|e| 
ArrowError::ExternalError(Box::new(e)));
-
-                            futures::stream::once(f).try_flatten()
-                        });
+            Some(job_status::Status::Successful(SuccessfulJob {
+                started_at,
+                ended_at,
+                partition_location,
+                ..
+            })) => {
+                let duration = ended_at.saturating_sub(started_at);
+                let duration = Duration::from_millis(duration);
+
+                info!("Job {} finished executing in {:?} ", job_id, duration);
+                let streams = partition_location.into_iter().map(move 
|partition| {
+                    let f = fetch_partition(partition, max_message_size)
+                        .map_err(|e| ArrowError::ExternalError(Box::new(e)));
+
+                    futures::stream::once(f).try_flatten()
+                });
 
                 break Ok(futures::stream::iter(streams).flatten());
             }
diff --git a/ballista/scheduler/src/state/execution_graph.rs 
b/ballista/scheduler/src/state/execution_graph.rs
index 9023a7ec..7114ba6e 100644
--- a/ballista/scheduler/src/state/execution_graph.rs
+++ b/ballista/scheduler/src/state/execution_graph.rs
@@ -1293,6 +1293,11 @@ impl ExecutionGraph {
             .map(|l| l.try_into())
             .collect::<Result<Vec<_>>>()?;
 
+        self.end_time = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_millis() as u64;
+
         self.status = JobStatus {
             job_id: self.job_id.clone(),
             job_name: self.job_name.clone(),
@@ -1304,10 +1309,6 @@ impl ExecutionGraph {
                 ended_at: self.end_time,
             })),
         };
-        self.end_time = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .unwrap()
-            .as_millis() as u64;
 
         Ok(())
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to