Skip to content

Commit 94eec9c

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

12 files changed

Lines changed: 298 additions & 56 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ aws-config = "1"
3232
aws-sdk-ec2 = "1"
3333
openssl = { version = "0.10", features = ["vendored"] } # Keep this. Necessary for the remote benchmarks worker.
3434
mimalloc = "0.1"
35+
sketches-ddsketch = "0.3"
3536

3637
[dev-dependencies]
3738
criterion = "0.5"

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export interface ExecuteQueryResult {
6868
plan: string
6969
elapsed: number
7070
tasks: number
71+
statsQErrorP50?: number
72+
statsQErrorP95?: number
7173
}
7274

7375
export interface BenchmarkRunner {
@@ -193,11 +195,19 @@ export async function runBenchmark(
193195
rowCount: response.rowCount,
194196
plan: response.plan,
195197
tasks: response.tasks,
198+
statsQErrorP50: response.statsQErrorP50,
199+
statsQErrorP95: response.statsQErrorP95,
196200
})
197201

198-
console.log(
199-
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms and returned ${response.rowCount} rows`
200-
);
202+
if (response.statsQErrorP50 !== undefined && response.statsQErrorP95 !== undefined) {
203+
console.log(
204+
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms, stats q-error P50 ${response.statsQErrorP50.toFixed(2)}x, P95 ${response.statsQErrorP95.toFixed(2)}x and returned ${response.rowCount} rows`
205+
);
206+
} else {
207+
console.log(
208+
`Query ${id} iteration ${i} took ${Math.round(response.elapsed)} ms and returned ${response.rowCount} rows`
209+
);
210+
}
201211
}
202212

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

benchmarks/cdk/bin/@results.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface QueryIter {
1212
rowCount: number;
1313
elapsed: number; // Duration in milliseconds
1414
tasks: number;
15+
statsQErrorP50?: number;
16+
statsQErrorP95?: number;
1517
error?: string;
1618
}
1719

@@ -80,16 +82,32 @@ export class BenchmarkRun {
8082
console.log(`=== Comparing ${this.dataset} results from engine '${other.engine}' [prev] with '${this.engine}' [new] ===`);
8183
let totalTimePrev = 0
8284
let totalTimeNew = 0
85+
const statsQErrorP50Prev: number[] = []
86+
const statsQErrorP50New: number[] = []
87+
const statsQErrorP95Prev: number[] = []
88+
const statsQErrorP95New: number[] = []
8389
for (const query of this.results) {
8490
const prevQuery = other.results.find(v => v.id === query.id);
8591
if (!prevQuery) {
8692
continue;
8793
}
8894
const timePrev = prevQuery.representativeTime()
8995
const timeNew = query.representativeTime()
90-
if (timePrev && timeNew) {
96+
if (timePrev !== undefined && timeNew !== undefined) {
9197
totalTimePrev += timePrev
9298
totalTimeNew += timeNew
99+
statsQErrorP50Prev.push(...prevQuery.iterations.flatMap(iter =>
100+
iter.statsQErrorP50 === undefined ? [] : [iter.statsQErrorP50]
101+
))
102+
statsQErrorP50New.push(...query.iterations.flatMap(iter =>
103+
iter.statsQErrorP50 === undefined ? [] : [iter.statsQErrorP50]
104+
))
105+
statsQErrorP95Prev.push(...prevQuery.iterations.flatMap(iter =>
106+
iter.statsQErrorP95 === undefined ? [] : [iter.statsQErrorP95]
107+
))
108+
statsQErrorP95New.push(...query.iterations.flatMap(iter =>
109+
iter.statsQErrorP95 === undefined ? [] : [iter.statsQErrorP95]
110+
))
93111
}
94112

95113
query.compare(prevQuery);
@@ -108,6 +126,9 @@ export class BenchmarkRun {
108126
console.log(
109127
`${"TOTAL".padStart(8)}: prev=${totalTimePrev.toString()} ms, new=${totalTimeNew.toString()} ms, diff=${f.toFixed(2)} ${tag} ${emoji}`
110128
);
129+
130+
printQErrorComparison("QERR P50", statsQErrorP50Prev, statsQErrorP50New)
131+
printQErrorComparison("QERR P95", statsQErrorP95Prev, statsQErrorP95New)
111132
}
112133

113134
compareWithPrevious(): void {
@@ -238,6 +259,8 @@ export class BenchResult {
238259
error: z.string().optional(),
239260
plan: z.string(),
240261
tasks: z.number().default(0),
262+
statsQErrorP50: z.number().optional(),
263+
statsQErrorP95: z.number().optional(),
241264
}).array(),
242265
})
243266
const data = fs.readFileSync(filePath, 'utf-8');
@@ -286,3 +309,24 @@ export class BenchResult {
286309
function numericId(queryName: string): number {
287310
return parseInt([...queryName.matchAll(/(\d+)/g)][0][0])
288311
}
312+
313+
function printQErrorComparison(label: string, prev: number[], next: number[]): void {
314+
const prevValue = median(prev)
315+
const nextValue = median(next)
316+
if (prevValue !== undefined && nextValue !== undefined) {
317+
console.log(`${label.padStart(8)}: prev=${prevValue.toFixed(2)}x, new=${nextValue.toFixed(2)}x`)
318+
} else if (prevValue !== undefined) {
319+
console.log(`${label.padStart(8)}: prev=${prevValue.toFixed(2)}x, new=n/a`)
320+
} else if (nextValue !== undefined) {
321+
console.log(`${label.padStart(8)}: prev=n/a, new=${nextValue.toFixed(2)}x`)
322+
}
323+
}
324+
325+
function median(values: number[]): number | undefined {
326+
if (values.length === 0) {
327+
return undefined
328+
}
329+
const sorted = [...values].sort((a, b) => a - b)
330+
const mid = Math.floor(sorted.length / 2)
331+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
332+
}

benchmarks/cdk/bin/datafusion-bench.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ 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_p50: z.number().nullable(),
92+
stats_q_error_p95: z.number().nullable()
9193
})
9294
type QueryResponse = z.infer<typeof QueryResponse>
9395

@@ -142,7 +144,14 @@ class DataFusionRunner implements BenchmarkRunner {
142144
response = await this.query(sql)
143145
}
144146

145-
return { rowCount: response.count, plan: response.plan, elapsed: response.elapsed_ms, tasks: response.tasks };
147+
return {
148+
rowCount: response.count,
149+
plan: response.plan,
150+
elapsed: response.elapsed_ms,
151+
tasks: response.tasks,
152+
statsQErrorP50: response.stats_q_error_p50 ?? undefined,
153+
statsQErrorP95: response.stats_q_error_p95 ?? undefined
154+
};
146155
}
147156

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

benchmarks/cdk/bin/worker.rs

Lines changed: 6 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,8 @@ struct QueryResult {
4849
count: usize,
4950
elapsed_ms: f64,
5051
tasks: usize,
52+
stats_q_error_p50: Option<f64>,
53+
stats_q_error_p95: Option<f64>,
5154
}
5255

5356
#[derive(Serialize)]
@@ -208,6 +211,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
208211
)
209212
.await
210213
.map_err(err)?;
214+
let stats_q_error = stats_estimation_q_error(&physical);
211215
let plan = display_plan_ascii(physical.as_ref(), true);
212216
drop(task);
213217

@@ -233,6 +237,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
233237
plan,
234238
elapsed_ms: ms,
235239
tasks: task_count,
240+
stats_q_error_p50: stats_q_error.map(|q_error| q_error.p50),
241+
stats_q_error_p95: stats_q_error.map(|q_error| q_error.p95),
236242
}))
237243
}
238244
.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: 58 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,12 @@ use std::time::{Duration, SystemTime};
1314
pub struct QueryIter {
1415
pub row_count: usize,
1516
pub n_tasks: usize,
17+
/// P50 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+
pub stats_q_error_p50: Option<f64>,
20+
/// P95 q-error of the byte estimates made at dynamic stage boundaries.
21+
/// `None` when dynamic planning was disabled or no sampled boundary was present.
22+
pub stats_q_error_p95: Option<f64>,
1623
#[serde(
1724
serialize_with = "serialize_elapsed",
1825
deserialize_with = "deserialize_elapsed"
@@ -279,29 +286,67 @@ impl BenchResult {
279286
pub fn print_comparison_total(base: &[BenchResult], new: &[BenchResult]) {
280287
let mut total_prev: u128 = 0;
281288
let mut total_new: u128 = 0;
289+
let mut stats_q_error_p50_prev = vec![];
290+
let mut stats_q_error_p50_new = vec![];
291+
let mut stats_q_error_p95_prev = vec![];
292+
let mut stats_q_error_p95_new = vec![];
282293
for query in new {
283294
let Some(prev) = base.iter().find(|v| v.id == query.id) else {
284295
continue;
285296
};
286297
if let (Some(p), Some(n)) = (prev.representative_time(), query.representative_time()) {
287298
total_prev += p;
288299
total_new += n;
300+
stats_q_error_p50_prev.extend(
301+
prev.iterations
302+
.iter()
303+
.filter_map(|iteration| iteration.stats_q_error_p50),
304+
);
305+
stats_q_error_p50_new.extend(
306+
query
307+
.iterations
308+
.iter()
309+
.filter_map(|iteration| iteration.stats_q_error_p50),
310+
);
311+
stats_q_error_p95_prev.extend(
312+
prev.iterations
313+
.iter()
314+
.filter_map(|iteration| iteration.stats_q_error_p95),
315+
);
316+
stats_q_error_p95_new.extend(
317+
query
318+
.iterations
319+
.iter()
320+
.filter_map(|iteration| iteration.stats_q_error_p95),
321+
);
289322
}
290323
}
291-
if total_prev == 0 && total_new == 0 {
292-
return;
324+
325+
if total_prev != 0 || total_new != 0 {
326+
let (f, tag, emoji) = if total_new < total_prev {
327+
let f = total_prev as f64 / total_new as f64;
328+
(f, "faster", if f > 1.2 { "✅" } else { "✔" })
329+
} else {
330+
let f = total_new as f64 / total_prev.max(1) as f64;
331+
(f, "slower", if f > 1.2 { "❌" } else { "✖" })
332+
};
333+
println!(
334+
"{:>8}: prev={total_prev} ms, new={total_new} ms, diff={f:.2} {tag} {emoji}",
335+
"TOTAL"
336+
);
337+
}
338+
339+
print_q_error_comparison("QERR P50", stats_q_error_p50_prev, stats_q_error_p50_new);
340+
print_q_error_comparison("QERR P95", stats_q_error_p95_prev, stats_q_error_p95_new);
341+
}
342+
343+
fn print_q_error_comparison(label: &str, prev: Vec<f64>, new: Vec<f64>) {
344+
match (median(prev), median(new)) {
345+
(Some(prev), Some(new)) => println!("{label:>8}: prev={prev:.2}x, new={new:.2}x"),
346+
(Some(prev), None) => println!("{label:>8}: prev={prev:.2}x, new=n/a"),
347+
(None, Some(new)) => println!("{label:>8}: prev=n/a, new={new:.2}x"),
348+
(None, None) => {}
293349
}
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-
);
305350
}
306351

307352
fn serialize_bench_results<S: Serializer>(

0 commit comments

Comments
 (0)