Skip to content

Commit f001b22

Browse files
authored
[#10029][benchmarks] arrow-flight roundtrip as well as encode/decode (#10031)
# Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. --> - Contributes towards closing #10029. # Rationale for this change Provides benchmarks for arrow-flight crate. benchmarks for round trip as well as encode/decode individually. <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> # What changes are included in this PR? Adds three criterion benches under arrow-flight/benchmarks/ (roundtrip.rs, flight_encode.rs, flight_decode.rs), each sweeping a tunable matrix of rows, cols, and column types (fixed Int64, variable StringArray, nested List, dict DictionaryArray) built via a shared common::build_batch helper. <!-- There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR. --> # Are these changes tested? n/a <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code If tests are not included in your PR, please explain why (for example, are they covered by existing tests)? If this PR claims a performance improvement, please include evidence such as benchmark results. --> # Are there any user-facing changes? no <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. If there are any breaking changes to public APIs, please call them out. -->
1 parent f03e1bc commit f001b22

4 files changed

Lines changed: 249 additions & 0 deletions

File tree

Cargo.lock

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

arrow-flight/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ cli = ["arrow-array/chrono-tz", "arrow-cast/prettyprint", "tonic/tls-webpki-root
7676
[dev-dependencies]
7777
arrow-cast = { workspace = true, features = ["prettyprint"] }
7878
assert_cmd = "2.0.8"
79+
criterion = { workspace = true, default-features = false, features = ["async_tokio"] }
7980
http = "1.1.0"
8081
http-body = "1.0.0"
8182
hyper-util = "0.1"
@@ -105,3 +106,8 @@ required-features = ["flight-sql", "tls-ring"]
105106
name = "flight_sql_client_cli"
106107
path = "tests/flight_sql_client_cli.rs"
107108
required-features = ["cli", "flight-sql", "tls-ring"]
109+
110+
[[bench]]
111+
name = "flight"
112+
path = "benches/flight.rs"
113+
harness = false

arrow-flight/benches/common/mod.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::sync::{Arc, RwLock};
19+
20+
use arrow_array::{
21+
Array, ArrayRef, DictionaryArray, Int32Array, Int64Array, ListArray, RecordBatch, StringArray,
22+
types::Int32Type,
23+
};
24+
use arrow_buffer::OffsetBuffer;
25+
use arrow_flight::{
26+
Action, ActionType, Criteria, Empty, FlightData, FlightDescriptor, FlightInfo,
27+
HandshakeRequest, HandshakeResponse, PollInfo, PutResult, SchemaResult, Ticket,
28+
flight_service_server::{FlightService, FlightServiceServer},
29+
};
30+
use arrow_schema::{DataType, Field, Schema};
31+
use bytes::Bytes;
32+
use futures::{StreamExt, TryStreamExt, stream::BoxStream};
33+
use hyper_util::rt::TokioIo;
34+
use tonic::{
35+
Request, Response, Status, Streaming,
36+
transport::{Channel, Endpoint, Server},
37+
};
38+
39+
pub type Builder = fn(usize) -> ArrayRef;
40+
41+
pub const TYPES: &[(&str, Builder)] = &[
42+
("fixed", fixed),
43+
("nested", nested),
44+
("variable", variable),
45+
("dict", dict),
46+
];
47+
48+
fn fixed(n: usize) -> ArrayRef {
49+
Arc::new(Int64Array::from_iter_values(0..n as i64))
50+
}
51+
52+
fn variable(n: usize) -> ArrayRef {
53+
Arc::new(StringArray::from_iter_values(
54+
(0..n).map(|i| format!("variable_string_{i}{}", "_".repeat(i % 16))),
55+
))
56+
}
57+
58+
fn nested(n: usize) -> ArrayRef {
59+
let values = Int32Array::from_iter_values(0..(n * 4) as i32);
60+
let offsets = OffsetBuffer::<i32>::from_lengths(std::iter::repeat_n(4usize, n));
61+
let field = Arc::new(Field::new_list_field(DataType::Int32, false));
62+
Arc::new(ListArray::new(field, offsets, Arc::new(values), None))
63+
}
64+
65+
fn dict(n: usize) -> ArrayRef {
66+
let keys = Int32Array::from_iter_values((0..n).map(|i| (i % 32) as i32));
67+
let values = StringArray::from_iter_values((0..32).map(|i| format!("dictionary_value_{i:03}")));
68+
Arc::new(DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap())
69+
}
70+
71+
pub fn build_batch(name: &str, rows: usize, cols: usize, build: Builder) -> RecordBatch {
72+
let arrays: Vec<ArrayRef> = (0..cols).map(|_| build(rows)).collect();
73+
let fields: Vec<Field> = arrays
74+
.iter()
75+
.enumerate()
76+
.map(|(i, a)| Field::new(format!("column_{i}_{name}"), a.data_type().clone(), false))
77+
.collect();
78+
RecordBatch::try_new(Arc::new(Schema::new(fields)), arrays).unwrap()
79+
}
80+
81+
#[derive(Clone, Default)]
82+
pub struct BenchServer {
83+
frames: Arc<RwLock<Vec<FlightData>>>,
84+
}
85+
86+
impl BenchServer {
87+
#[allow(dead_code)]
88+
pub fn set_frames(&self, frames: Vec<FlightData>) {
89+
*self.frames.write().unwrap() = frames;
90+
}
91+
}
92+
93+
fn unimpl<T>() -> Result<T, Status> {
94+
Err(Status::unimplemented(""))
95+
}
96+
97+
#[rustfmt::skip]
98+
#[tonic::async_trait]
99+
impl FlightService for BenchServer {
100+
type HandshakeStream = BoxStream<'static, Result<HandshakeResponse, Status>>;
101+
type ListFlightsStream = BoxStream<'static, Result<FlightInfo, Status>>;
102+
type DoGetStream = BoxStream<'static, Result<FlightData, Status>>;
103+
type DoPutStream = BoxStream<'static, Result<PutResult, Status>>;
104+
type DoActionStream = BoxStream<'static, Result<arrow_flight::Result, Status>>;
105+
type ListActionsStream = BoxStream<'static, Result<ActionType, Status>>;
106+
type DoExchangeStream = BoxStream<'static, Result<FlightData, Status>>;
107+
108+
async fn do_get(&self, _: Request<Ticket>) -> Result<Response<Self::DoGetStream>, Status> {
109+
let frames = self.frames.read().unwrap().clone();
110+
Ok(Response::new(futures::stream::iter(frames.into_iter().map(Ok)).boxed()))
111+
}
112+
113+
async fn do_put(&self, req: Request<Streaming<FlightData>>) -> Result<Response<Self::DoPutStream>, Status> {
114+
let _: Vec<FlightData> = req.into_inner().try_collect().await?;
115+
let ack = PutResult { app_metadata: Bytes::new() };
116+
Ok(Response::new(futures::stream::iter([Ok(ack)]).boxed()))
117+
}
118+
119+
async fn do_exchange(&self, req: Request<Streaming<FlightData>>) -> Result<Response<Self::DoExchangeStream>, Status> {
120+
Ok(Response::new(req.into_inner().boxed()))
121+
}
122+
123+
async fn handshake(&self, _: Request<Streaming<HandshakeRequest>>) -> Result<Response<Self::HandshakeStream>, Status> { unimpl() }
124+
async fn list_flights(&self, _: Request<Criteria>) -> Result<Response<Self::ListFlightsStream>, Status> { unimpl() }
125+
async fn get_flight_info(&self, _: Request<FlightDescriptor>) -> Result<Response<FlightInfo>, Status> { unimpl() }
126+
async fn poll_flight_info(&self, _: Request<FlightDescriptor>) -> Result<Response<PollInfo>, Status> { unimpl() }
127+
async fn get_schema(&self, _: Request<FlightDescriptor>) -> Result<Response<SchemaResult>, Status> { unimpl() }
128+
async fn do_action(&self, _: Request<Action>) -> Result<Response<Self::DoActionStream>, Status> { unimpl() }
129+
async fn list_actions(&self, _: Request<Empty>) -> Result<Response<Self::ListActionsStream>, Status> { unimpl() }
130+
}
131+
#[allow(dead_code)]
132+
pub async fn start_server() -> (Channel, BenchServer) {
133+
const DUMMY_URL: &str = "http://localhost:50051";
134+
135+
let bench_server = BenchServer::default();
136+
137+
let (client, server) = tokio::io::duplex(1024 * 1024);
138+
139+
let mut client = Some(client);
140+
let channel = Endpoint::try_from(DUMMY_URL)
141+
.expect("Invalid dummy URL for building an endpoint. This should never happen")
142+
.connect_with_connector_lazy(tower::service_fn(move |_| {
143+
let client = client
144+
.take()
145+
.expect("Client taken twice. This should never happen");
146+
async move { Ok::<_, std::io::Error>(TokioIo::new(client)) }
147+
}));
148+
tokio::spawn(
149+
Server::builder()
150+
.add_service(FlightServiceServer::new(bench_server.clone()))
151+
.serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))),
152+
);
153+
(channel, bench_server)
154+
}

arrow-flight/benches/flight.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use arrow_array::RecordBatch;
19+
use arrow_flight::{FlightClient, FlightData, encode::FlightDataEncoderBuilder};
20+
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
21+
use futures::TryStreamExt;
22+
use tonic::transport::Channel;
23+
24+
mod common;
25+
use common::{TYPES, build_batch, start_server};
26+
27+
const ROWS: [usize; 2] = [8 * 1024, 64 * 1024];
28+
const COLS: [usize; 2] = [1, 8];
29+
30+
fn bench_encode(c: &mut Criterion) {
31+
let rt = tokio::runtime::Runtime::new().unwrap();
32+
let mut g = c.benchmark_group("encode");
33+
34+
for &(name, build) in TYPES {
35+
for &rows in &ROWS {
36+
for &cols in &COLS {
37+
let batch = build_batch(name, rows, cols, build);
38+
let id = BenchmarkId::new(name, format!("{rows}x{cols}"));
39+
g.throughput(Throughput::Bytes(batch.get_array_memory_size() as u64));
40+
g.bench_with_input(id, &batch, |b, batch| {
41+
b.to_async(&rt).iter(|| async {
42+
let _: Vec<FlightData> = FlightDataEncoderBuilder::new()
43+
.build(futures::stream::iter([Ok(batch.clone())]))
44+
.try_collect()
45+
.await
46+
.unwrap();
47+
});
48+
});
49+
}
50+
}
51+
}
52+
}
53+
54+
async fn roundtrip(channel: Channel, batch: RecordBatch) {
55+
let mut client = FlightClient::new(channel);
56+
let frames = FlightDataEncoderBuilder::new().build(futures::stream::iter([Ok(batch)]));
57+
let _: Vec<RecordBatch> = client
58+
.do_exchange(frames)
59+
.await
60+
.unwrap()
61+
.try_collect()
62+
.await
63+
.unwrap();
64+
}
65+
66+
fn bench_roundtrip(c: &mut Criterion) {
67+
let rt = tokio::runtime::Runtime::new().unwrap();
68+
let (channel, _) = rt.block_on(start_server());
69+
let mut g = c.benchmark_group("roundtrip");
70+
71+
for &(name, build) in TYPES {
72+
for &rows in &ROWS {
73+
for &cols in &COLS {
74+
let batch = build_batch(name, rows, cols, build);
75+
let id = BenchmarkId::new(name, format!("{rows}x{cols}"));
76+
g.throughput(Throughput::Bytes(batch.get_array_memory_size() as u64));
77+
g.bench_with_input(id, &batch, |b, batch| {
78+
b.to_async(&rt)
79+
.iter(|| roundtrip(channel.clone(), batch.clone()));
80+
});
81+
}
82+
}
83+
}
84+
}
85+
86+
criterion_group!(benches, bench_encode, bench_roundtrip);
87+
criterion_main!(benches);

0 commit comments

Comments
 (0)