adriangb commented on code in PR #10917: URL: https://github.com/apache/arrow-rs/pull/10917#discussion_r3891303085
########## parquet/examples/chunk_probe_writer.rs: ########## @@ -0,0 +1,652 @@ +// 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. + +//! An adaptive Parquet writer that measures encodings instead of guessing them. +//! +//! The writer's default encoding choices are made ahead of time from the schema +//! and a handful of size limits. They cannot know whether a particular column's +//! actual values compress better as a dictionary, as deltas, or as plain +//! values. This example shows how a caller can find out by measurement, using +//! [`ArrowRowGroupWriterFactory::create_column_writer`], which builds a writer +//! for one leaf column of one row group at properties of the caller's choosing. +//! +//! For each row group, and for each column that has not yet made up its mind, +//! the writer encodes a short probe prefix of that column once per candidate +//! set of writer properties, through throwaway single-column writers. Closing +//! one yields a `ColumnCloseResult` whose metadata carries the compressed size +//! that candidate actually achieved. The smallest wins, the probe chunks are +//! discarded, and the row group is then written for real through one ordinary +//! column writer per column at that column's current choice. +//! +//! The cost model is deliberately simple. A column that is still deciding +//! encodes its probe prefix K + 1 times: K throwaway passes plus the real one. +//! The prefix is one data page worth of rows rather than a whole row group, so +//! the extra work is bounded by the page size and not by the data. A column +//! that has settled costs nothing extra at all: no probe writer is built for +//! it, so no page store is allocated for it either. That is what addressing one +//! column at a time buys over building every column writer at once. +//! +//! # Dictionary candidates +//! +//! A dictionary is not a special case here, because a probe measures it rather +//! than estimating it. Closing a probe writer produces a complete column chunk, +//! and the `compressed_size` on its `ColumnCloseResult` is the size of the whole +//! chunk: the dictionary page, then every data page that indexes into it. A +//! dictionary candidate is therefore charged the full cost of the dictionary it +//! built, and cheap looking `RLE_DICTIONARY` data pages cannot flatter it. So +//! "is a dictionary worthwhile for this column" needs no ratio or cardinality +//! heuristic; it is decided the same way as every other candidate, by the +//! smallest measured chunk winning. +//! +//! What a probe cannot see is the rest of the row group. A prefix's distinct +//! value count is only a lower bound on the row group's, so a prefix can flatter +//! a dictionary that the full data would not sustain. Two things keep that from +//! being a problem. The first is the writer's own fallback: a column chunk may +//! carry at most one dictionary page, so when the dictionary being built passes +//! `dictionary_page_size_limit`, the writer emits that page for the values +//! indexed so far, flushes the data pages that reference it, and encodes the +//! remainder of the chunk with the fallback encoding. The chunk stays valid and +//! the choice degrades instead of failing. The second is the periodic re-race +//! below, which is what moves a column off a dictionary in later row groups once +//! the data has drifted away from what the prefix suggested. +//! +//! ```text +//! cargo run --example chunk_probe_writer --features arrow +//! ``` + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::fs::File; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + +use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; + +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use parquet::arrow::arrow_writer::{ + ArrowColumnWriter, ArrowLeafColumn, ArrowWriter, compute_leaves, +}; +use parquet::basic::{Compression, Encoding, Type as PhysicalType}; +use parquet::errors::Result; +use parquet::file::properties::{WriterProperties, WriterPropertiesBuilder, WriterPropertiesPtr}; +use parquet::schema::types::{ColumnDescPtr, ColumnPath}; + +/// Total rows in the generated dataset. +const ROWS: usize = 400_000; +/// Rows per row group. Eight row groups, so re-racing has something to do. +const ROW_GROUP_ROWS: usize = 50_000; +/// Rows per record batch handed to the writers. +const BATCH_ROWS: usize = 10_000; +/// Rows per data page, shared by both writers so their output is comparable. +const DATA_PAGE_ROWS: usize = 10_000; + +/// Rows of each row group encoded through the throwaway probe writers. +/// +/// One data page worth. Large enough that a dictionary candidate has built a +/// real dictionary and the compressor has something to chew on, small enough +/// that encoding it twice is a fraction of the row group rather than all of it. +const PROBE_ROWS: usize = DATA_PAGE_ROWS; + +/// A column settles on a candidate once the best and worst probe results differ +/// by at least this fraction. A close race is not evidence, so such a column +/// keeps racing on later row groups. +const SETTLE_GAP: f64 = 0.10; Review Comment: Should it just pick one at random? Or should we do some sort of exponential reprobe based on score proximity? ########## parquet/examples/chunk_probe_writer.rs: ########## @@ -0,0 +1,652 @@ +// 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. + +//! An adaptive Parquet writer that measures encodings instead of guessing them. +//! +//! The writer's default encoding choices are made ahead of time from the schema +//! and a handful of size limits. They cannot know whether a particular column's +//! actual values compress better as a dictionary, as deltas, or as plain +//! values. This example shows how a caller can find out by measurement, using +//! [`ArrowRowGroupWriterFactory::create_column_writer`], which builds a writer +//! for one leaf column of one row group at properties of the caller's choosing. +//! +//! For each row group, and for each column that has not yet made up its mind, +//! the writer encodes a short probe prefix of that column once per candidate +//! set of writer properties, through throwaway single-column writers. Closing +//! one yields a `ColumnCloseResult` whose metadata carries the compressed size +//! that candidate actually achieved. The smallest wins, the probe chunks are +//! discarded, and the row group is then written for real through one ordinary +//! column writer per column at that column's current choice. +//! +//! The cost model is deliberately simple. A column that is still deciding +//! encodes its probe prefix K + 1 times: K throwaway passes plus the real one. +//! The prefix is one data page worth of rows rather than a whole row group, so +//! the extra work is bounded by the page size and not by the data. A column +//! that has settled costs nothing extra at all: no probe writer is built for +//! it, so no page store is allocated for it either. That is what addressing one +//! column at a time buys over building every column writer at once. +//! +//! # Dictionary candidates +//! +//! A dictionary is not a special case here, because a probe measures it rather +//! than estimating it. Closing a probe writer produces a complete column chunk, +//! and the `compressed_size` on its `ColumnCloseResult` is the size of the whole +//! chunk: the dictionary page, then every data page that indexes into it. A +//! dictionary candidate is therefore charged the full cost of the dictionary it +//! built, and cheap looking `RLE_DICTIONARY` data pages cannot flatter it. So +//! "is a dictionary worthwhile for this column" needs no ratio or cardinality +//! heuristic; it is decided the same way as every other candidate, by the +//! smallest measured chunk winning. +//! +//! What a probe cannot see is the rest of the row group. A prefix's distinct +//! value count is only a lower bound on the row group's, so a prefix can flatter +//! a dictionary that the full data would not sustain. Two things keep that from +//! being a problem. The first is the writer's own fallback: a column chunk may +//! carry at most one dictionary page, so when the dictionary being built passes +//! `dictionary_page_size_limit`, the writer emits that page for the values +//! indexed so far, flushes the data pages that reference it, and encodes the +//! remainder of the chunk with the fallback encoding. The chunk stays valid and +//! the choice degrades instead of failing. The second is the periodic re-race +//! below, which is what moves a column off a dictionary in later row groups once +//! the data has drifted away from what the prefix suggested. +//! +//! ```text +//! cargo run --example chunk_probe_writer --features arrow +//! ``` + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::fs::File; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + +use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; + +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use parquet::arrow::arrow_writer::{ + ArrowColumnWriter, ArrowLeafColumn, ArrowWriter, compute_leaves, +}; +use parquet::basic::{Compression, Encoding, Type as PhysicalType}; +use parquet::errors::Result; +use parquet::file::properties::{WriterProperties, WriterPropertiesBuilder, WriterPropertiesPtr}; +use parquet::schema::types::{ColumnDescPtr, ColumnPath}; + +/// Total rows in the generated dataset. +const ROWS: usize = 400_000; +/// Rows per row group. Eight row groups, so re-racing has something to do. +const ROW_GROUP_ROWS: usize = 50_000; +/// Rows per record batch handed to the writers. +const BATCH_ROWS: usize = 10_000; +/// Rows per data page, shared by both writers so their output is comparable. +const DATA_PAGE_ROWS: usize = 10_000; Review Comment: Instead of a fixed probe size, could we look for convergence? Can we track "1 page" or "3 pages" instead of a fixed row count? ########## parquet/examples/chunk_probe_writer.rs: ########## @@ -0,0 +1,652 @@ +// 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. + +//! An adaptive Parquet writer that measures encodings instead of guessing them. +//! +//! The writer's default encoding choices are made ahead of time from the schema +//! and a handful of size limits. They cannot know whether a particular column's +//! actual values compress better as a dictionary, as deltas, or as plain +//! values. This example shows how a caller can find out by measurement, using +//! [`ArrowRowGroupWriterFactory::create_column_writer`], which builds a writer +//! for one leaf column of one row group at properties of the caller's choosing. +//! +//! For each row group, and for each column that has not yet made up its mind, +//! the writer encodes a short probe prefix of that column once per candidate +//! set of writer properties, through throwaway single-column writers. Closing +//! one yields a `ColumnCloseResult` whose metadata carries the compressed size +//! that candidate actually achieved. The smallest wins, the probe chunks are +//! discarded, and the row group is then written for real through one ordinary +//! column writer per column at that column's current choice. +//! +//! The cost model is deliberately simple. A column that is still deciding +//! encodes its probe prefix K + 1 times: K throwaway passes plus the real one. +//! The prefix is one data page worth of rows rather than a whole row group, so +//! the extra work is bounded by the page size and not by the data. A column +//! that has settled costs nothing extra at all: no probe writer is built for +//! it, so no page store is allocated for it either. That is what addressing one +//! column at a time buys over building every column writer at once. +//! +//! # Dictionary candidates +//! +//! A dictionary is not a special case here, because a probe measures it rather +//! than estimating it. Closing a probe writer produces a complete column chunk, +//! and the `compressed_size` on its `ColumnCloseResult` is the size of the whole +//! chunk: the dictionary page, then every data page that indexes into it. A +//! dictionary candidate is therefore charged the full cost of the dictionary it +//! built, and cheap looking `RLE_DICTIONARY` data pages cannot flatter it. So +//! "is a dictionary worthwhile for this column" needs no ratio or cardinality +//! heuristic; it is decided the same way as every other candidate, by the +//! smallest measured chunk winning. +//! +//! What a probe cannot see is the rest of the row group. A prefix's distinct +//! value count is only a lower bound on the row group's, so a prefix can flatter +//! a dictionary that the full data would not sustain. Two things keep that from +//! being a problem. The first is the writer's own fallback: a column chunk may +//! carry at most one dictionary page, so when the dictionary being built passes +//! `dictionary_page_size_limit`, the writer emits that page for the values +//! indexed so far, flushes the data pages that reference it, and encodes the +//! remainder of the chunk with the fallback encoding. The chunk stays valid and +//! the choice degrades instead of failing. The second is the periodic re-race +//! below, which is what moves a column off a dictionary in later row groups once +//! the data has drifted away from what the prefix suggested. +//! +//! ```text +//! cargo run --example chunk_probe_writer --features arrow +//! ``` + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::fs::File; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + +use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; + +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use parquet::arrow::arrow_writer::{ + ArrowColumnWriter, ArrowLeafColumn, ArrowWriter, compute_leaves, +}; +use parquet::basic::{Compression, Encoding, Type as PhysicalType}; +use parquet::errors::Result; +use parquet::file::properties::{WriterProperties, WriterPropertiesBuilder, WriterPropertiesPtr}; +use parquet::schema::types::{ColumnDescPtr, ColumnPath}; + +/// Total rows in the generated dataset. +const ROWS: usize = 400_000; +/// Rows per row group. Eight row groups, so re-racing has something to do. +const ROW_GROUP_ROWS: usize = 50_000; +/// Rows per record batch handed to the writers. +const BATCH_ROWS: usize = 10_000; +/// Rows per data page, shared by both writers so their output is comparable. +const DATA_PAGE_ROWS: usize = 10_000; + +/// Rows of each row group encoded through the throwaway probe writers. +/// +/// One data page worth. Large enough that a dictionary candidate has built a +/// real dictionary and the compressor has something to chew on, small enough +/// that encoding it twice is a fraction of the row group rather than all of it. +const PROBE_ROWS: usize = DATA_PAGE_ROWS; + +/// A column settles on a candidate once the best and worst probe results differ +/// by at least this fraction. A close race is not evidence, so such a column +/// keeps racing on later row groups. +const SETTLE_GAP: f64 = 0.10; + +/// A settled column re-races every this many row groups, in case the data has +/// changed character since it settled. This is also what corrects a dictionary +/// that a probe prefix made look better than the whole column can sustain. +const RERACE_EVERY: usize = 4; Review Comment: Does seem like this could be tied to how big the gap is - if there's a decisive winner less probably we re-probe. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
