Skip to content
This repository was archived by the owner on Jan 7, 2025. It is now read-only.

Commit 7b2b2b1

Browse files
authored
Merge branch 'bowad/unnest-exists' into bowad/tpch-q18-attempt
2 parents 3ddd53c + bcc28c9 commit 7b2b2b1

39 files changed

+1975
-967
lines changed

Cargo.lock

+397-232
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

optd-sqlplannertest/Cargo.toml

+9-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ repository = { workspace = true }
1313
[dependencies]
1414
clap = { version = "4.5.4", features = ["derive"] }
1515
anyhow = { version = "1", features = ["backtrace"] }
16-
sqlplannertest = "0.3"
16+
sqlplannertest = "0.4"
1717
async-trait = "0.1"
1818
datafusion-optd-cli = { path = "../datafusion-optd-cli", version = "43.0.0" }
1919
optd-datafusion-repr-adv-cost = { path = "../optd-datafusion-repr-adv-cost", version = "0.1" }
@@ -40,6 +40,14 @@ optd-datafusion-repr = { path = "../optd-datafusion-repr", version = "0.1" }
4040
itertools = "0.13"
4141
lazy_static = "1.4.0"
4242

43+
[dev-dependencies]
44+
criterion = { version = "0.5.1", features = ["async_tokio"] }
45+
serde_yaml = "0.9"
46+
4347
[[test]]
4448
name = "planner_test"
4549
harness = false
50+
51+
[[bench]]
52+
name = "planner_bench"
53+
harness = false

optd-sqlplannertest/README.md

+50-7
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,55 @@
33
These test cases use the [sqlplannertest](https://crates.io/crates/sqlplannertest) crate to execute SQL queries and inspect their output.
44
They do not check whether a plan is correct, and instead rely on a text-based diff of the query's output to determine whether something is different than the expected output.
55

6+
We are also using this crate to generate benchmarks for evaluating optd's performance. With the help with the [criterion](https://crates.io/crates/criterion) crate, we can benchmark planning time and the execution time of physical plan produced by the optimizer.
67

78
## Execute Test Cases
89

10+
**Running all test cases**
11+
912
```shell
1013
cargo test -p optd-sqlplannertest
1114
# or use nextest
1215
cargo nextest run -p optd-sqlplannertest
1316
```
17+
18+
**Running tests in specfic modules or files**
19+
20+
```shell
21+
# Running all test cases in the tpch module
22+
cargo nextest run -p optd-sqlplannertest tpch
23+
# Running all test cases in the tests/subqueries/subquery_unnesting.yml
24+
cargo nextest run -p optd-sqlplannertest subquery::subquery_unnesting
25+
```
26+
27+
## Executing Benchmarks
28+
29+
There are two metrics we care about when evaluating
30+
31+
### Usage
32+
33+
```shell
34+
# Benchmark all TPC-H queries with "bench" task enabled
35+
cargo bench --bench planner_bench tpch/
36+
37+
# Benchmark TPC-H Q1
38+
cargo bench --bench planner_bench tpch/q1/
39+
40+
# Benchmark TPC-H Q1 planning
41+
cargo bench --bench planner_bench tpch/q1/planning
42+
43+
# Benchmark TPC-H Q1 execution
44+
cargo bench --bench planner_bench tpch/q1/execution
45+
46+
# View the HTML report
47+
python3 -m http.server -d ./target/criterion/
48+
```
49+
50+
### Limitations
51+
52+
`planner_bench` can only handle `sqlplannertest` yaml-based test file with single test case.
53+
54+
1455
## Add New Test Case
1556

1657
To add a SQL query tests, create a YAML file in a subdir in "tests".
@@ -30,11 +71,11 @@ Each file can contain multiple tests that are executed in sequential order from
3071
- explain:logical_optd,physical_optd
3172
desc: Equality predicate
3273
```
33-
| Name | Description |
34-
| ---------- | ------------------------------------------------------------------ |
35-
| `sql` | List of SQL statements to execute separate by newlines |
36-
| `tasks` | How to execute the SQL statements. See [Tasks](#tasks) below |
37-
| `desc` | (Optional) Text description of what the test cases represents |
74+
| Name | Description |
75+
| ------- | ------------------------------------------------------------- |
76+
| `sql` | List of SQL statements to execute separate by newlines |
77+
| `tasks` | How to execute the SQL statements. See [Tasks](#tasks) below |
78+
| `desc` | (Optional) Text description of what the test cases represents |
3879

3980
After adding the YAML file, you then need to use the update command to automatically create the matching SQL file that contains the expected output of the test cases.
4081

@@ -46,14 +87,16 @@ The following commands will automatically update all of them for you. You should
4687
```shell
4788
# Update all test cases
4889
cargo run -p optd-sqlplannertest --bin planner_test_apply
49-
# or, supply a list of directories to update
50-
cargo run -p optd-sqlplannertest --bin planner_test_apply -- subqueries
90+
# or, supply a list of modules or files to update
91+
cargo run -p optd-sqlplannertest --bin planner_test_apply -- subqueries tpch::q1
5192
```
5293

5394
## Tasks
5495

5596
The `explain` and `execute` task will be run with datafusion's logical optimizer disabled. Each task has some toggleable flags to control its behavior.
5697

98+
The `bench` task is only used in benchmarks. A test case can only be executed as a benchmark if a bench task exists.
99+
57100
### `execute` Task
58101

59102
#### Flags
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use std::{
2+
future::Future,
3+
path::{Path, PathBuf},
4+
};
5+
6+
use anyhow::{bail, Context, Result};
7+
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
8+
use optd_sqlplannertest::bench_helper::{
9+
bench_run, bench_setup, ExecutionBenchRunner, PlannerBenchRunner, PlanningBenchRunner,
10+
};
11+
use sqlplannertest::{discover_tests_with_selections, parse_test_cases, TestCase};
12+
use tokio::runtime::Runtime;
13+
14+
fn criterion_benchmark(c: &mut Criterion) {
15+
let selection = "tpch";
16+
let selections = vec![selection.to_string()];
17+
18+
let tests_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
19+
planner_bench_runner(
20+
&tests_dir,
21+
|| async { PlanningBenchRunner::new().await },
22+
&selections,
23+
c,
24+
)
25+
.unwrap();
26+
27+
let path = tests_dir.join(format!("{selection}/bench_populate.sql"));
28+
let populate_sql = std::fs::read_to_string(&path)
29+
.with_context(|| format!("failed to read {}", path.display()))
30+
.unwrap();
31+
32+
planner_bench_runner(
33+
&tests_dir,
34+
move || {
35+
let populate_sql = populate_sql.clone();
36+
async { ExecutionBenchRunner::new(populate_sql).await }
37+
},
38+
&selections,
39+
c,
40+
)
41+
.unwrap();
42+
}
43+
44+
/// Discovers and bench each test case.
45+
///
46+
/// The user needs to provide a runner function that creates a runner that
47+
/// implements the [`PlannerBenchRunner`] trait.
48+
///
49+
/// A test case will be selected if:
50+
///
51+
/// 1. it's included in the `tests_dir` as part of `selections`.
52+
/// 2. has `bench` listed in the task list.
53+
///
54+
/// ## Limitation
55+
///
56+
/// Currently only accept sqlplannertest files with single test case.
57+
fn planner_bench_runner<F, Ft, R>(
58+
tests_dir: impl AsRef<Path>,
59+
runner_fn: F,
60+
selections: &[String],
61+
c: &mut Criterion,
62+
) -> Result<()>
63+
where
64+
F: Fn() -> Ft + Send + Sync + 'static + Clone,
65+
Ft: Future<Output = Result<R>> + Send,
66+
R: PlannerBenchRunner + 'static,
67+
{
68+
let tests = discover_tests_with_selections(&tests_dir, selections)?
69+
.map(|path| {
70+
let path = path?;
71+
let relative_path = path
72+
.strip_prefix(&tests_dir)
73+
.context("unable to relative path")?
74+
.as_os_str();
75+
let testname = relative_path
76+
.to_str()
77+
.context("unable to convert to string")?
78+
.to_string();
79+
Ok::<_, anyhow::Error>((path, testname))
80+
})
81+
.collect::<Result<Vec<_>, _>>()?;
82+
83+
for (path, testname) in tests {
84+
bench_runner(path, testname, runner_fn.clone(), c)?;
85+
}
86+
87+
Ok(())
88+
}
89+
90+
/// Bench runner for a test case.
91+
fn bench_runner<F, Ft, R>(
92+
path: PathBuf,
93+
testname: String,
94+
runner_fn: F,
95+
c: &mut Criterion,
96+
) -> Result<()>
97+
where
98+
F: Fn() -> Ft + Send + Sync + 'static + Clone,
99+
Ft: Future<Output = Result<R>> + Send,
100+
R: PlannerBenchRunner,
101+
{
102+
fn build_runtime() -> Runtime {
103+
tokio::runtime::Builder::new_current_thread()
104+
.enable_all()
105+
.build()
106+
.unwrap()
107+
}
108+
109+
let testcases = std::fs::read(&path)?;
110+
let testcases: Vec<TestCase> = serde_yaml::from_slice(&testcases)?;
111+
112+
let testcases = parse_test_cases(
113+
{
114+
let mut path = path.clone();
115+
path.pop();
116+
path
117+
},
118+
testcases,
119+
)?;
120+
121+
if testcases.len() != 1 {
122+
bail!(
123+
"planner_bench can only handle sqlplannertest yml file with one test cases, {} has {}",
124+
path.display(),
125+
testcases.len()
126+
);
127+
}
128+
129+
let testcase = &testcases[0];
130+
131+
let should_bench = testcase.tasks.iter().any(|x| x.starts_with("bench"));
132+
if should_bench {
133+
let mut group = c.benchmark_group(testname.strip_suffix(".yml").unwrap());
134+
let runtime = build_runtime();
135+
group.bench_function(R::BENCH_NAME, |b| {
136+
b.iter_batched(
137+
|| bench_setup(&runtime, runner_fn.clone(), testcase),
138+
|(runner, input, flags)| {
139+
bench_run(&runtime, runner, black_box(input), testcase, &flags)
140+
},
141+
BatchSize::PerIteration,
142+
);
143+
});
144+
group.finish();
145+
}
146+
Ok(())
147+
}
148+
149+
criterion_group!(benches, criterion_benchmark);
150+
criterion_main!(benches);
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
pub mod execution;
2+
pub mod planning;
3+
4+
use std::future::Future;
5+
6+
use crate::TestFlags;
7+
use anyhow::Result;
8+
use tokio::runtime::Runtime;
9+
10+
pub use execution::ExecutionBenchRunner;
11+
pub use planning::PlanningBenchRunner;
12+
13+
pub trait PlannerBenchRunner {
14+
/// Describes what the benchmark is evaluating.
15+
const BENCH_NAME: &str;
16+
/// Benchmark's input.
17+
type BenchInput;
18+
19+
/// Setups the necessary environment for the benchmark based on the test case.
20+
/// Returns the input needed for the benchmark.
21+
fn setup(
22+
&mut self,
23+
test_case: &sqlplannertest::ParsedTestCase,
24+
) -> impl std::future::Future<Output = Result<(Self::BenchInput, TestFlags)>> + Send;
25+
26+
/// Runs the actual benchmark based on the test case and input.
27+
fn bench(
28+
self,
29+
input: Self::BenchInput,
30+
test_case: &sqlplannertest::ParsedTestCase,
31+
flags: &TestFlags,
32+
) -> impl std::future::Future<Output = Result<()>> + Send;
33+
}
34+
35+
/// Sync wrapper for [`PlannerBenchRunner::setup`]
36+
pub fn bench_setup<F, Ft, R>(
37+
runtime: &Runtime,
38+
runner_fn: F,
39+
testcase: &sqlplannertest::ParsedTestCase,
40+
) -> (R, R::BenchInput, TestFlags)
41+
where
42+
F: Fn() -> Ft + Send + Sync + 'static + Clone,
43+
Ft: Future<Output = Result<R>> + Send,
44+
R: PlannerBenchRunner,
45+
{
46+
runtime.block_on(async {
47+
let mut runner = runner_fn().await.unwrap();
48+
let (input, flags) = runner.setup(testcase).await.unwrap();
49+
(runner, input, flags)
50+
})
51+
}
52+
53+
/// Sync wrapper for [`PlannerBenchRunner::bench`]
54+
pub fn bench_run<R>(
55+
runtime: &Runtime,
56+
runner: R,
57+
input: R::BenchInput,
58+
testcase: &sqlplannertest::ParsedTestCase,
59+
flags: &TestFlags,
60+
) where
61+
R: PlannerBenchRunner,
62+
{
63+
runtime.block_on(async { runner.bench(input, testcase, flags).await.unwrap() });
64+
}

0 commit comments

Comments
 (0)