use arrow::array::{
ArrayRef, Int32Array, Int64Array, ListArray, StringArray, StructArray,
};
use arrow::buffer::OffsetBuffer;
use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaRef};
use arrow::record_batch::RecordBatch;
use criterion::{Criterion, criterion_group, criterion_main};
use datafusion::datasource::listing::{
ListingTable, ListingTableConfig, ListingTableConfigExt,
};
use datafusion::physical_plan::metrics::MetricsSet;
use datafusion::physical_plan::{ExecutionPlan, collect};
use datafusion::prelude::SessionContext;
use datafusion_datasource::ListingTableUrl;
use parquet::arrow::ArrowWriter;
use parquet::file::properties::{WriterProperties, WriterVersion};
use std::hint::black_box;
use std::sync::Arc;
use std::time::Duration;
use tempfile::NamedTempFile;
use tokio::runtime::Runtime;
const NUM_BATCHES: usize = 2;
const ROWS_PER_BATCH: usize = 256;
const ROW_GROUP_ROW_COUNT: usize = 256;
const ELEMS_PER_ROW: usize = 3;
const NUM_PAD_FIELDS: usize = 8;
const PAD_LEN: usize = 2048;
fn narrow_item_fields() -> Fields {
Fields::from(vec![
Field::new("x", DataType::Int64, true),
Field::new("y", DataType::Utf8, true),
])
}
fn wide_item_fields() -> Fields {
let mut fields: Vec<Field> = narrow_item_fields()
.iter()
.map(|f| f.as_ref().clone())
.collect();
for i in 0..NUM_PAD_FIELDS {
fields.push(Field::new(format!("pad_{i}"), DataType::Utf8, false));
}
Fields::from(fields)
}
fn list_schema(item_fields: Fields) -> SchemaRef {
let item = Arc::new(Field::new("item", DataType::Struct(item_fields), true));
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("events", DataType::List(item), true),
]))
}
fn struct_schema(item_fields: Fields) -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("s", DataType::Struct(item_fields), true),
]))
}
fn pad_values(count: usize, seed: usize) -> ArrayRef {
let base = "x".repeat(PAD_LEN);
let values: Vec<String> = (0..count)
.map(|i| format!("{:08}{base}", seed + i))
.collect();
Arc::new(StringArray::from(values))
}
fn item_columns(fields: &Fields, count: usize, seed: usize) -> Vec<ArrayRef> {
fields
.iter()
.enumerate()
.map(|(i, field)| match field.name().as_str() {
"x" => Arc::new(Int64Array::from_iter_values(
(0..count).map(|j| (seed + j) as i64),
)) as ArrayRef,
"y" => Arc::new(StringArray::from_iter_values(
(0..count).map(|j| format!("y-{}", seed + j)),
)) as ArrayRef,
_ => pad_values(count, seed + i),
})
.collect()
}
fn list_batch(fields: &Fields, batch_id: usize) -> RecordBatch {
let num_elems = ROWS_PER_BATCH * ELEMS_PER_ROW;
let seed = batch_id * num_elems;
let struct_array =
StructArray::new(fields.clone(), item_columns(fields, num_elems, seed), None);
let item = Arc::new(Field::new("item", DataType::Struct(fields.clone()), true));
let events = ListArray::new(
item,
OffsetBuffer::from_lengths(std::iter::repeat_n(ELEMS_PER_ROW, ROWS_PER_BATCH)),
Arc::new(struct_array),
None,
);
let ids = Int32Array::from_iter_values(
(0..ROWS_PER_BATCH).map(|i| (batch_id * ROWS_PER_BATCH + i) as i32),
);
RecordBatch::try_new(
list_schema(fields.clone()),
vec![Arc::new(ids), Arc::new(events)],
)
.unwrap()
}
fn struct_batch(fields: &Fields, batch_id: usize) -> RecordBatch {
let seed = batch_id * ROWS_PER_BATCH;
let struct_array = StructArray::new(
fields.clone(),
item_columns(fields, ROWS_PER_BATCH, seed),
None,
);
let ids =
Int32Array::from_iter_values((0..ROWS_PER_BATCH).map(|i| (seed + i) as i32));
RecordBatch::try_new(
struct_schema(fields.clone()),
vec![Arc::new(ids), Arc::new(struct_array)],
)
.unwrap()
}
fn generate_file(
schema: SchemaRef,
batch_fn: impl Fn(usize) -> RecordBatch,
prefix: &str,
) -> NamedTempFile {
let mut named_file = tempfile::Builder::new()
.prefix(prefix)
.suffix(".parquet")
.tempfile()
.unwrap();
let properties = WriterProperties::builder()
.set_writer_version(WriterVersion::PARQUET_2_0)
.set_dictionary_enabled(false)
.set_max_row_group_row_count(Some(ROW_GROUP_ROW_COUNT))
.build();
let mut writer =
ArrowWriter::try_new(&mut named_file, schema, Some(properties)).unwrap();
for batch_id in 0..NUM_BATCHES {
writer.write(&batch_fn(batch_id)).unwrap();
}
let metadata = writer.close().unwrap();
println!(
"Generated {} ({} rows, {} row groups, {} bytes)",
named_file.path().display(),
metadata.file_metadata().num_rows(),
metadata.row_groups().len(),
std::fs::metadata(named_file.path()).unwrap().len(),
);
named_file
}
fn register_table(
ctx: &SessionContext,
rt: &Runtime,
table: &str,
path: &str,
table_schema: SchemaRef,
) {
let url = ListingTableUrl::parse(path).unwrap();
let config = rt
.block_on(ListingTableConfig::new(url).infer_options(&ctx.state()))
.unwrap()
.with_schema(table_schema);
let provider = ListingTable::try_new(config).unwrap();
ctx.register_table(table, Arc::new(provider)).unwrap();
}
fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) {
let df = rt.block_on(ctx.sql(sql)).unwrap();
black_box(rt.block_on(df.collect()).unwrap());
}
fn gather_metrics(plan: &Arc<dyn ExecutionPlan>, out: &mut MetricsSet) {
if let Some(metrics) = plan.metrics() {
for metric in metrics.iter() {
out.push(Arc::clone(metric));
}
}
for child in plan.children() {
gather_metrics(child, out);
}
}
fn scan_bytes(ctx: &SessionContext, rt: &Runtime, sql: &str) -> usize {
let df = rt.block_on(ctx.sql(sql)).unwrap();
let plan = rt.block_on(df.create_physical_plan()).unwrap();
black_box(
rt.block_on(collect(Arc::clone(&plan), ctx.task_ctx()))
.unwrap(),
);
let mut metrics = MetricsSet::new();
gather_metrics(&plan, &mut metrics);
metrics
.aggregate_by_name()
.sum_by_name("bytes_scanned")
.map(|v| v.as_usize())
.expect("parquet scan should report a bytes_scanned metric")
}
fn assert_scan_prunes(
ctx: &SessionContext,
rt: &Runtime,
label: &str,
narrow_sql: &str,
full_sql: &str,
floor_sql: &str,
) {
let narrow = scan_bytes(ctx, rt, narrow_sql);
let full = scan_bytes(ctx, rt, full_sql);
let floor = scan_bytes(ctx, rt, floor_sql);
println!(
"{label}: bytes_scanned narrow_schema={narrow} full_schema={full} \
physically_narrow={floor}"
);
assert!(
narrow * 2 < full,
"{label}: expected the narrow declared schema to read less than half \
of the full schema's {full} bytes (physically-narrow floor is \
{floor} bytes), but it read {narrow}"
);
}
struct Fixture {
ctx: SessionContext,
rt: Runtime,
_files: Vec<NamedTempFile>,
}
fn setup(
name: &str,
schema_fn: fn(Fields) -> SchemaRef,
batch_fn: fn(&Fields, usize) -> RecordBatch,
) -> Fixture {
let rt = Runtime::new().unwrap();
let ctx = SessionContext::new();
let wide = wide_item_fields();
let narrow = narrow_item_fields();
let wide_file = generate_file(schema_fn(wide.clone()), |i| batch_fn(&wide, i), name);
let narrow_file = generate_file(
schema_fn(narrow.clone()),
|i| batch_fn(&narrow, i),
&format!("{name}_narrow"),
);
let wide_path = wide_file.path().display().to_string();
let narrow_path = narrow_file.path().display().to_string();
register_table(
&ctx,
&rt,
&format!("{name}_narrow_schema"),
&wide_path,
schema_fn(narrow.clone()),
);
register_table(
&ctx,
&rt,
&format!("{name}_full_schema"),
&wide_path,
schema_fn(wide.clone()),
);
register_table(
&ctx,
&rt,
&format!("{name}_physically_narrow"),
&narrow_path,
schema_fn(narrow.clone()),
);
Fixture {
ctx,
rt,
_files: vec![wide_file, narrow_file],
}
}
fn list_struct_benchmarks(c: &mut Criterion) {
let f = setup("list_struct", list_schema, list_batch);
let (ctx, rt) = (&f.ctx, &f.rt);
assert_scan_prunes(
ctx,
rt,
"list_struct",
"SELECT events FROM list_struct_narrow_schema",
"SELECT events FROM list_struct_full_schema",
"SELECT events FROM list_struct_physically_narrow",
);
let mut group = c.benchmark_group("list_struct");
group.sample_size(10);
group.warm_up_time(Duration::from_secs(1));
group.measurement_time(Duration::from_secs(3));
group.bench_function("select_events_narrow_schema", |b| {
b.iter(|| query(ctx, rt, "SELECT events FROM list_struct_narrow_schema"))
});
group.bench_function("select_events_full_schema", |b| {
b.iter(|| query(ctx, rt, "SELECT events FROM list_struct_full_schema"))
});
group.bench_function("select_events_physically_narrow", |b| {
b.iter(|| query(ctx, rt, "SELECT events FROM list_struct_physically_narrow"))
});
group.bench_function("sum_x_narrow_schema", |b| {
b.iter(|| {
query(
ctx,
rt,
"SELECT SUM(e['x']) FROM (SELECT UNNEST(events) AS e FROM list_struct_narrow_schema)",
)
})
});
group.finish();
}
fn top_level_struct_benchmarks(c: &mut Criterion) {
let f = setup("struct", struct_schema, struct_batch);
let (ctx, rt) = (&f.ctx, &f.rt);
assert_scan_prunes(
ctx,
rt,
"top_level_struct",
"SELECT s FROM struct_narrow_schema",
"SELECT s FROM struct_full_schema",
"SELECT s FROM struct_physically_narrow",
);
let mut group = c.benchmark_group("top_level_struct");
group.sample_size(10);
group.warm_up_time(Duration::from_secs(1));
group.measurement_time(Duration::from_secs(3));
group.bench_function("select_struct_narrow_schema", |b| {
b.iter(|| query(ctx, rt, "SELECT s FROM struct_narrow_schema"))
});
group.bench_function("select_struct_full_schema", |b| {
b.iter(|| query(ctx, rt, "SELECT s FROM struct_full_schema"))
});
group.bench_function("select_struct_physically_narrow", |b| {
b.iter(|| query(ctx, rt, "SELECT s FROM struct_physically_narrow"))
});
group.bench_function("sum_x_narrow_schema", |b| {
b.iter(|| query(ctx, rt, "SELECT SUM(s['x']) FROM struct_narrow_schema"))
});
group.finish();
}
criterion_group!(benches, list_struct_benchmarks, top_level_struct_benchmarks);
criterion_main!(benches);