Skip to content

Commit ada6559

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

9 files changed

Lines changed: 205 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: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ use std::time::{Duration, SystemTime};
1313
pub struct QueryIter {
1414
pub row_count: usize,
1515
pub n_tasks: usize,
16+
/// Median q-error of the byte estimates made at dynamic stage boundaries.
17+
/// `None` when dynamic planning was disabled or no sampled boundary was present.
18+
#[serde(default)]
19+
pub stats_q_error: Option<f64>,
1620
#[serde(
1721
serialize_with = "serialize_elapsed",
1822
deserialize_with = "deserialize_elapsed"
@@ -21,6 +25,19 @@ pub struct QueryIter {
2125
pub error: Option<String>,
2226
}
2327

28+
fn median(mut values: Vec<f64>) -> Option<f64> {
29+
if values.is_empty() {
30+
return None;
31+
}
32+
values.sort_unstable_by(f64::total_cmp);
33+
let mid = values.len() / 2;
34+
Some(if values.len().is_multiple_of(2) {
35+
(values[mid - 1] + values[mid]) / 2.0
36+
} else {
37+
values[mid]
38+
})
39+
}
40+
2441
/// A single benchmark case
2542
#[derive(Debug, Serialize, Deserialize)]
2643
pub struct BenchResult {
@@ -279,29 +296,49 @@ impl BenchResult {
279296
pub fn print_comparison_total(base: &[BenchResult], new: &[BenchResult]) {
280297
let mut total_prev: u128 = 0;
281298
let mut total_new: u128 = 0;
299+
let mut stats_q_error_prev = vec![];
300+
let mut stats_q_error_new = vec![];
282301
for query in new {
283302
let Some(prev) = base.iter().find(|v| v.id == query.id) else {
284303
continue;
285304
};
286305
if let (Some(p), Some(n)) = (prev.representative_time(), query.representative_time()) {
287306
total_prev += p;
288307
total_new += n;
308+
stats_q_error_prev.extend(
309+
prev.iterations
310+
.iter()
311+
.filter_map(|iteration| iteration.stats_q_error),
312+
);
313+
stats_q_error_new.extend(
314+
query
315+
.iterations
316+
.iter()
317+
.filter_map(|iteration| iteration.stats_q_error),
318+
);
289319
}
290320
}
291-
if total_prev == 0 && total_new == 0 {
292-
return;
321+
322+
if total_prev != 0 || total_new != 0 {
323+
let (f, tag, emoji) = if total_new < total_prev {
324+
let f = total_prev as f64 / total_new as f64;
325+
(f, "faster", if f > 1.2 { "✅" } else { "✔" })
326+
} else {
327+
let f = total_new as f64 / total_prev.max(1) as f64;
328+
(f, "slower", if f > 1.2 { "❌" } else { "✖" })
329+
};
330+
println!(
331+
"{:>8}: prev={total_prev} ms, new={total_new} ms, diff={f:.2} {tag} {emoji}",
332+
"TOTAL"
333+
);
334+
}
335+
336+
match (median(stats_q_error_prev), median(stats_q_error_new)) {
337+
(Some(prev), Some(new)) => println!("{:>8}: prev={prev:.2}x, new={new:.2}x", "QERR P50"),
338+
(Some(prev), None) => println!("{:>8}: prev={prev:.2}x, new=n/a", "QERR P50"),
339+
(None, Some(new)) => println!("{:>8}: prev=n/a, new={new:.2}x", "QERR P50"),
340+
(None, None) => {}
293341
}
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-
);
305342
}
306343

307344
fn serialize_bench_results<S: Serializer>(

0 commit comments

Comments
 (0)