Skip to content

feat(benchmark): add format-level Parquet read/write micro-benchmarks - #232

Open
SteNicholas wants to merge 1 commit into
apache:mainfrom
SteNicholas:PAIMON-228
Open

feat(benchmark): add format-level Parquet read/write micro-benchmarks#232
SteNicholas wants to merge 1 commit into
apache:mainfrom
SteNicholas:PAIMON-228

Conversation

@SteNicholas

@SteNicholas SteNicholas commented Aug 20, 2026

Copy link
Copy Markdown
Member

Purpose

Linked issue: close #228

benchmark/ only had table-level cases, so a change confined to the format layer — writer properties, encoding passthrough, per-batch overhead in the read loop — could not be measured without the noise of catalog lookup, split planning, merge/sort and commit.

This adds paimon-parquet-format-benchmark, driving ParquetWriterBuilder and ParquetFileBatchReader directly. Every axis is swept on its own rather than as a combined matrix, so each case answers one question and a change can be attributed to it.

Writer cases — flat primitives; VARCHAR by cardinality with and without file-level dictionary encoding; dictionary-encoded VARCHAR / INTEGER input arrays against flat controls that hold value, cardinality and width fixed; DECIMAL at precision 9 / 18 / 38; nested STRUCT / LIST / VECTOR / MAP; null density 0–100%; rows per AddBatch; column count; row group size; the byte-triggered writer memory threshold; and every codec Parquet accepts.

Reader cases — full scan; projection; predicate-filtered reads with page-index filtering on and off; skip-heavy reads driven by a strided selection bitmap whose strides straddle the coalesce hole limit; null density; DECIMAL; DOUBLE; dictionary against plain files; rows per NextBatch; nested column reads.

Metricsns_per_row / bytes_per_row alongside ns_per_input_row / bytes_per_input_row, so settings that prune by different amounts stay comparable (pruning shrinks the per-materialized-row numerator and denominator together), plus the reader's own row_groups / row_groups_after_filter / batches counters. Read cases assert on the rows they materialized, and the process exits non-zero on any SkipWithError, so a case that stops measuring what it claims fails instead of looking fast.

Dictionary-encoded input gets cases of its own rather than being folded into the string ones. Paimon has no dictionary FieldType, but nothing in ParquetWriterBuilder rejects a dictionary Arrow type, so the writer does reach Arrow's decision about whether to pass indices through or materialize them — and each dictionary case is paired with a flat control that holds value, cardinality and width fixed, so the delta is the materialization alone.

Tests

New paimon-parquet-format-benchmark-test (7 cases). The benchmark only builds under PAIMON_BUILD_BENCHMARKS, which CI does not set, so this test is what keeps the format-layer assumptions it rests on covered:

  • RegisteredCodecsWrite — every registered codec writes and reads back with matching values; asserts "lz4" is still rejected (it resolves to Arrow's LZ4_FRAME, which parquet::IsCodecSupported rejects — this is why the sweep registers lz4_raw and lz4_hadoop instead).
  • DictionaryInputRoundTrip — dictionary-encoded VARCHAR / INT32 input over multiple AddBatch calls, compared value by value against the flat equivalent.
  • DecimalPrecisionRoundTrip — precision 9 / 18 / 38, checking type and values (38 is the only one reaching FIXED_LEN_BYTE_ARRAY).
  • NestedRoundTripLIST / MAP round trip, full row and per-column projection.
  • FilteredAndBitmapRowCounts — predicate row counts bounded at the row-group grain, and selection-bitmap lower bound.
  • PlainAndDictionaryFilesAgree — the two files the encoding case compares must decode identically.
  • MemoryThresholdFlushesRowGroups — a small parquet.writer.max.memory.use cuts extra row groups on both plain and dictionary input, and row-group boundaries do not change the data.

API and Format

No. Nothing under include/ changes, and no storage format or protocol is affected. The new files are a benchmark binary and its test; the only existing files touched are benchmark/CMakeLists.txt and the benchmark docs.

Documentation

Yes — docs/source/examples/benchmark.rst gains a paimon-parquet-format-benchmark section covering the case inventory, how to read each counter (including why filtered cases must be compared on ns_per_input_row rather than ns_per_row, and that row_groups_after_filter is row-group level and does not show page-level pruning), and how the local-FileSystem write path bounds what the absolute numbers mean.

Generative AI tooling

Generated-by: Claude Opus 5 (Claude Code)

🤖 Generated with Claude Code

@SteNicholas
SteNicholas force-pushed the PAIMON-228 branch 2 times, most recently from 697d536 to 5b26215 Compare August 20, 2026 14:15
Add paimon-parquet-format-benchmark, driving ParquetWriterBuilder and
ParquetFileBatchReader directly so a change confined to the format layer can
be attributed without the catalog lookup, split planning, merge/sort and
commit that the table-level read_write_benchmark includes. Every axis is
swept on its own rather than as a combined matrix, so each case answers one
question and a change can be attributed to it.

Writer cases cover flat primitives, VARCHAR by cardinality with and without
file-level dictionary encoding, dictionary-encoded VARCHAR/INTEGER input
arrays against flat controls holding value and width fixed, DECIMAL at
precision 9/18/38, nested STRUCT/LIST/VECTOR/MAP, null density, rows per
AddBatch call, column count, row group size, the byte-triggered writer memory
threshold, and every codec Parquet accepts.

Reader cases cover full scan, projection, predicate-filtered reads with
page-index filtering on and off, skip-heavy reads driven by a strided
selection bitmap whose strides straddle the coalesce hole limit, null
density, DECIMAL, DOUBLE, dictionary against plain files, rows per NextBatch
call, and nested column reads.

Each case reports ns_per_row and bytes_per_row next to ns_per_input_row and
bytes_per_input_row, so settings that prune by different amounts stay
comparable, plus the reader's own row-group and batch counters. Read cases
assert on the rows they materialized and the process exits non-zero on any
SkipWithError, so a case that stops measuring what it claims fails instead of
looking fast.

Also add paimon-parquet-format-benchmark-test, which guards the format-layer
assumptions the benchmark rests on. The benchmark only builds under
PAIMON_BUILD_BENCHMARKS, which CI does not set, so the test is what keeps
codec acceptance, dictionary round-trips, DECIMAL precision, nested types,
predicate and bitmap row counts, encoding agreement and the memory-threshold
flush covered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add format-level Parquet read/write micro-benchmarks

1 participant