Skip to content

Commit 53809fb

Browse files
committed
Add stats estimation accuracy to benchmarks
1 parent 5851c72 commit 53809fb

9 files changed

Lines changed: 193 additions & 54 deletions

File tree

benchmarks/cdk/bin/@bench-common.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface ExecuteQueryResult {
6868
plan: string
6969
elapsed: number
7070
tasks: number
71+
statsQError?: number
7172
}
7273

7374
export interface BenchmarkRunner {
@@ -193,11 +194,18 @@ export async function runBenchmark(
193194
rowCount: response.rowCount,
194195
plan: response.plan,
195196
tasks: response.tasks,
197+
statsQError: response.statsQError,
196198
})
197199

198-
console.log(
199-
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms and returned ${response.rowCount} rows`
200-
);
200+
if (response.statsQError !== undefined) {
201+
console.log(
202+
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms, stats q-error ${response.statsQError.toFixed(2)}x and returned ${response.rowCount} rows`
203+
);
204+
} else {
205+
console.log(
206+
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms and returned ${response.rowCount} rows`
207+
);
208+
}
201209
}
202210

203211
console.log(`Query ${id} p50 time: ${result.p50()} ms`);

benchmarks/cdk/bin/@results.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface QueryIter {
1212
rowCount: number;
1313
elapsed: number; // Duration in milliseconds
1414
tasks: number;
15+
statsQError?: number;
1516
error?: string;
1617
}
1718

@@ -80,16 +81,24 @@ export class BenchmarkRun {
8081
console.log(`=== Comparing ${this.dataset} results from engine '${other.engine}' [prev] with '${this.engine}' [new] ===`);
8182
let totalTimePrev = 0
8283
let totalTimeNew = 0
84+
const statsQErrorPrev: number[] = []
85+
const statsQErrorNew: number[] = []
8386
for (const query of this.results) {
8487
const prevQuery = other.results.find(v => v.id === query.id);
8588
if (!prevQuery) {
8689
continue;
8790
}
8891
const timePrev = prevQuery.representativeTime()
8992
const timeNew = query.representativeTime()
90-
if (timePrev && timeNew) {
93+
if (timePrev !== undefined && timeNew !== undefined) {
9194
totalTimePrev += timePrev
9295
totalTimeNew += timeNew
96+
statsQErrorPrev.push(...prevQuery.iterations.flatMap(iter =>
97+
iter.statsQError === undefined ? [] : [iter.statsQError]
98+
))
99+
statsQErrorNew.push(...query.iterations.flatMap(iter =>
100+
iter.statsQError === undefined ? [] : [iter.statsQError]
101+
))
93102
}
94103

95104
query.compare(prevQuery);
@@ -108,6 +117,16 @@ export class BenchmarkRun {
108117
console.log(
109118
`${"TOTAL".padStart(8)}: prev=${totalTimePrev.toString()} ms, new=${totalTimeNew.toString()} ms, diff=${f.toFixed(2)} ${tag} ${emoji}`
110119
);
120+
121+
const qErrorPrev = median(statsQErrorPrev)
122+
const qErrorNew = median(statsQErrorNew)
123+
if (qErrorPrev !== undefined && qErrorNew !== undefined) {
124+
console.log(`${"QERR P50".padStart(8)}: prev=${qErrorPrev.toFixed(2)}x, new=${qErrorNew.toFixed(2)}x`)
125+
} else if (qErrorPrev !== undefined) {
126+
console.log(`${"QERR P50".padStart(8)}: prev=${qErrorPrev.toFixed(2)}x, new=n/a`)
127+
} else if (qErrorNew !== undefined) {
128+
console.log(`${"QERR P50".padStart(8)}: prev=n/a, new=${qErrorNew.toFixed(2)}x`)
129+
}
111130
}
112131

113132
compareWithPrevious(): void {
@@ -238,6 +257,7 @@ export class BenchResult {
238257
error: z.string().optional(),
239258
plan: z.string(),
240259
tasks: z.number().default(0),
260+
statsQError: z.number().optional(),
241261
}).array(),
242262
})
243263
const data = fs.readFileSync(filePath, 'utf-8');
@@ -286,3 +306,12 @@ export class BenchResult {
286306
function numericId(queryName: string): number {
287307
return parseInt([...queryName.matchAll(/(\d+)/g)][0][0])
288308
}
309+
310+
function median(values: number[]): number | undefined {
311+
if (values.length === 0) {
312+
return undefined
313+
}
314+
const sorted = [...values].sort((a, b) => a - b)
315+
const mid = Math.floor(sorted.length / 2)
316+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
317+
}

benchmarks/cdk/bin/datafusion-bench.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ const QueryResponse = z.object({
8787
count: z.number(),
8888
plan: z.string(),
8989
elapsed_ms: z.number(),
90-
tasks: z.number()
90+
tasks: z.number(),
91+
stats_q_error: z.number().nullable().optional()
9192
})
9293
type QueryResponse = z.infer<typeof QueryResponse>
9394

@@ -142,7 +143,13 @@ class DataFusionRunner implements BenchmarkRunner {
142143
response = await this.query(sql)
143144
}
144145

145-
return { rowCount: response.count, plan: response.plan, elapsed: response.elapsed_ms, tasks: response.tasks };
146+
return {
147+
rowCount: response.count,
148+
plan: response.plan,
149+
elapsed: response.elapsed_ms,
150+
tasks: response.tasks,
151+
statsQError: response.stats_q_error ?? undefined
152+
};
146153
}
147154

148155
private async query(sql: string): Promise<QueryResponse> {

benchmarks/cdk/bin/worker.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use datafusion_distributed::{
2020
get_distributed_channel_resolver, get_distributed_worker_resolver,
2121
rewrite_distributed_plan_with_metrics,
2222
};
23+
use datafusion_distributed_benchmarks::stats::stats_estimation_q_error;
2324
use futures::{StreamExt, TryFutureExt};
2425
use log::{error, info, warn};
2526
use object_store::aws::AmazonS3Builder;
@@ -48,6 +49,7 @@ struct QueryResult {
4849
count: usize,
4950
elapsed_ms: f64,
5051
tasks: usize,
52+
stats_q_error: Option<f64>,
5153
}
5254

5355
#[derive(Serialize)]
@@ -208,6 +210,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
208210
)
209211
.await
210212
.map_err(err)?;
213+
let stats_q_error = stats_estimation_q_error(&physical);
211214
let plan = display_plan_ascii(physical.as_ref(), true);
212215
drop(task);
213216

@@ -233,6 +236,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
233236
plan,
234237
elapsed_ms: ms,
235238
tasks: task_count,
239+
stats_q_error,
236240
}))
237241
}
238242
.inspect_err(|(_, msg)| {

benchmarks/src/datasets/common.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ pub async fn register_tables(
7777
let path = entry?.path();
7878
if path.is_dir() {
7979
let table_name = path.file_name().unwrap().to_str().unwrap();
80-
ctx.register_parquet(
81-
table_name,
82-
path.to_str().unwrap(),
83-
ParquetReadOptions::default(),
84-
)
85-
.await?;
80+
let _ = ctx
81+
.register_parquet(
82+
table_name,
83+
path.to_str().unwrap(),
84+
ParquetReadOptions::default(),
85+
)
86+
.await;
8687
}
8788
}
8889
Ok(())

benchmarks/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod datasets;
2+
pub mod stats;

benchmarks/src/results.rs

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{DATA_PATH, RESULTS_DIR};
22
use datafusion::common::utils::get_available_parallelism;
33
use datafusion::common::{Result, internal_datafusion_err};
4+
use datafusion_distributed_benchmarks::stats::median;
45
use serde::ser::SerializeSeq;
56
use serde::{Deserialize, Deserializer, Serialize, Serializer};
67
use std::fs;
@@ -13,6 +14,10 @@ use std::time::{Duration, SystemTime};
1314
pub struct QueryIter {
1415
pub row_count: usize,
1516
pub n_tasks: usize,
17+
/// Median q-error of the byte estimates made at dynamic stage boundaries.
18+
/// `None` when dynamic planning was disabled or no sampled boundary was present.
19+
#[serde(default)]
20+
pub stats_q_error: Option<f64>,
1621
#[serde(
1722
serialize_with = "serialize_elapsed",
1823
deserialize_with = "deserialize_elapsed"
@@ -279,29 +284,49 @@ impl BenchResult {
279284
pub fn print_comparison_total(base: &[BenchResult], new: &[BenchResult]) {
280285
let mut total_prev: u128 = 0;
281286
let mut total_new: u128 = 0;
287+
let mut stats_q_error_prev = vec![];
288+
let mut stats_q_error_new = vec![];
282289
for query in new {
283290
let Some(prev) = base.iter().find(|v| v.id == query.id) else {
284291
continue;
285292
};
286293
if let (Some(p), Some(n)) = (prev.representative_time(), query.representative_time()) {
287294
total_prev += p;
288295
total_new += n;
296+
stats_q_error_prev.extend(
297+
prev.iterations
298+
.iter()
299+
.filter_map(|iteration| iteration.stats_q_error),
300+
);
301+
stats_q_error_new.extend(
302+
query
303+
.iterations
304+
.iter()
305+
.filter_map(|iteration| iteration.stats_q_error),
306+
);
289307
}
290308
}
291-
if total_prev == 0 && total_new == 0 {
292-
return;
309+
310+
if total_prev != 0 || total_new != 0 {
311+
let (f, tag, emoji) = if total_new < total_prev {
312+
let f = total_prev as f64 / total_new as f64;
313+
(f, "faster", if f > 1.2 { "✅" } else { "✔" })
314+
} else {
315+
let f = total_new as f64 / total_prev.max(1) as f64;
316+
(f, "slower", if f > 1.2 { "❌" } else { "✖" })
317+
};
318+
println!(
319+
"{:>8}: prev={total_prev} ms, new={total_new} ms, diff={f:.2} {tag} {emoji}",
320+
"TOTAL"
321+
);
322+
}
323+
324+
match (median(stats_q_error_prev), median(stats_q_error_new)) {
325+
(Some(prev), Some(new)) => println!("{:>8}: prev={prev:.2}x, new={new:.2}x", "QERR P50"),
326+
(Some(prev), None) => println!("{:>8}: prev={prev:.2}x, new=n/a", "QERR P50"),
327+
(None, Some(new)) => println!("{:>8}: prev=n/a, new={new:.2}x", "QERR P50"),
328+
(None, None) => {}
293329
}
294-
let (f, tag, emoji) = if total_new < total_prev {
295-
let f = total_prev as f64 / total_new as f64;
296-
(f, "faster", if f > 1.2 { "✅" } else { "✔" })
297-
} else {
298-
let f = total_new as f64 / total_prev.max(1) as f64;
299-
(f, "slower", if f > 1.2 { "❌" } else { "✖" })
300-
};
301-
println!(
302-
"{:>8}: prev={total_prev} ms, new={total_new} ms, diff={f:.2} {tag} {emoji}",
303-
"TOTAL"
304-
);
305330
}
306331

307332
fn serialize_bench_results<S: Serializer>(

benchmarks/src/run.rs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use datafusion::common::{config_err, exec_err, not_impl_err};
2525
use datafusion::datasource::source::DataSourceExec;
2626
use datafusion::error::{DataFusionError, Result};
2727
use datafusion::execution::SessionStateBuilder;
28-
use datafusion::physical_plan::collect;
28+
use datafusion::physical_plan::{ExecutionPlan, collect};
2929
use datafusion::prelude::*;
3030
use datafusion_distributed::test_utils::localhost::LocalHostWorkerResolver;
3131
use datafusion_distributed::test_utils::work_unit_file_scan::{
@@ -37,6 +37,7 @@ use datafusion_distributed::{
3737
display_plan_ascii, rewrite_distributed_plan_with_metrics,
3838
};
3939
use datafusion_distributed_benchmarks::datasets::{clickbench, register_tables, tpcds, tpch};
40+
use datafusion_distributed_benchmarks::stats::stats_estimation_q_error;
4041
use std::error::Error;
4142
use std::fs;
4243
use std::path::PathBuf;
@@ -88,14 +89,6 @@ pub struct RunOpt {
8889
#[structopt(long)]
8990
cardinality_task_sf: Option<f64>,
9091

91-
/// Use children isolator UNIONs for distributing UNION operations.
92-
#[structopt(long)]
93-
children_isolator_unions: bool,
94-
95-
/// Turns on broadcast joins.
96-
#[structopt(long = "broadcast-joins")]
97-
broadcast_joins: bool,
98-
9992
/// Collects metrics across network boundaries
10093
#[structopt(long)]
10194
collect_metrics: bool,
@@ -216,9 +209,11 @@ impl RunOpt {
216209
"none" => None,
217210
v => return config_err!("Unknown compression type {v}"),
218211
})?
219-
.with_distributed_children_isolator_unions(self.children_isolator_unions)?
220-
.with_distributed_broadcast_joins(self.broadcast_joins)?
221-
.with_distributed_metrics_collection(self.collect_metrics || self.debug)?
212+
.with_distributed_children_isolator_unions(true)?
213+
.with_distributed_broadcast_joins(true)?
214+
.with_distributed_metrics_collection(
215+
self.collect_metrics || self.debug || self.dynamic,
216+
)?
222217
.with_distributed_max_tasks_per_stage(self.max_tasks_per_stage)?
223218
.with_distributed_user_codec(WorkUnitFileScanCodec)
224219
.with_distributed_task_estimator(WorkUnitFileScanTaskEstimator)
@@ -290,18 +285,44 @@ impl RunOpt {
290285
}
291286

292287
match self.execute_query(ctx, query).await {
293-
Ok((result, n_tasks)) => {
288+
Ok((result, n_tasks, physical_plan)) => {
294289
let elapsed = start.elapsed();
295290
let ms = elapsed.as_secs_f64() * 1000.0;
296291
let row_count = result.iter().map(|b| b.num_rows()).sum();
297-
println!(
298-
"Query {id} iteration {i} took {ms:.1} ms and returned {row_count} rows"
299-
);
292+
let physical_plan = if self.dynamic || self.debug {
293+
rewrite_distributed_plan_with_metrics(
294+
physical_plan,
295+
DistributedMetricsFormat::PerTask,
296+
)
297+
.await?
298+
} else {
299+
physical_plan
300+
};
301+
let stats_q_error = match self.dynamic {
302+
true => stats_estimation_q_error(&physical_plan),
303+
false => None,
304+
};
305+
if let Some(q_error) = stats_q_error {
306+
println!(
307+
"Query {id} iteration {i} took {ms:.1} ms, stats q-error {q_error:.2}x and returned {row_count} rows"
308+
);
309+
} else {
310+
println!(
311+
"Query {id} iteration {i} took {ms:.1} ms and returned {row_count} rows"
312+
);
313+
}
314+
if self.debug {
315+
println!(
316+
"=== Physical plan with metrics ===\n{}\n",
317+
display_plan_ascii(physical_plan.as_ref(), true)
318+
);
319+
}
300320

301321
bench_query.iterations.push(QueryIter {
302322
elapsed,
303323
row_count,
304324
n_tasks,
325+
stats_q_error,
305326
error: None,
306327
});
307328
}
@@ -311,6 +332,7 @@ impl RunOpt {
311332
elapsed: Duration::from_millis(0),
312333
row_count: 0,
313334
n_tasks: 0,
335+
stats_q_error: None,
314336
error: Some(err.to_string()),
315337
});
316338
continue 'outer;
@@ -327,7 +349,7 @@ impl RunOpt {
327349
&self,
328350
ctx: &SessionContext,
329351
sql: &str,
330-
) -> Result<(Vec<RecordBatch>, usize)> {
352+
) -> Result<(Vec<RecordBatch>, usize, Arc<dyn ExecutionPlan>)> {
331353
let plan = ctx.sql(sql).await?;
332354
let (state, plan) = plan.into_parts();
333355

@@ -341,18 +363,7 @@ impl RunOpt {
341363
Ok(Transformed::no(node))
342364
})?;
343365
let result = collect(physical_plan.clone(), state.task_ctx()).await?;
344-
if self.debug {
345-
let physical_plan = rewrite_distributed_plan_with_metrics(
346-
physical_plan.clone(),
347-
DistributedMetricsFormat::PerTask,
348-
)
349-
.await?;
350-
println!(
351-
"=== Physical plan with metrics ===\n{}\n",
352-
display_plan_ascii(physical_plan.as_ref(), true)
353-
);
354-
}
355-
Ok((result, n_tasks))
366+
Ok((result, n_tasks, physical_plan))
356367
}
357368

358369
fn get_path(&self) -> Result<PathBuf> {

0 commit comments

Comments
 (0)