Skip to content

Commit c7c6a8f

Browse files
Concurrent bench (#357)
1 parent b6a4fd3 commit c7c6a8f

12 files changed

Lines changed: 525 additions & 114 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"Bash(ls:*)",
2929
"Bash(grep:*)",
3030
"Bash(cargo fmt:*)",
31-
"Bash(./target/release/dft:*)"
31+
"Bash(./target/release/dft:*)",
32+
"Bash(/Users/matth/OpenSource/datafusion-tui/target/debug/dft:*)"
3233
],
3334
"deny": [],
3435
"ask": []

CLAUDE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,55 @@ cargo run --features=flightsql -- serve-flightsql
3333
cargo run -- generate-tpch
3434
```
3535

36+
### Benchmarking
37+
38+
Benchmarks measure query performance with detailed timing breakdowns:
39+
40+
```bash
41+
# Serial benchmark (default, 10 iterations)
42+
cargo run -- -c "SELECT 1" --bench
43+
44+
# Custom iteration count
45+
cargo run -- -c "SELECT 1" --bench -n 100
46+
47+
# Concurrent benchmark (measures throughput under load)
48+
cargo run -- -c "SELECT 1" --bench --concurrent
49+
50+
# With custom iterations and concurrency
51+
cargo run -- -c "SELECT 1" --bench -n 100 --concurrent
52+
53+
# Save results to CSV
54+
cargo run -- -c "SELECT 1" --bench --save results.csv
55+
56+
# Append to existing results
57+
cargo run -- -c "SELECT 2" --bench --concurrent --save results.csv --append
58+
59+
# Warm up cache before benchmarking
60+
cargo run -- -c "SELECT * FROM t" --bench --run-before "CREATE TABLE t AS VALUES (1)"
61+
```
62+
63+
**Benchmark Modes:**
64+
- **Serial** (default): Measures query performance in isolation
65+
- Shows pure query execution time without contention
66+
- Ideal for understanding baseline performance
67+
68+
- **Concurrent** (`--concurrent`): Measures performance under load
69+
- Runs iterations in parallel (concurrency = min(iterations, CPU cores))
70+
- Shows throughput (queries/second) with multiple clients
71+
- Reveals resource contention and bottlenecks
72+
- Higher mean/median times are expected due to concurrent load
73+
74+
**Output:**
75+
- Timing breakdown: logical planning, physical planning, execution, total
76+
- Statistics: min, max, mean, median for each phase
77+
- CSV format includes `concurrency_mode` column (serial or concurrent(N))
78+
79+
**FlightSQL Benchmarks:**
80+
```bash
81+
# Benchmark FlightSQL server (requires --flightsql flag and server running)
82+
cargo run -- -c "SELECT 1" --bench --flightsql --concurrent
83+
```
84+
3685
### Testing
3786

3887
Tests are organized by feature and component:

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ dft -f query.sql
6868
# Benchmark a query (with stats)
6969
dft -c "SELECT * FROM my_table" --bench
7070

71+
# Concurrent benchmark (measures throughput under load)
72+
dft -c "SELECT * FROM my_table" --bench --concurrent
73+
74+
# Save benchmark results to CSV
75+
dft -c "SELECT * FROM my_table" --bench --save results.csv
76+
7177
# Start FlightSQL Server (requires `flightsql` feature)
7278
dft serve-flightsql
7379

@@ -78,6 +84,39 @@ dft serve-http
7884
dft generate-tpch
7985
```
8086

87+
### Benchmarking
88+
89+
`dft` includes built-in benchmarking to measure query performance with detailed timing breakdowns:
90+
91+
```sh
92+
# Serial benchmark (default) - measures query performance in isolation
93+
dft -c "SELECT * FROM my_table" --bench
94+
95+
# Concurrent benchmark - measures throughput under load
96+
dft -c "SELECT * FROM my_table" --bench --concurrent
97+
98+
# Custom iteration count
99+
dft -c "SELECT * FROM my_table" --bench -n 100
100+
101+
# Save results to CSV for analysis
102+
dft -c "SELECT * FROM my_table" --bench --save results.csv
103+
104+
# Compare serial vs concurrent performance
105+
dft -c "SELECT * FROM my_table" --bench --save results.csv
106+
dft -c "SELECT * FROM my_table" --bench --concurrent --save results.csv --append
107+
```
108+
109+
**Benchmark Output:**
110+
- Timing breakdown by phase: logical planning, physical planning, execution
111+
- Statistics: min, max, mean, median for each phase
112+
- Row counts validation across all runs
113+
- CSV export with `concurrency_mode` column for result comparison
114+
115+
**Serial vs Concurrent:**
116+
- **Serial**: Pure query execution time without contention (baseline performance)
117+
- **Concurrent**: Throughput measurement with parallel execution (reveals bottlenecks and contention)
118+
- Concurrent mode uses adaptive concurrency: `min(iterations, CPU cores)`
119+
81120
### Setting Up Tables with DDL
82121

83122
`dft` can automatically load table definitions at startup, giving you a persistent "database-like" experience.

crates/datafusion-app/src/flightsql.rs

Lines changed: 139 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ use tokio_stream::StreamExt;
4343
use tonic::{transport::Channel, IntoRequest};
4444

4545
use crate::{
46-
config::FlightSQLConfig, flightsql_benchmarks::FlightSQLBenchmarkStats, ExecOptions, ExecResult,
46+
config::FlightSQLConfig, flightsql_benchmarks::FlightSQLBenchmarkStats,
47+
local_benchmarks::BenchmarkMode, ExecOptions, ExecResult,
4748
};
4849

4950
pub type FlightSQLClient = Arc<Mutex<Option<FlightSqlServiceClient<Channel>>>>;
@@ -120,70 +121,158 @@ impl FlightSQLContext {
120121
&self,
121122
query: &str,
122123
cli_iterations: Option<usize>,
124+
concurrent: bool,
123125
) -> Result<FlightSQLBenchmarkStats> {
124126
let iterations = cli_iterations.unwrap_or(self.config.benchmark_iterations);
127+
let dialect = datafusion::sql::sqlparser::dialect::GenericDialect {};
128+
let statements = DFParser::parse_sql_with_dialect(query, &dialect)?;
129+
130+
if statements.len() != 1 {
131+
return Err(eyre::eyre!("Only a single statement can be benchmarked"));
132+
}
133+
134+
// Check that client exists
135+
{
136+
let guard = self.client.lock().await;
137+
if guard.is_none() {
138+
return Err(eyre::eyre!("No FlightSQL client configured"));
139+
}
140+
}
141+
142+
let concurrency = if concurrent {
143+
std::cmp::min(iterations, num_cpus::get())
144+
} else {
145+
1
146+
};
147+
let mode = if concurrent {
148+
BenchmarkMode::Concurrent(concurrency)
149+
} else {
150+
BenchmarkMode::Serial
151+
};
152+
153+
info!(
154+
"Benchmarking FlightSQL query with {} iterations (concurrency: {})",
155+
iterations, concurrency
156+
);
157+
125158
let mut rows_returned = Vec::with_capacity(iterations);
126159
let mut get_flight_info_durations = Vec::with_capacity(iterations);
127160
let mut ttfb_durations = Vec::with_capacity(iterations);
128161
let mut do_get_durations = Vec::with_capacity(iterations);
129162
let mut total_durations = Vec::with_capacity(iterations);
130163

131-
let dialect = datafusion::sql::sqlparser::dialect::GenericDialect {};
132-
let statements = DFParser::parse_sql_with_dialect(query, &dialect)?;
133-
if statements.len() == 1 {
134-
if let Some(ref mut client) = *self.client.lock().await {
164+
if !concurrent {
165+
// Serial execution
166+
let mut guard = self.client.lock().await;
167+
if let Some(ref mut client) = *guard {
135168
for _ in 0..iterations {
136-
let mut rows = 0;
137-
let start = std::time::Instant::now();
138-
let flight_info = client.execute(query.to_string(), None).await?;
139-
if flight_info.endpoint.len() > 1 {
140-
warn!("More than one endpoint: Benchmark results will not be reliable");
141-
}
142-
let get_flight_info_duration = start.elapsed();
143-
// Current logic wont properly handle having multiple endpoints
144-
for endpoint in flight_info.endpoint {
145-
if let Some(ticket) = &endpoint.ticket {
146-
match client.do_get(ticket.clone().into_request()).await {
147-
Ok(ref mut s) => {
148-
let mut batch_count = 0;
149-
while let Some(b) = s.next().await {
150-
rows += b?.num_rows();
151-
if batch_count == 0 {
152-
let ttfb_duration =
153-
start.elapsed() - get_flight_info_duration;
154-
ttfb_durations.push(ttfb_duration);
155-
}
156-
batch_count += 1;
157-
}
158-
let do_get_duration =
159-
start.elapsed() - get_flight_info_duration;
160-
do_get_durations.push(do_get_duration);
161-
}
162-
Err(e) => {
163-
error!("Error getting Flight stream: {:?}", e);
164-
}
169+
let (rows, gfi_dur, ttfb_dur, dg_dur, total_dur) =
170+
Self::benchmark_single_iteration(client, query).await?;
171+
rows_returned.push(rows);
172+
get_flight_info_durations.push(gfi_dur);
173+
ttfb_durations.push(ttfb_dur);
174+
do_get_durations.push(dg_dur);
175+
total_durations.push(total_dur);
176+
}
177+
}
178+
} else {
179+
// Concurrent execution - spawn tasks that share the client
180+
let mut completed = 0;
181+
182+
while completed < iterations {
183+
let batch_size = std::cmp::min(concurrency, iterations - completed);
184+
let mut join_set = tokio::task::JoinSet::new();
185+
186+
for _ in 0..batch_size {
187+
let client = Arc::clone(&self.client);
188+
let query_str = query.to_string();
189+
190+
join_set.spawn(async move {
191+
let mut guard = client.lock().await;
192+
if let Some(ref mut c) = *guard {
193+
Self::benchmark_single_iteration(c, &query_str).await
194+
} else {
195+
Err(eyre::eyre!("No FlightSQL client configured"))
196+
}
197+
});
198+
}
199+
200+
while let Some(result) = join_set.join_next().await {
201+
let (rows, gfi_dur, ttfb_dur, dg_dur, total_dur) = result??;
202+
rows_returned.push(rows);
203+
get_flight_info_durations.push(gfi_dur);
204+
ttfb_durations.push(ttfb_dur);
205+
do_get_durations.push(dg_dur);
206+
total_durations.push(total_dur);
207+
}
208+
209+
completed += batch_size;
210+
}
211+
}
212+
213+
Ok(FlightSQLBenchmarkStats::new(
214+
query.to_string(),
215+
rows_returned,
216+
mode,
217+
get_flight_info_durations,
218+
ttfb_durations,
219+
do_get_durations,
220+
total_durations,
221+
))
222+
}
223+
224+
async fn benchmark_single_iteration(
225+
client: &mut FlightSqlServiceClient<Channel>,
226+
query: &str,
227+
) -> Result<(
228+
usize,
229+
std::time::Duration,
230+
std::time::Duration,
231+
std::time::Duration,
232+
std::time::Duration,
233+
)> {
234+
let mut rows = 0;
235+
let start = std::time::Instant::now();
236+
let flight_info = client.execute(query.to_string(), None).await?;
237+
238+
if flight_info.endpoint.len() > 1 {
239+
warn!("More than one endpoint: Benchmark results will not be reliable");
240+
}
241+
242+
let get_flight_info_duration = start.elapsed();
243+
let mut ttfb_duration = std::time::Duration::from_secs(0);
244+
let mut do_get_duration = std::time::Duration::from_secs(0);
245+
246+
for endpoint in flight_info.endpoint {
247+
if let Some(ticket) = &endpoint.ticket {
248+
match client.do_get(ticket.clone().into_request()).await {
249+
Ok(ref mut s) => {
250+
let mut batch_count = 0;
251+
while let Some(b) = s.next().await {
252+
rows += b?.num_rows();
253+
if batch_count == 0 {
254+
ttfb_duration = start.elapsed() - get_flight_info_duration;
165255
}
256+
batch_count += 1;
166257
}
258+
do_get_duration = start.elapsed() - get_flight_info_duration;
259+
}
260+
Err(e) => {
261+
error!("Error getting Flight stream: {:?}", e);
262+
return Err(e.into());
167263
}
168-
rows_returned.push(rows);
169-
get_flight_info_durations.push(get_flight_info_duration);
170-
let total_duration = start.elapsed();
171-
total_durations.push(total_duration);
172264
}
173-
} else {
174-
return Err(eyre::eyre!("No FlightSQL client configured"));
175265
}
176-
Ok(FlightSQLBenchmarkStats::new(
177-
query.to_string(),
178-
rows_returned,
179-
get_flight_info_durations,
180-
ttfb_durations,
181-
do_get_durations,
182-
total_durations,
183-
))
184-
} else {
185-
Err(eyre::eyre!("Only a single statement can be benchmarked"))
186266
}
267+
268+
let total_duration = start.elapsed();
269+
Ok((
270+
rows,
271+
get_flight_info_duration,
272+
ttfb_duration,
273+
do_get_duration,
274+
total_duration,
275+
))
187276
}
188277

189278
pub async fn execute_sql_with_opts(

crates/datafusion-app/src/flightsql_benchmarks.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ use std::time::Duration;
1919

2020
use crate::local_benchmarks::is_all_same;
2121

22+
use crate::local_benchmarks::BenchmarkMode;
2223
use crate::local_benchmarks::DurationsSummary;
2324

2425
pub struct FlightSQLBenchmarkStats {
2526
query: String,
2627
runs: usize,
28+
mode: BenchmarkMode,
2729
rows: Vec<usize>,
2830
get_flight_info_durations: Vec<Duration>,
2931
ttfb_durations: Vec<Duration>,
@@ -35,6 +37,7 @@ impl FlightSQLBenchmarkStats {
3537
pub fn new(
3638
query: String,
3739
rows: Vec<usize>,
40+
mode: BenchmarkMode,
3841
get_flight_info_durations: Vec<Duration>,
3942
ttfb_durations: Vec<Duration>,
4043
do_get_durations: Vec<Duration>,
@@ -44,6 +47,7 @@ impl FlightSQLBenchmarkStats {
4447
Self {
4548
query,
4649
runs,
50+
mode,
4751
rows,
4852
get_flight_info_durations,
4953
ttfb_durations,
@@ -103,6 +107,8 @@ impl FlightSQLBenchmarkStats {
103107
csv.push_str(execution_summary.to_csv_fields().as_str());
104108
csv.push(',');
105109
csv.push_str(total_summary.to_csv_fields().as_str());
110+
csv.push(',');
111+
csv.push_str(&self.mode.to_string());
106112
csv
107113
}
108114
}
@@ -111,7 +117,7 @@ impl std::fmt::Display for FlightSQLBenchmarkStats {
111117
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112118
writeln!(f)?;
113119
writeln!(f, "----------------------------")?;
114-
writeln!(f, "Benchmark Stats ({} runs)", self.runs)?;
120+
writeln!(f, "Benchmark Stats ({} runs, {})", self.runs, self.mode)?;
115121
writeln!(f, "----------------------------")?;
116122
writeln!(f, "{}", self.query)?;
117123
writeln!(f, "----------------------------")?;

0 commit comments

Comments
 (0)