From ec996e7c490b260bb027efef54f206a8688b99ba Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:24:59 -0400 Subject: [PATCH 1/7] Make ObjectBuilder faster --- parquet-variant/src/builder.rs | 117 ++++++++------------------------- 1 file changed, 29 insertions(+), 88 deletions(-) diff --git a/parquet-variant/src/builder.rs b/parquet-variant/src/builder.rs index fda15c2b4336..a5daee1bb537 100644 --- a/parquet-variant/src/builder.rs +++ b/parquet-variant/src/builder.rs @@ -16,7 +16,7 @@ // under the License. use crate::decoder::{VariantBasicType, VariantPrimitiveType}; use crate::{ShortString, Variant, VariantDecimal16, VariantDecimal4, VariantDecimal8}; -use std::collections::BTreeMap; +use std::collections::HashMap; const BASIC_TYPE_BITS: u8 = 2; const UNIX_EPOCH_DATE: chrono::NaiveDate = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); @@ -233,14 +233,14 @@ impl ValueBuffer { #[derive(Default)] struct MetadataBuilder { - field_name_to_id: BTreeMap, + field_name_to_id: HashMap, field_names: Vec, } impl MetadataBuilder { /// Upsert field name to dictionary, return its ID fn upsert_field_name(&mut self, field_name: &str) -> u32 { - use std::collections::btree_map::Entry; + use std::collections::hash_map::Entry; match self.field_name_to_id.entry(field_name.to_string()) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { @@ -256,6 +256,10 @@ impl MetadataBuilder { self.field_names.len() } + fn field_name(&self, i: usize) -> &str { + &self.field_names[i] + } + fn metadata_size(&self) -> usize { self.field_names.iter().map(|k| k.len()).sum() } @@ -567,7 +571,7 @@ impl<'a> ListBuilder<'a> { pub struct ObjectBuilder<'a, 'b> { parent_buffer: &'a mut ValueBuffer, metadata_builder: &'a mut MetadataBuilder, - fields: BTreeMap, // (field_id, offset) + fields: Vec<(u32, usize)>, // (field_id, offset) buffer: ValueBuffer, /// Is there a pending list or object that needs to be finalized? pending: Option<(&'b str, usize)>, @@ -578,19 +582,27 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { Self { parent_buffer, metadata_builder, - fields: BTreeMap::new(), + fields: Vec::new(), buffer: ValueBuffer::default(), pending: None, } } + fn upsert_field(&mut self, field_id: u32, field_start: usize) { + if let Ok(i) = self.fields.binary_search_by(|(id, _)| id.cmp(&field_id)) { + self.fields[i] = (field_id, field_start); + } else { + self.fields.push((field_id, field_start)); + } + } + fn check_pending_field(&mut self) { let Some((field_name, field_start)) = self.pending.as_ref() else { return; }; let field_id = self.metadata_builder.upsert_field_name(field_name); - self.fields.insert(field_id, *field_start); + self.upsert_field(field_id, *field_start); self.pending = None; } @@ -605,7 +617,7 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { let field_id = self.metadata_builder.upsert_field_name(key); let field_start = self.buffer.offset(); - self.fields.insert(field_id, field_start); + self.upsert_field(field_id, field_start); self.buffer.append_non_nested_value(value); } @@ -643,16 +655,15 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { let num_fields = self.fields.len(); let is_large = num_fields > u8::MAX as usize; - let field_ids_by_sorted_field_name = self - .metadata_builder - .field_name_to_id - .iter() - .filter_map(|(_, id)| self.fields.contains_key(id).then_some(*id)) - .collect::>(); + self.fields.sort_by(|a, b| { + let key_a = &self.metadata_builder.field_name(a.0 as usize); + let key_b = &self.metadata_builder.field_name(b.0 as usize); + key_a.cmp(key_b) + }); - let max_id = self.fields.keys().last().copied().unwrap_or(0) as usize; + let max_id = self.fields.iter().map(|&(id, _)| id).max().unwrap_or(0); - let id_size = int_size(max_id); + let id_size = int_size(max_id as usize); let offset_size = int_size(data_size); // Write header @@ -664,13 +675,12 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { ); // Write field IDs (sorted order) - for id in &field_ids_by_sorted_field_name { - write_offset(self.parent_buffer.inner_mut(), *id as usize, id_size); + for &(id, _) in &self.fields { + write_offset(self.parent_buffer.inner_mut(), id as usize, id_size); } // Write field offsets - for id in &field_ids_by_sorted_field_name { - let &offset = self.fields.get(id).unwrap(); + for &(_, offset) in &self.fields { write_offset(self.parent_buffer.inner_mut(), offset, offset_size); } @@ -861,75 +871,6 @@ mod tests { assert_eq!(field_ids, vec![1, 2, 0]); } - #[test] - fn test_object_and_metadata_ordering() { - let mut builder = VariantBuilder::new(); - - let mut obj = builder.new_object(); - - obj.insert("zebra", "stripes"); // ID = 0 - obj.insert("apple", "red"); // ID = 1 - - { - // fields_map is ordered by insertion order (field id) - let fields_map = obj.fields.keys().copied().collect::>(); - assert_eq!(fields_map, vec![0, 1]); - - // dict is ordered by field names - let dict_metadata = obj - .metadata_builder - .field_name_to_id - .iter() - .map(|(f, i)| (f.as_str(), *i)) - .collect::>(); - - assert_eq!(dict_metadata, vec![("apple", 1), ("zebra", 0)]); - - // dict_keys is ordered by insertion order (field id) - let dict_keys = obj - .metadata_builder - .field_names - .iter() - .map(|k| k.as_str()) - .collect::>(); - assert_eq!(dict_keys, vec!["zebra", "apple"]); - } - - obj.insert("banana", "yellow"); // ID = 2 - - { - // fields_map is ordered by insertion order (field id) - let fields_map = obj.fields.keys().copied().collect::>(); - assert_eq!(fields_map, vec![0, 1, 2]); - - // dict is ordered by field names - let dict_metadata = obj - .metadata_builder - .field_name_to_id - .iter() - .map(|(f, i)| (f.as_str(), *i)) - .collect::>(); - - assert_eq!( - dict_metadata, - vec![("apple", 1), ("banana", 2), ("zebra", 0)] - ); - - // dict_keys is ordered by insertion order (field id) - let dict_keys = obj - .metadata_builder - .field_names - .iter() - .map(|k| k.as_str()) - .collect::>(); - assert_eq!(dict_keys, vec!["zebra", "apple", "banana"]); - } - - obj.finish(); - - builder.finish(); - } - #[test] fn test_duplicate_fields_in_object() { let mut builder = VariantBuilder::new(); From efe8d13cb171ab2934bc7d25634a343c8ae0d0ff Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Fri, 27 Jun 2025 22:47:52 -0400 Subject: [PATCH 2/7] Do linear scan when upserting field in ObjectBuilder --- parquet-variant/src/builder.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/parquet-variant/src/builder.rs b/parquet-variant/src/builder.rs index a5daee1bb537..6d9cd143d657 100644 --- a/parquet-variant/src/builder.rs +++ b/parquet-variant/src/builder.rs @@ -589,10 +589,9 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { } fn upsert_field(&mut self, field_id: u32, field_start: usize) { - if let Ok(i) = self.fields.binary_search_by(|(id, _)| id.cmp(&field_id)) { - self.fields[i] = (field_id, field_start); - } else { - self.fields.push((field_id, field_start)); + match self.fields.iter().position(|&(id, _)| id == field_id) { + Some(i) => self.fields[i] = (field_id, field_start), + None => self.fields.push((field_id, field_start)), } } @@ -1183,8 +1182,10 @@ mod tests { /* { "c": { + "b": false, "c": "a" - } + }, + "b": false, } */ @@ -1194,10 +1195,17 @@ mod tests { let mut outer_object_builder = builder.new_object(); { let mut inner_object_builder = outer_object_builder.new_object("c"); + inner_object_builder.insert("b", false); inner_object_builder.insert("c", "a"); + inner_object_builder.finish(); } + outer_object_builder.insert("b", false); + + // note, we can't guarantee an Objects field is sorted by field id. + assert_eq!(outer_object_builder.fields, vec![(1, 0), (0, 10)]); + outer_object_builder.finish(); } @@ -1205,15 +1213,17 @@ mod tests { let variant = Variant::try_new(&metadata, &value).unwrap(); let outer_object = variant.as_object().unwrap(); - assert_eq!(outer_object.len(), 1); - assert_eq!(outer_object.field_name(0).unwrap(), "c"); + assert_eq!(outer_object.len(), 2); + assert_eq!(outer_object.field_name(0).unwrap(), "b"); - let inner_object_variant = outer_object.field(0).unwrap(); + let inner_object_variant = outer_object.field(1).unwrap(); let inner_object = inner_object_variant.as_object().unwrap(); - assert_eq!(inner_object.len(), 1); - assert_eq!(inner_object.field_name(0).unwrap(), "c"); - assert_eq!(inner_object.field(0).unwrap(), Variant::from("a")); + assert_eq!(inner_object.len(), 2); + assert_eq!(inner_object.field_name(0).unwrap(), "b"); + assert_eq!(inner_object.field(0).unwrap(), Variant::from(false)); + assert_eq!(inner_object.field_name(1).unwrap(), "c"); + assert_eq!(inner_object.field(1).unwrap(), Variant::from("a")); } #[test] From 4384c62a1ee32c2ce172b00c523bd7c4d05e49db Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Sat, 28 Jun 2025 09:20:00 -0400 Subject: [PATCH 3/7] Add benches --- parquet-variant/Cargo.toml | 13 +++ parquet-variant/benches/builder.rs | 177 +++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 parquet-variant/benches/builder.rs diff --git a/parquet-variant/Cargo.toml b/parquet-variant/Cargo.toml index 838ca7de8885..cd5999d923e5 100644 --- a/parquet-variant/Cargo.toml +++ b/parquet-variant/Cargo.toml @@ -38,4 +38,17 @@ chrono = { workspace = true } serde_json = "1.0" base64 = "0.22" +[dev-dependencies] +criterion = { version = "0.6", default-features = false } +rand = { version = "0.9", default-features = false, features = [ + "std", + "std_rng", + "thread_rng", +] } + [lib] + + +[[bench]] +name = "builder" +harness = false diff --git a/parquet-variant/benches/builder.rs b/parquet-variant/benches/builder.rs new file mode 100644 index 000000000000..91903c895c66 --- /dev/null +++ b/parquet-variant/benches/builder.rs @@ -0,0 +1,177 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +extern crate parquet_variant; + +use criterion::*; + +use parquet_variant::VariantBuilder; +use rand::{ + distr::{uniform::SampleUniform, Alphanumeric}, + rngs::ThreadRng, + Rng, +}; +use std::{hint, ops::Range}; + +fn random(rng: &mut ThreadRng, range: Range) -> T { + rng.random_range::(range) +} + +// generates a string with a 50/50 chance whether it's a short or a long string +fn random_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(1..128); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +// generates a string guaranteed to be longer than 64 bytes +fn random_long_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(65..200); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +// Creates an object with field names inserted in reverse lexicographical order +fn bench_object_field_names_reverse_order(c: &mut Criterion) { + c.bench_function("bench_object_field_names_reverse_order", |b| { + b.iter(|| { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + let mut object_builder = variant.new_object(); + + for i in 0..1000 { + object_builder.insert( + format!("{}", 1000 - i).as_str(), + random_string(&mut rng).as_str(), + ); + } + + object_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +// Creates a list of objects with the same schema (same field names) +/* + { + name: String, + age: i32, + likes_cilantro: bool, + comments: Long string + dishes: Vec + } +*/ +fn bench_object_list_same_schemas(c: &mut Criterion) { + c.bench_function("bench_object_list_same_schema", |b| { + b.iter(|| { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..100 { + let mut object_builder = list_builder.new_object(); + object_builder.insert("name", random_string(&mut rng).as_str()); + object_builder.insert("age", random::(&mut rng, 18..100) as i32); + object_builder.insert("likes_cilantro", rng.random_bool(0.5)); + object_builder.insert("comments", random_long_string(&mut rng).as_str()); + + let mut list_builder = object_builder.new_list("dishes"); + list_builder.append_value(random_string(&mut rng).as_str()); + list_builder.append_value(random_string(&mut rng).as_str()); + list_builder.append_value(random_string(&mut rng).as_str()); + + list_builder.finish(); + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +// Creates a list of variant objects with an undefined schema (random field names) +// values are randomly generated, with an equal distribution to whether it's a String, Object, or List +fn bench_object_list_unknown_schema(c: &mut Criterion) { + c.bench_function("bench_object_list_unknown_schema", |b| { + b.iter(|| { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..200 { + let mut object_builder = list_builder.new_object(); + + for _num_fields in 0..random::(&mut rng, 0..100) { + if rng.random_bool(0.33) { + object_builder.insert( + random_string(&mut rng).as_str(), + random_string(&mut rng).as_str(), + ); + continue; + } + + if rng.random_bool(0.33) { + let mut inner_object_builder = object_builder.new_object("rand_object"); + + for _num_fields in 0..random::(&mut rng, 0..25) { + inner_object_builder.insert( + random_string(&mut rng).as_str(), + random_string(&mut rng).as_str(), + ); + } + inner_object_builder.finish(); + + continue; + } + + let mut inner_list_builder = object_builder.new_list("rand_list"); + + for _num_elements in 0..random::(&mut rng, 0..25) { + inner_list_builder.append_value(random_string(&mut rng).as_str()); + } + + inner_list_builder.finish(); + } + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +criterion_group!( + benches, + bench_object_field_names_reverse_order, + bench_object_list_same_schemas, + bench_object_list_unknown_schema, +); +criterion_main!(benches); From d57050d9a88664ed3b19da540186a555fd48defe Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:17:11 -0400 Subject: [PATCH 4/7] Commit perf logs --- parquet-variant/Cargo.toml | 14 +++- .../perf/object_list_same_schemas.rs | 59 +++++++++++++++ .../perf/object_list_unknown_schemas.rs | 70 ++++++++++++++++++ profile.json.gz | Bin 0 -> 6628 bytes 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 parquet-variant/perf/object_list_same_schemas.rs create mode 100644 parquet-variant/perf/object_list_unknown_schemas.rs create mode 100644 profile.json.gz diff --git a/parquet-variant/Cargo.toml b/parquet-variant/Cargo.toml index cd5999d923e5..954bcb24acde 100644 --- a/parquet-variant/Cargo.toml +++ b/parquet-variant/Cargo.toml @@ -37,17 +37,25 @@ arrow-schema = { workspace = true } chrono = { workspace = true } serde_json = "1.0" base64 = "0.22" - -[dev-dependencies] -criterion = { version = "0.6", default-features = false } rand = { version = "0.9", default-features = false, features = [ "std", "std_rng", "thread_rng", ] } +[dev-dependencies] +criterion = { version = "0.6", default-features = false } + + [lib] +[[bin]] +name = "object_list_same_schemas" +path = "./perf/object_list_same_schemas.rs" + +[[bin]] +name = "object_list_unknown_schemas" +path = "./perf/object_list_unknown_schemas.rs" [[bench]] name = "builder" diff --git a/parquet-variant/perf/object_list_same_schemas.rs b/parquet-variant/perf/object_list_same_schemas.rs new file mode 100644 index 000000000000..449a3d253340 --- /dev/null +++ b/parquet-variant/perf/object_list_same_schemas.rs @@ -0,0 +1,59 @@ +use std::{hint, ops::Range}; + +use parquet_variant::VariantBuilder; +use rand::{ + distr::{uniform::SampleUniform, Alphanumeric}, + rngs::ThreadRng, + Rng, +}; + +fn random(rng: &mut ThreadRng, range: Range) -> T { + rng.random_range::(range) +} + +// generates a string with a 50/50 chance whether it's a short or a long string +fn random_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(1..128); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +// generates a string guaranteed to be longer than 64 bytes +fn random_long_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(65..200); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +fn main() { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..10_000 { + let mut object_builder = list_builder.new_object(); + object_builder.insert("name", random_string(&mut rng).as_str()); + object_builder.insert("age", random::(&mut rng, 18..100) as i32); + object_builder.insert("likes_cilantro", rng.random_bool(0.5)); + object_builder.insert("comments", random_long_string(&mut rng).as_str()); + + let mut list_builder = object_builder.new_list("dishes"); + list_builder.append_value(random_string(&mut rng).as_str()); + list_builder.append_value(random_string(&mut rng).as_str()); + list_builder.append_value(random_string(&mut rng).as_str()); + + list_builder.finish(); + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); +} diff --git a/parquet-variant/perf/object_list_unknown_schemas.rs b/parquet-variant/perf/object_list_unknown_schemas.rs new file mode 100644 index 000000000000..eca17a4eb2c9 --- /dev/null +++ b/parquet-variant/perf/object_list_unknown_schemas.rs @@ -0,0 +1,70 @@ +use std::{hint, ops::Range}; + +use parquet_variant::VariantBuilder; +use rand::{ + distr::{uniform::SampleUniform, Alphanumeric}, + rngs::ThreadRng, + Rng, +}; + +fn random(rng: &mut ThreadRng, range: Range) -> T { + rng.random_range::(range) +} + +// generates a string with a 50/50 chance whether it's a short or a long string +fn random_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(1..128); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +fn main() { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..1_000 { + let mut object_builder = list_builder.new_object(); + + for _num_fields in 0..random::(&mut rng, 0..100) { + if rng.random_bool(0.33) { + object_builder.insert( + random_string(&mut rng).as_str(), + random_string(&mut rng).as_str(), + ); + continue; + } + + if rng.random_bool(0.33) { + let mut inner_object_builder = object_builder.new_object("rand_object"); + + for _num_fields in 0..random::(&mut rng, 0..25) { + inner_object_builder.insert( + random_string(&mut rng).as_str(), + random_string(&mut rng).as_str(), + ); + } + inner_object_builder.finish(); + + continue; + } + + let mut inner_list_builder = object_builder.new_list("rand_list"); + + for _num_elements in 0..random::(&mut rng, 0..25) { + inner_list_builder.append_value(random_string(&mut rng).as_str()); + } + + inner_list_builder.finish(); + } + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); +} diff --git a/profile.json.gz b/profile.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c58c4b1553e89af66654fc4ebcf3b89a76a6b471 GIT binary patch literal 6628 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 literal 0 HcmV?d00001 From 7a7cff0865db6faf6f8dac675f35e2806573d9c4 Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Sat, 28 Jun 2025 11:42:31 -0400 Subject: [PATCH 5/7] Tune --- parquet-variant/benches/builder.rs | 6 +++--- .../perf/object_list_same_schemas.rs | 2 +- profile.json.gz | Bin 6628 -> 3951 bytes 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/parquet-variant/benches/builder.rs b/parquet-variant/benches/builder.rs index 91903c895c66..afa2896ed0c5 100644 --- a/parquet-variant/benches/builder.rs +++ b/parquet-variant/benches/builder.rs @@ -60,9 +60,9 @@ fn bench_object_field_names_reverse_order(c: &mut Criterion) { let mut variant = VariantBuilder::new(); let mut object_builder = variant.new_object(); - for i in 0..1000 { + for i in 0..10_000 { object_builder.insert( - format!("{}", 1000 - i).as_str(), + format!("{}", 10_000 - i).as_str(), random_string(&mut rng).as_str(), ); } @@ -92,7 +92,7 @@ fn bench_object_list_same_schemas(c: &mut Criterion) { let mut list_builder = variant.new_list(); - for _ in 0..100 { + for _ in 0..200 { let mut object_builder = list_builder.new_object(); object_builder.insert("name", random_string(&mut rng).as_str()); object_builder.insert("age", random::(&mut rng, 18..100) as i32); diff --git a/parquet-variant/perf/object_list_same_schemas.rs b/parquet-variant/perf/object_list_same_schemas.rs index 449a3d253340..1224d8e5c083 100644 --- a/parquet-variant/perf/object_list_same_schemas.rs +++ b/parquet-variant/perf/object_list_same_schemas.rs @@ -38,7 +38,7 @@ fn main() { let mut list_builder = variant.new_list(); - for _ in 0..10_000 { + for _ in 0..25_000 { let mut object_builder = list_builder.new_object(); object_builder.insert("name", random_string(&mut rng).as_str()); object_builder.insert("age", random::(&mut rng, 18..100) as i32); diff --git a/profile.json.gz b/profile.json.gz index c58c4b1553e89af66654fc4ebcf3b89a76a6b471..4d71f7906864ac5acef7a2cf9468535eca10b121 100644 GIT binary patch literal 3951 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 From 0c1464451a8af77513c2c07c054b84e52684173f Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Sat, 28 Jun 2025 17:43:28 -0400 Subject: [PATCH 6/7] ibid --- parquet-variant/src/builder.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/parquet-variant/src/builder.rs b/parquet-variant/src/builder.rs index 6d9cd143d657..74e4b5492bce 100644 --- a/parquet-variant/src/builder.rs +++ b/parquet-variant/src/builder.rs @@ -571,7 +571,8 @@ impl<'a> ListBuilder<'a> { pub struct ObjectBuilder<'a, 'b> { parent_buffer: &'a mut ValueBuffer, metadata_builder: &'a mut MetadataBuilder, - fields: Vec<(u32, usize)>, // (field_id, offset) + fields: Vec<(u32, usize)>, // (field_id, offset) + field_id_to_index: HashMap, // (field_id, index to `fields`) buffer: ValueBuffer, /// Is there a pending list or object that needs to be finalized? pending: Option<(&'b str, usize)>, @@ -583,15 +584,24 @@ impl<'a, 'b> ObjectBuilder<'a, 'b> { parent_buffer, metadata_builder, fields: Vec::new(), + field_id_to_index: HashMap::new(), buffer: ValueBuffer::default(), pending: None, } } fn upsert_field(&mut self, field_id: u32, field_start: usize) { - match self.fields.iter().position(|&(id, _)| id == field_id) { - Some(i) => self.fields[i] = (field_id, field_start), - None => self.fields.push((field_id, field_start)), + use std::collections::hash_map::Entry; + + match self.field_id_to_index.entry(field_id) { + Entry::Occupied(occupied_entry) => { + let i = *occupied_entry.get(); + self.fields[i] = (field_id, field_start); + } + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(self.fields.len()); + self.fields.push((field_id, field_start)); + } } } From 7ec3bedeaf9e3e832a48ae0c45cc054a0521645d Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Sun, 29 Jun 2025 09:33:24 -0400 Subject: [PATCH 7/7] Update --- parquet-variant/benches/variant_builder.rs | 401 +++++++++++++++++++++ profile.json.gz | Bin 3951 -> 3802 bytes 2 files changed, 401 insertions(+) create mode 100644 parquet-variant/benches/variant_builder.rs diff --git a/parquet-variant/benches/variant_builder.rs b/parquet-variant/benches/variant_builder.rs new file mode 100644 index 000000000000..65eede3baf9e --- /dev/null +++ b/parquet-variant/benches/variant_builder.rs @@ -0,0 +1,401 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +extern crate parquet_variant; + +use criterion::*; + +use parquet_variant::VariantBuilder; +use rand::{ + distr::{uniform::SampleUniform, Alphanumeric}, + rngs::ThreadRng, + Rng, +}; +use std::{hint, ops::Range}; + +fn random(rng: &mut ThreadRng, range: Range) -> T { + rng.random_range::(range) +} + +// generates a string with a 50/50 chance whether it's a short or a long string +fn random_string(rng: &mut ThreadRng) -> String { + let len = rng.random_range::(1..128); + + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +struct RandomStringGenerator { + cursor: usize, + table: Vec, +} + +impl RandomStringGenerator { + pub fn new(rng: &mut ThreadRng, capacity: usize) -> Self { + Self { + cursor: 0, + table: vec![random_string(rng); capacity], + } + } + + pub fn next(&mut self) -> &str { + let this = &self.table[self.cursor]; + + self.cursor = (self.cursor + 1) % self.table.len(); + + this + } +} + +// Creates an object with field names inserted in reverse lexicographical order +fn bench_object_field_names_reverse_order(c: &mut Criterion) { + c.bench_function("bench_object_field_names_reverse_order", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 117); + b.iter(|| { + let mut variant = VariantBuilder::new(); + let mut object_builder = variant.new_object(); + + for i in 0..50_000 { + object_builder.insert(format!("{}", 1000 - i).as_str(), string_table.next()); + } + + object_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +// Creates objects with a homogenous schema (same field names) +/* + { + name: String, + age: i32, + likes_cilantro: bool, + comments: Long string + dishes: Vec + } +*/ +fn bench_object_same_schema(c: &mut Criterion) { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 117); + + c.bench_function("bench_object_same_schema", |b| { + b.iter(|| { + for _ in 0..25_000 { + let mut variant = VariantBuilder::new(); + let mut object_builder = variant.new_object(); + object_builder.insert("name", string_table.next()); + object_builder.insert("age", random::(&mut rng, 18..100) as i32); + object_builder.insert("likes_cilantro", rng.random_bool(0.5)); + object_builder.insert("comments", string_table.next()); + + let mut inner_list_builder = object_builder.new_list("dishes"); + inner_list_builder.append_value(string_table.next()); + inner_list_builder.append_value(string_table.next()); + inner_list_builder.append_value(string_table.next()); + + inner_list_builder.finish(); + object_builder.finish(); + + hint::black_box(variant.finish()); + } + }) + }); +} + +// Creates a list of objects with the same schema (same field names) +/* + { + name: String, + age: i32, + likes_cilantro: bool, + comments: Long string + dishes: Vec + } +*/ +fn bench_object_list_same_schema(c: &mut Criterion) { + c.bench_function("bench_object_list_same_schema", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 101); + + b.iter(|| { + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..25_000 { + let mut object_builder = list_builder.new_object(); + object_builder.insert("name", string_table.next()); + object_builder.insert("age", random::(&mut rng, 18..100) as i32); + object_builder.insert("likes_cilantro", rng.random_bool(0.5)); + object_builder.insert("comments", string_table.next()); + + let mut list_builder = object_builder.new_list("dishes"); + list_builder.append_value(string_table.next()); + list_builder.append_value(string_table.next()); + list_builder.append_value(string_table.next()); + + list_builder.finish(); + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +// Creates variant objects with an undefined schema (random field names) +// values are randomly generated, with an equal distribution to whether it's a String, Object, or List +fn bench_object_unknown_schema(c: &mut Criterion) { + c.bench_function("bench_object_unknown_schema", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 1001); + + b.iter(|| { + for _ in 0..200 { + let mut variant = VariantBuilder::new(); + let mut object_builder = variant.new_object(); + + for _num_fields in 0..random::(&mut rng, 0..100) { + if rng.random_bool(0.33) { + let key = string_table.next(); + object_builder.insert(key, key); + continue; + } + + if rng.random_bool(0.5) { + let mut inner_object_builder = object_builder.new_object("rand_object"); + + for _num_fields in 0..random::(&mut rng, 0..25) { + let key = string_table.next(); + inner_object_builder.insert(key, key); + } + inner_object_builder.finish(); + + continue; + } + + let mut inner_list_builder = object_builder.new_list("rand_list"); + + for _num_elements in 0..random::(&mut rng, 0..25) { + inner_list_builder.append_value(string_table.next()); + } + + inner_list_builder.finish(); + } + object_builder.finish(); + hint::black_box(variant.finish()); + } + }) + }); +} + +// Creates a list of variant objects with an undefined schema (random field names) +// values are randomly generated, with an equal distribution to whether it's a String, Object, or List +fn bench_object_list_unknown_schema(c: &mut Criterion) { + c.bench_function("bench_object_list_unknown_schema", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 1001); + + b.iter(|| { + let mut rng = rand::rng(); + + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..200 { + let mut object_builder = list_builder.new_object(); + + for _num_fields in 0..random::(&mut rng, 0..100) { + let key = string_table.next(); + + if rng.random_bool(0.33) { + object_builder.insert(key, key); + continue; + } + + if rng.random_bool(0.5) { + let mut inner_object_builder = object_builder.new_object("rand_object"); + + for _num_fields in 0..random::(&mut rng, 0..25) { + let key = string_table.next(); + inner_object_builder.insert(key, key); + } + inner_object_builder.finish(); + + continue; + } + + let mut inner_list_builder = object_builder.new_list("rand_list"); + + for _num_elements in 0..random::(&mut rng, 0..25) { + inner_list_builder.append_value(key); + } + + inner_list_builder.finish(); + } + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +// Creates objects with a homogenous schema (same field names) +/* + { + "id": &[u8], // Following are common across all objects + "span_id: &[u8], + "created": u32, + "ended": u32, + "span_name": String, + + "attributees": { + // following fields are randomized + } + } +*/ +fn bench_object_partially_same_schema(c: &mut Criterion) { + c.bench_function("bench_object_partially_same_schema", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 117); + + b.iter(|| { + let mut rng = rand::rng(); + + for _ in 0..200 { + let mut variant = VariantBuilder::new(); + let mut object_builder = variant.new_object(); + + object_builder.insert( + "id", + random::(&mut rng, 0..i128::MAX) + .to_le_bytes() + .as_slice(), + ); + + object_builder.insert( + "span_id", + random::(&mut rng, 0..i128::MAX) + .to_le_bytes() + .as_slice(), + ); + + object_builder.insert("created", random::(&mut rng, 0..u32::MAX) as i32); + object_builder.insert("ended", random::(&mut rng, 0..u32::MAX) as i32); + object_builder.insert("span_name", string_table.next()); + + { + let mut inner_object_builder = object_builder.new_object("attributes"); + + for _num_fields in 0..random::(&mut rng, 0..100) { + let key = string_table.next(); + inner_object_builder.insert(key, key); + } + inner_object_builder.finish(); + } + + object_builder.finish(); + hint::black_box(variant.finish()); + } + }) + }); +} + +// Creates a list of variant objects with a partially homogenous schema (similar field names) +/* + { + "id": &[u8], // Following are common across all objects + "span_id: &[u8], + "created": u32, + "ended": u32, + "span_name": String, + + "attributees": { + // following fields are randomized + } + } +*/ +fn bench_object_list_partially_same_schema(c: &mut Criterion) { + c.bench_function("bench_object_list_partially_same_schema", |b| { + let mut rng = rand::rng(); + let mut string_table = RandomStringGenerator::new(&mut rng, 117); + + b.iter(|| { + let mut variant = VariantBuilder::new(); + + let mut list_builder = variant.new_list(); + + for _ in 0..100 { + let mut object_builder = list_builder.new_object(); + + object_builder.insert( + "id", + random::(&mut rng, 0..i128::MAX) + .to_le_bytes() + .as_slice(), + ); + + object_builder.insert( + "span_id", + random::(&mut rng, 0..i128::MAX) + .to_le_bytes() + .as_slice(), + ); + + object_builder.insert("created", random::(&mut rng, 0..u32::MAX) as i32); + object_builder.insert("ended", random::(&mut rng, 0..u32::MAX) as i32); + object_builder.insert("span_name", string_table.next()); + + { + let mut inner_object_builder = object_builder.new_object("attributes"); + + for _num_fields in 0..random::(&mut rng, 0..100) { + let key = string_table.next(); + inner_object_builder.insert(key, key); + } + inner_object_builder.finish(); + } + + object_builder.finish(); + } + + list_builder.finish(); + hint::black_box(variant.finish()); + }) + }); +} + +criterion_group!( + benches, + bench_object_field_names_reverse_order, + bench_object_same_schema, + bench_object_list_same_schema, + bench_object_unknown_schema, + bench_object_list_unknown_schema, + bench_object_partially_same_schema, + bench_object_list_partially_same_schema +); + +criterion_main!(benches); diff --git a/profile.json.gz b/profile.json.gz index 4d71f7906864ac5acef7a2cf9468535eca10b121..d76385376432fdcba99acddfe4e768a71111f0a0 100644 GIT binary patch literal 3802 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 QlDYo$e>lYfx@$uK0I1!AYXATM literal 3951 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