This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ec7f76d15e [bench](parquet) Add native reader microbenchmark matrix 
(#65921)
9ec7f76d15e is described below

commit 9ec7f76d15efc2df544b8ba6c1afdee76fb9d8c4
Author: Gabriel <[email protected]>
AuthorDate: Thu Jul 23 08:01:50 2026 +0800

    [bench](parquet) Add native reader microbenchmark matrix (#65921)
    
    ## What problem does this PR solve?
    
    Adds a reproducible local microbenchmark foundation for the native
    Parquet reader described in the [benchmark
    design]. It
    separates page decoder cost from end-to-end local reader cost and keeps
    fixture construction and encoding validation outside measured regions.
    
    ## What is changed?
    
    - Add 152 native decoder cases covering PLAIN, dictionary,
    byte-stream-split, and DELTA encodings across fixed-width and binary
    physical types, with clustered and alternating sparse selections.
    - Add 137 local reader cases covering open-to-first-block, full scan,
    predicate scan, LIMIT shapes, null density/pattern, predicate
    selectivity, projection mode, schema width, predicate position, and four
    physical encodings.
    - Generate deterministic fixtures lazily and validate every row group
    and column encoding from Parquet footer metadata before measuring.
    - Report rows/s, bytes/s, raw/selected rows, fixture size, ns/raw row,
    and ns/selected row.
    - Add matrix constraint tests and local build/run documentation.
    
    ## Check List
    
    - [x] Release benchmark target builds with `ninja -C be/build_RELEASE
    -j128 benchmark_test`
    - [x] Scenario matrix tests pass (4/4)
    - [x] All 152 decoder benchmarks pass
    - [x] All 137 reader benchmarks pass
    - [x] clang-format 16 and `git diff --check` pass
---
 be/benchmark/benchmark_main.cpp                    |  39 ++
 be/benchmark/parquet/AGENTS.md                     | 296 +++++++++++++
 be/benchmark/parquet/README.md                     |  63 +++
 be/benchmark/parquet/benchmark_parquet_decoder.hpp | 479 +++++++++++++++++++++
 be/benchmark/parquet/benchmark_parquet_reader.hpp  | 448 +++++++++++++++++++
 be/benchmark/parquet/parquet_benchmark_scenarios.h | 250 +++++++++++
 .../parquet/parquet_benchmark_scenarios_test.cpp   | 129 ++++++
 7 files changed, 1704 insertions(+)

diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index d6ac9c113f4..582db25ff6d 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -17,6 +17,11 @@
 
 #include <benchmark/benchmark.h>
 
+#include <cstdlib>
+#include <filesystem>
+#include <iostream>
+#include <vector>
+
 #include "benchmark_arrow_validation.hpp"
 #include "benchmark_bit_pack.hpp"
 #include "benchmark_bits.hpp"
@@ -39,6 +44,8 @@
 #include "core/column/column_string.h"
 #include "core/data_type/data_type.h"
 #include "core/data_type/data_type_string.h"
+#include "parquet/benchmark_parquet_decoder.hpp"
+#include "parquet/benchmark_parquet_reader.hpp"
 #include "runtime/exec_env.h"
 #include "runtime/memory/mem_tracker_limiter.h"
 #include "runtime/memory/thread_mem_tracker_mgr.h"
@@ -52,6 +59,35 @@
 
 namespace doris { // change if need
 
+static bool init_benchmark_config(const char* executable) {
+    std::vector<std::filesystem::path> candidates;
+    if (const char* doris_home = std::getenv("DORIS_HOME"); doris_home != 
nullptr) {
+        candidates.emplace_back(std::filesystem::path(doris_home) / "conf" / 
"be.conf");
+    }
+    candidates.emplace_back(std::filesystem::current_path() / "conf" / 
"be.conf");
+    const auto executable_dir = 
std::filesystem::absolute(executable).parent_path();
+    candidates.emplace_back(executable_dir / ".." / ".." / "conf" / "be.conf");
+    candidates.emplace_back(executable_dir / ".." / ".." / ".." / "conf" / 
"be.conf");
+    for (const auto& candidate : candidates) {
+        if (!std::filesystem::exists(candidate)) {
+            continue;
+        }
+        if (std::getenv("DORIS_HOME") == nullptr) {
+            const auto inferred_home = 
candidate.parent_path().parent_path().string();
+            // be.conf contains paths relative to DORIS_HOME. Keep standalone 
benchmark launches
+            // equivalent to build/test scripts after locating the same 
repository config.
+            if (::setenv("DORIS_HOME", inferred_home.c_str(), 0) != 0) {
+                return false;
+            }
+        }
+        // Mutable config storage is populated by config::init. Reader 
benchmarks must use the
+        // production defaults because zero-initialized safety limits reject 
every valid footer.
+        return config::init(candidate.c_str(), false);
+    }
+    std::cerr << "Unable to find conf/be.conf for benchmark initialization\n";
+    return false;
+}
+
 static void Example1(benchmark::State& state) {
     // init. dont time it.
     state.PauseTiming();
@@ -78,6 +114,9 @@ BENCHMARK(Example1);
 // ThreadContext + mem tracker, otherwise the allocator throws E-7412. Mirrors
 // the minimal subset of be/test/testutil/run_all_tests.cpp::main.
 int main(int argc, char** argv) {
+    if (!doris::init_benchmark_config(argv[0])) {
+        return 1;
+    }
     doris::config::enable_bmi2_optimizations = true;
 
     SCOPED_INIT_THREAD_CONTEXT();
diff --git a/be/benchmark/parquet/AGENTS.md b/be/benchmark/parquet/AGENTS.md
new file mode 100644
index 00000000000..a8667fb43cd
--- /dev/null
+++ b/be/benchmark/parquet/AGENTS.md
@@ -0,0 +1,296 @@
+# Parquet microbenchmark guide for agents
+
+This file applies to `be/benchmark/parquet/`. Read it before changing, 
running, or interpreting
+these benchmarks. The current suite is a local benchmark foundation, not the 
complete Parquet
+benchmark system described in the design document.
+
+## What exists today
+
+The benchmark binary registers two groups:
+
+- `ParquetDecoder`: native page decoder benchmarks using in-memory encoded 
pages.
+- `ParquetReader`: local-file benchmarks that call the format V2 Parquet 
reader directly.
+
+The relevant files are:
+
+- `benchmark_parquet_decoder.hpp`: deterministic page construction and decoder 
registration.
+- `benchmark_parquet_reader.hpp`: deterministic local Parquet fixtures and 
reader registration.
+- `parquet_benchmark_scenarios.h`: scenario definitions and the selected 
matrix.
+- `README.md`: short human-oriented build and invocation examples.
+- `be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp`: matrix 
invariants.
+
+Do not describe this suite as end-to-end SQL, `FileScannerV2`, remote I/O, 
V1/V2 comparison, or a
+cross-engine benchmark. The reader benchmark starts at 
`format::parquet::ParquetReader` and does
+not include FE planning, scanner scheduling, `TableReader`, client latency, or 
Runtime Profile
+collection.
+
+## Build and list cases
+
+Performance results must come from a Release build:
+
+```shell
+./build.sh --benchmark -j128
+```
+
+List all Parquet cases and verify the expected registration counts:
+
+```shell
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -E '^Parquet(Decoder|Reader)/'
+
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -c '^ParquetDecoder/'  # currently 152
+
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -c '^ParquetReader/'   # currently 137
+```
+
+When running the binary directly from `be/build_RELEASE/bin`, make sure the 
JVM and third-party
+libraries are discoverable. Prefer the installed 
`be/output/lib/benchmark_test` produced by
+`build.sh` because the normal Doris environment already configures its 
dependencies.
+
+## Run smoke verification
+
+A smoke run proves that every registered case initializes and completes. It 
does not produce a
+stable performance baseline:
+
+```shell
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetDecoder/' \
+  --benchmark_min_time=0.001s \
+  --benchmark_out=parquet-decoder-smoke.json \
+  --benchmark_out_format=json
+
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetReader/' \
+  --benchmark_min_time=0.001s \
+  --benchmark_out=parquet-reader-smoke.json \
+  --benchmark_out_format=json
+```
+
+Reject a smoke run if the process is non-zero, the expected number of JSON 
results is absent, or
+any result contains `error_occurred`. Also run the scenario matrix unit test 
after changing the
+matrix. Never use the 1 ms smoke measurements to claim a speedup or regression.
+
+## Run a performance comparison
+
+Compare the same named cases on the same host, compiler, build flags, CPU set, 
NUMA node, and power
+policy. Use the same fixture directory and cache state for both revisions. A 
suitable local command
+is:
+
+```shell
+taskset -c 8 be/output/lib/benchmark_test \
+  
--benchmark_filter='^ParquetReader/predicate_scan/plain/null_50/alternating/sel_10/'
 \
+  --benchmark_min_time=1s \
+  --benchmark_repetitions=10 \
+  --benchmark_report_aggregates_only=true \
+  --benchmark_out=parquet-reader.json \
+  --benchmark_out_format=json
+```
+
+Use at least three untimed warmups before collecting local warm-cache results. 
Run revisions in an
+ABBA order when comparing two commits. Do not compare results from different 
CPUs, cache
+topologies, compiler versions, or build types.
+
+The suite does not currently control the OS page cache. Repeated reader cases 
are normally
+warm-cache measurements. Do not label them cold-NVMe results. Do not clear a 
shared machine's page
+cache to manufacture a cold run.
+
+## Current scenario matrix
+
+`ParquetDecoder` contains 19 encoding/type pairs. Each pair is run at 1%, 10%, 
50%, and 100%
+selection with clustered and alternating selection ranges, for 152 registered 
cases.
+
+| Encoding | Physical types |
+|---|---|
+| PLAIN | INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY |
+| Dictionary | INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY |
+| BYTE_STREAM_SPLIT | FLOAT, DOUBLE, FIXED_LEN_BYTE_ARRAY |
+| DELTA_BINARY_PACKED | INT32, INT64 |
+| DELTA_LENGTH_BYTE_ARRAY | BYTE_ARRAY |
+| DELTA_BYTE_ARRAY | BYTE_ARRAY |
+
+`ParquetReader` deliberately uses a single-variable matrix rather than a 
Cartesian product. After
+deduplication it contains 137 cases covering:
+
+- operations: open-to-first-block, full scan, predicate scan, limit 1, and 
limit 1000;
+- file encodings: PLAIN, dictionary, BYTE_STREAM_SPLIT, and 
DELTA_BINARY_PACKED;
+- null ratios: 0%, 1%, 10%, 50%, and 90%;
+- null shapes: clustered and alternating;
+- predicate selectivities: 0%, 1%, 10%, 50%, 90%, and 100%;
+- projection shapes: predicate-only and predicate plus one lazy payload column;
+- schema widths: 4, 32, 128, and 512 columns;
+- predicate position: first or last column.
+
+Except for the axis being varied, reader cases inherit the baseline: nullable 
INT32, PLAIN,
+alternating 10% nulls, 10% selectivity, 32 columns, predicate at column zero, 
and predicate plus
+payload projection.
+
+## How decoder data is generated
+
+Decoder pages are constructed in memory before the timed loop. There is no 
Parquet file, Python
+generator, random seed, or manifest involved.
+
+- Every page contains 65,536 logical values.
+- Integer values use `(row * 17) % 1000003`.
+- Floating-point values use `(row % 1009) * 0.25 - 100.0`.
+- Binary values are fixed at 16 bytes. Their first bytes contain `row % 1009` 
and the remaining
+  bytes are deterministic filler.
+- PLAIN fixed-width values are copied in physical byte order. PLAIN BYTE_ARRAY 
values use a
+  four-byte length followed by payload bytes.
+- BYTE_STREAM_SPLIT pages transpose the byte lanes of the fixed-width PLAIN 
representation.
+- DELTA pages are produced with the Arrow Parquet DELTA encoders.
+- Dictionary pages contain 256 deterministic entries. IDs repeat from 0 
through 255 and are
+  encoded with an eight-bit RLE/bit-packed stream.
+- Clustered selection is one continuous range. Alternating selection spreads 
selected rows evenly
+  and intentionally produces many one-row physical ranges.
+
+Page generation, selection construction, decoder creation, dictionary setup, 
and `set_data` are
+outside the timed decode call. The sinks consume decoder callbacks and prevent 
compiler removal,
+but they do not build a Doris `Column`. Consequently these cases isolate 
decoder traversal and
+selection cost; they do not measure definition-level decoding, nullable 
reconstruction, type
+conversion, or full column materialization.
+
+## How reader Parquet files are generated
+
+Reader fixtures are generated lazily by C++ code using Arrow builders and the 
Arrow Parquet writer.
+They are stored under:
+
+```text
+${TMPDIR:-/tmp}/doris_parquet_reader_benchmark/
+```
+
+Fixture contents and writer settings are:
+
+- 16,384 rows and 4 row groups of 4,096 rows each;
+- all columns are nullable INT32 and reuse the same deterministic Arrow array;
+- every non-null value is `row % 100`;
+- the predicate is `value < selectivity_percent`, so the threshold maps 
directly to the intended
+  non-null selectivity;
+- alternating nulls use a 101-row period and `(row * 37) % 101`, avoiding 
direct correlation with
+  the 100-value predicate period;
+- clustered nulls use contiguous null prefixes inside each 1,024-row cluster;
+- Parquet format version 2.6, DataPage V2, no compression, and statistics 
disabled;
+- dictionary is explicitly enabled only for dictionary fixtures; other 
fixtures disable dictionary
+  and request their named encoding;
+- after writing, every column chunk in every row group is checked through 
footer metadata to ensure
+  that the writer did not silently choose another encoding.
+
+The fixture name depends only on axes that change file contents. Therefore 
predicate thresholds,
+projection mode, and operation can reuse the same file. Existing fixtures are 
footer-validated
+before reuse. Delete only the dedicated `doris_parquet_reader_benchmark` 
temporary directory when
+fixture rules change; never remove a broad temporary or workspace directory.
+
+Fixture creation and footer verification happen before benchmark iterations. 
Reader initialization
+and `close()` are excluded from steady-state full/predicate/LIMIT timing. The
+`open_to_first_block` case intentionally includes reader construction, 
metadata loading, `open()`,
+and the first `get_block()` call.
+
+## Interpret the result correctly
+
+Google Benchmark JSON contains `real_time`, `cpu_time`, repetitions, and 
custom counters. Use:
+
+- `cpu_time` for decoder CPU efficiency;
+- both `real_time` and `cpu_time` for reader cases, because latency and CPU 
cost answer different
+  questions;
+- median across repetitions as the primary result;
+- p95 and a confidence interval for automated gates; never select the fastest 
repetition.
+
+Custom counters mean:
+
+- `raw_rows`: logical source rows considered by the case;
+- `selected_rows`: rows returned after the predicate or selection;
+- `ns/raw_row`: normalized source-row cost and the primary comparison across 
selectivities;
+- `ns/selected_row`: cost per surviving row; it naturally rises as selectivity 
falls;
+- `selection_ranges`: decoder physical ranges; compare alternating with 
clustered at the same
+  selectivity to expose per-range overhead;
+- `fixture_bytes` or `encoded_bytes`: fixture/page size, not peak memory;
+- `items_per_second`: selected rows per second, so it is not directly 
comparable across different
+  selectivities;
+- `bytes_per_second`: a logical benchmark counter. In reader cases it is 
estimated from projected
+  INT32 values, not measured storage-device bytes or compressed bytes.
+
+Useful comparisons are:
+
+- clustered versus alternating at equal selectivity: sparse-range/cursor 
overhead;
+- null 0% versus 1/10/50/90% at equal selection: definition-level and nullable 
reconstruction
+  overhead in the reader;
+- predicate-only versus predicate-projected: lazy materialization benefit;
+- width 4/32/128/512 with predicate first/last: schema and metadata traversal 
overhead;
+- PLAIN versus dictionary/BYTE_STREAM_SPLIT/DELTA at the same reader baseline: 
encoding path cost;
+- open-to-first-block and LIMIT cases separately from steady-state full scans.
+
+Before attributing a regression, confirm that output row counts are identical, 
the benchmark name
+and fixture encoding match, and repetition variability is acceptable. A 
coefficient of variation
+above 3% is inconclusive and should be rerun. Correlate CPU regressions with 
`perf stat` counters
+such as cycles, instructions, branch misses, and cache misses when possible.
+
+## Coverage audit and required follow-up
+
+The current matrix is not comprehensive. Preserve this distinction in PR 
descriptions and reports.
+
+### P0: complete the local benchmark phase
+
+1. Add the design's `matrix.yaml`, deterministic corpus generator, 
`manifest.json`, checksum, and
+   standalone corpus verifier. The current runtime-generated files cannot be 
shared unchanged with
+   V1, StarRocks, or DuckDB.
+2. Add correctness oracles. Benchmarks must validate consumed counts and 
representative checksums
+   outside the timed region before their performance samples are trusted.
+3. Add reader-level INT64, FLOAT, DOUBLE, BYTE_ARRAY/string, 
FIXED_LEN_BYTE_ARRAY, DATE,
+   TIMESTAMP, and DECIMAL cases. Today only nullable INT32 reaches the 
complete reader path.
+4. Extend decoder coverage with 0% and 90% selection, definition levels/null 
reconstruction,
+   dictionary conversion, and real Doris `Column` materialization. The current 
decoder sink does
+   not cover those costs or report decoded bytes per second.
+5. Add the representative nullable sparse corpus requested by the design: 32 
INT64 columns, 128
+   row groups, and enough rows to exercise many pages. The current 
16K-row/four-row-group fixture
+   is a smoke-sized workload.
+6. Add dictionary footprint sweeps based on actual bytes relative to L1/L2 
cache, value-width
+   sweeps, cardinality sweeps, and dictionary-to-PLAIN fallback/mixed-page 
files. The current
+   dictionary is small and fixed.
+7. Add a Doris V1/V2 SQL runner over the same verified corpus, correctness 
comparison, Runtime
+   Profile collection, normalized result JSON, comparison report, and the 
first stable local
+   baseline. The current 1 ms run only proves executability.
+
+### P1: broaden Parquet format and execution behavior
+
+1. Vary compression codecs: uncompressed, Snappy, ZSTD, LZ4, Brotli, and Gzip 
where supported.
+2. Cover DataPage V1 and V2, page size, row-group size/count, page boundaries, 
small files, and
+   large files that exceed cache capacity.
+3. Add required/non-nullable columns, correlated and anti-correlated 
null/predicate distributions,
+   random deterministic runs, long null runs, and selections that cross page 
boundaries.
+4. Add Boolean, INT96 compatibility, logical annotations, timestamp units/time 
zones, decimal
+   physical representations, short/long strings, skew, and empty values.
+5. Add nested ARRAY/MAP/STRUCT and repeated definition/repetition-level cases.
+6. Add row-group statistics, page index, Bloom filter, dictionary filtering, 
all-filtered batches,
+   missing-column/default-value handling, schema mapping, and schema-evolution 
cases. Keep pruning
+   benchmarks separate from decode benchmarks so skipped work is not 
misattributed to a faster
+   decoder.
+7. Add predicate LIMIT 1/1000 cases. Current LIMIT cases only vary reader 
batch size without a
+   predicate.
+8. Add peak tracked memory, allocation counts, and an explicit warm/cold-cache 
methodology.
+
+### Later phases
+
+Remote S3/FileCache/RTT workloads, prefetch metrics, cross-engine adapters, 
nightly/weekly jobs, and
+release regression gates belong to later phases. They require isolated 
infrastructure and must not
+be simulated by silently changing the local reader benchmark.
+
+## Current validation record
+
+At commit `16e05dd5c71`, a Release build completed and the matrix unit test 
passed 4/4. A 1 ms smoke
+run executed 152 decoder and 137 reader cases with zero benchmark errors. This 
is an execution
+record only. It is not a reviewed performance baseline because repetitions, 
host isolation,
+warmups, cache control, `perf` data, variance, and before/after comparison 
were not collected.
+
+## Rules for extending the suite
+
+- Keep deterministic data construction and validation outside timed regions.
+- Verify actual footer encodings; never trust writer configuration alone.
+- Change one primary axis at a time and add only meaningful second-order 
interactions.
+- Add or update matrix invariant tests before registering new dimensions.
+- Keep correctness/error-injection tests separate from throughput benchmarks.
+- Do not commit large generated corpora. Commit generation rules and 
manifests; keep only small
+  correctness fixtures in the repository when necessary.
+- Record the exact commit, compiler, build type, host topology, command, 
repetitions, cache state,
+  and raw JSON for any claimed performance result.
+- Never claim a performance improvement from a smoke run or from incomparable 
machines.
diff --git a/be/benchmark/parquet/README.md b/be/benchmark/parquet/README.md
new file mode 100644
index 00000000000..40c79bc28be
--- /dev/null
+++ b/be/benchmark/parquet/README.md
@@ -0,0 +1,63 @@
+# Parquet reader microbenchmarks
+
+These benchmarks separate native page decoding from the complete local-file 
reader path. They use
+deterministic data and verify the physical encoding recorded in each generated 
Parquet footer before
+running a measurement.
+
+Agents and maintainers must read [AGENTS.md](AGENTS.md) for the exact timing 
boundaries, synthetic
+data rules, result interpretation, current coverage limitations, and 
prioritized follow-up work.
+
+## Build
+
+Build the Release benchmark binary from the repository root:
+
+```shell
+./build.sh --benchmark -j128
+```
+
+List only the Parquet cases:
+
+```shell
+be/output/lib/benchmark_test --benchmark_list_tests | grep '^Parquet'
+```
+
+## Decoder cases
+
+`ParquetDecoder` measures the native decoder with data generation and encoder 
setup outside the
+timed region. It covers PLAIN, dictionary, byte-stream-split, and DELTA 
encodings across their
+supported fixed-width and binary physical types. Sparse selections are 
provided as both one
+clustered range and many alternating ranges.
+
+```shell
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetDecoder/plain/int64/sel_10/alternating$' \
+  --benchmark_min_time=0.1s
+```
+
+## Local reader cases
+
+`ParquetReader` measures local open-to-first-block, full scan, predicate scan, 
and LIMIT-shaped
+reads. The matrix covers:
+
+- PLAIN, dictionary, byte-stream-split, and DELTA binary-packed files;
+- NULL ratios of 0%, 1%, 10%, 50%, and 90%, with clustered and alternating 
placement;
+- predicate selectivities of 0%, 1%, 10%, 50%, 90%, and 100%;
+- predicate-only and predicate-plus-lazy-projected reads;
+- schemas with 4, 32, 128, and 512 columns, with the predicate first or last.
+
+Fixtures are created lazily under the system temporary directory in
+`doris_parquet_reader_benchmark`. Generation, footer validation, and reader 
setup are excluded from
+steady-state scan timings. `open_to_first_block` intentionally includes reader 
initialization,
+footer loading, open, and the first `get_block` call.
+
+```shell
+be/output/lib/benchmark_test \
+  
--benchmark_filter='^ParquetReader/predicate_scan/plain/null_50/alternating/sel_10/'
 \
+  --benchmark_min_time=0.1s \
+  --benchmark_out=parquet-reader.json \
+  --benchmark_out_format=json
+```
+
+Every result reports throughput plus `raw_rows`, `selected_rows`, 
`fixture_bytes`, `ns/raw_row`,
+and (when at least one row survives) `ns/selected_row`. Keep CPU frequency, 
build type, compiler,
+machine placement, and benchmark filters fixed when comparing two commits.
diff --git a/be/benchmark/parquet/benchmark_parquet_decoder.hpp 
b/be/benchmark/parquet/benchmark_parquet_decoder.hpp
new file mode 100644
index 00000000000..25fb05d1449
--- /dev/null
+++ b/be/benchmark/parquet/benchmark_parquet_decoder.hpp
@@ -0,0 +1,479 @@
+// 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.
+
+#pragma once
+
+#include <benchmark/benchmark.h>
+#include <parquet/encoding.h>
+#include <parquet/schema.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <numeric>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include "core/custom_allocator.h"
+#include "format_v2/parquet/reader/native/decoder.h"
+#include "parquet_benchmark_scenarios.h"
+#include "util/faststring.h"
+#include "util/rle_encoding.h"
+#include "util/slice.h"
+
+namespace doris::parquet_benchmark {
+namespace detail {
+
+constexpr size_t DECODER_ROWS = 1UL << 16;
+constexpr size_t DICTIONARY_ENTRIES = 256;
+constexpr size_t FIXED_BINARY_WIDTH = 16;
+
+struct EncodedPage {
+    std::vector<uint8_t> data;
+    std::vector<uint8_t> dictionary;
+    size_t dictionary_entries = 0;
+    size_t value_width = 0;
+    bool binary = false;
+};
+
+inline tparquet::Type::type physical_type(ValueType value_type) {
+    switch (value_type) {
+    case ValueType::INT32:
+        return tparquet::Type::INT32;
+    case ValueType::INT64:
+        return tparquet::Type::INT64;
+    case ValueType::FLOAT:
+        return tparquet::Type::FLOAT;
+    case ValueType::DOUBLE:
+        return tparquet::Type::DOUBLE;
+    case ValueType::BYTE_ARRAY:
+        return tparquet::Type::BYTE_ARRAY;
+    case ValueType::FIXED_LEN_BYTE_ARRAY:
+        return tparquet::Type::FIXED_LEN_BYTE_ARRAY;
+    }
+    throw std::logic_error("unknown Parquet benchmark value type");
+}
+
+inline tparquet::Encoding::type parquet_encoding(Encoding encoding) {
+    switch (encoding) {
+    case Encoding::PLAIN:
+        return tparquet::Encoding::PLAIN;
+    case Encoding::DICTIONARY:
+        return tparquet::Encoding::RLE_DICTIONARY;
+    case Encoding::BYTE_STREAM_SPLIT:
+        return tparquet::Encoding::BYTE_STREAM_SPLIT;
+    case Encoding::DELTA_BINARY_PACKED:
+        return tparquet::Encoding::DELTA_BINARY_PACKED;
+    case Encoding::DELTA_LENGTH_BYTE_ARRAY:
+        return tparquet::Encoding::DELTA_LENGTH_BYTE_ARRAY;
+    case Encoding::DELTA_BYTE_ARRAY:
+        return tparquet::Encoding::DELTA_BYTE_ARRAY;
+    }
+    throw std::logic_error("unknown Parquet benchmark encoding");
+}
+
+inline std::shared_ptr<::parquet::ColumnDescriptor> 
descriptor(::parquet::Type::type type,
+                                                               int type_length 
= -1) {
+    auto node = type == ::parquet::Type::FIXED_LEN_BYTE_ARRAY
+                        ? ::parquet::schema::PrimitiveNode::Make(
+                                  "value", ::parquet::Repetition::REQUIRED, 
type,
+                                  ::parquet::ConvertedType::NONE, type_length)
+                        : ::parquet::schema::PrimitiveNode::Make(
+                                  "value", ::parquet::Repetition::REQUIRED, 
type);
+    return std::make_shared<::parquet::ColumnDescriptor>(node, 0, 0);
+}
+
+template <typename T>
+std::vector<T> fixed_values(size_t rows) {
+    std::vector<T> values(rows);
+    for (size_t row = 0; row < rows; ++row) {
+        if constexpr (std::is_floating_point_v<T>) {
+            values[row] = static_cast<T>((row % 1009) * 0.25 - 100.0);
+        } else {
+            values[row] = static_cast<T>((row * 17) % 1000003);
+        }
+    }
+    return values;
+}
+
+inline std::vector<std::string> binary_values(size_t rows, size_t width = 
FIXED_BINARY_WIDTH) {
+    std::vector<std::string> values;
+    values.reserve(rows);
+    for (size_t row = 0; row < rows; ++row) {
+        std::string value(width, 'a');
+        const uint64_t id = row % 1009;
+        memcpy(value.data(), &id, std::min(width, sizeof(id)));
+        values.push_back(std::move(value));
+    }
+    return values;
+}
+
+inline std::vector<uint8_t> encode_plain_binary(const 
std::vector<std::string>& values) {
+    size_t bytes = 0;
+    for (const auto& value : values) {
+        bytes += sizeof(uint32_t) + value.size();
+    }
+    std::vector<uint8_t> encoded;
+    encoded.reserve(bytes);
+    for (const auto& value : values) {
+        const auto length = static_cast<uint32_t>(value.size());
+        const auto* length_bytes = reinterpret_cast<const uint8_t*>(&length);
+        encoded.insert(encoded.end(), length_bytes, length_bytes + 
sizeof(length));
+        encoded.insert(encoded.end(), value.begin(), value.end());
+    }
+    return encoded;
+}
+
+template <typename T>
+std::vector<uint8_t> encode_plain_fixed(const std::vector<T>& values) {
+    std::vector<uint8_t> encoded(values.size() * sizeof(T));
+    memcpy(encoded.data(), values.data(), encoded.size());
+    return encoded;
+}
+
+inline std::vector<uint8_t> encode_fixed_binary(const 
std::vector<std::string>& values) {
+    std::vector<uint8_t> encoded;
+    encoded.reserve(values.size() * FIXED_BINARY_WIDTH);
+    for (const auto& value : values) {
+        encoded.insert(encoded.end(), value.begin(), value.end());
+    }
+    return encoded;
+}
+
+inline std::vector<uint8_t> encode_byte_stream_split(const 
std::vector<uint8_t>& plain,
+                                                     size_t value_width) {
+    const size_t rows = plain.size() / value_width;
+    std::vector<uint8_t> encoded(plain.size());
+    for (size_t row = 0; row < rows; ++row) {
+        for (size_t byte = 0; byte < value_width; ++byte) {
+            encoded[byte * rows + row] = plain[row * value_width + byte];
+        }
+    }
+    return encoded;
+}
+
+template <typename ParquetType, typename T>
+std::vector<uint8_t> encode_delta_fixed(const std::vector<T>& values) {
+    auto desc = descriptor(ParquetType::type_num);
+    auto encoder = ::parquet::MakeTypedEncoder<ParquetType>(
+            ::parquet::Encoding::DELTA_BINARY_PACKED, false, desc.get());
+    encoder->Put(values.data(), static_cast<int>(values.size()));
+    const auto buffer = encoder->FlushValues();
+    return {buffer->data(), buffer->data() + buffer->size()};
+}
+
+inline std::vector<uint8_t> encode_delta_binary(const 
std::vector<std::string>& values,
+                                                ::parquet::Encoding::type 
encoding) {
+    std::vector<::parquet::ByteArray> arrays;
+    arrays.reserve(values.size());
+    for (const auto& value : values) {
+        arrays.emplace_back(static_cast<uint32_t>(value.size()),
+                            reinterpret_cast<const uint8_t*>(value.data()));
+    }
+    auto desc = descriptor(::parquet::Type::BYTE_ARRAY);
+    auto encoder =
+            ::parquet::MakeTypedEncoder<::parquet::ByteArrayType>(encoding, 
false, desc.get());
+    encoder->Put(arrays.data(), static_cast<int>(arrays.size()));
+    const auto buffer = encoder->FlushValues();
+    return {buffer->data(), buffer->data() + buffer->size()};
+}
+
+inline std::vector<uint8_t> encode_dictionary_indices(size_t rows) {
+    faststring encoded_ids;
+    RleEncoder<uint32_t> encoder(&encoded_ids, 8);
+    for (size_t row = 0; row < rows; ++row) {
+        encoder.Put(static_cast<uint32_t>(row % DICTIONARY_ENTRIES));
+    }
+    for (size_t padding = rows; padding % 8 != 0; ++padding) {
+        encoder.Put(0);
+    }
+    encoder.Flush();
+    std::vector<uint8_t> result(encoded_ids.size() + 1);
+    result[0] = 8;
+    memcpy(result.data() + 1, encoded_ids.data(), encoded_ids.size());
+    return result;
+}
+
+inline EncodedPage dictionary_page(ValueType value_type) {
+    EncodedPage page;
+    page.data = encode_dictionary_indices(DECODER_ROWS);
+    page.dictionary_entries = DICTIONARY_ENTRIES;
+    page.binary = value_type == ValueType::BYTE_ARRAY;
+    switch (value_type) {
+    case ValueType::INT32:
+        page.value_width = sizeof(int32_t);
+        page.dictionary = 
encode_plain_fixed(fixed_values<int32_t>(DICTIONARY_ENTRIES));
+        break;
+    case ValueType::INT64:
+        page.value_width = sizeof(int64_t);
+        page.dictionary = 
encode_plain_fixed(fixed_values<int64_t>(DICTIONARY_ENTRIES));
+        break;
+    case ValueType::FLOAT:
+        page.value_width = sizeof(float);
+        page.dictionary = 
encode_plain_fixed(fixed_values<float>(DICTIONARY_ENTRIES));
+        break;
+    case ValueType::DOUBLE:
+        page.value_width = sizeof(double);
+        page.dictionary = 
encode_plain_fixed(fixed_values<double>(DICTIONARY_ENTRIES));
+        break;
+    case ValueType::BYTE_ARRAY:
+        page.value_width = FIXED_BINARY_WIDTH;
+        page.dictionary = 
encode_plain_binary(binary_values(DICTIONARY_ENTRIES));
+        break;
+    case ValueType::FIXED_LEN_BYTE_ARRAY:
+        page.value_width = FIXED_BINARY_WIDTH;
+        page.dictionary = 
encode_fixed_binary(binary_values(DICTIONARY_ENTRIES));
+        break;
+    }
+    return page;
+}
+
+inline EncodedPage encoded_page(const DecoderScenario& scenario) {
+    if (scenario.encoding == Encoding::DICTIONARY) {
+        return dictionary_page(scenario.value_type);
+    }
+
+    EncodedPage page;
+    page.binary = scenario.value_type == ValueType::BYTE_ARRAY;
+    switch (scenario.value_type) {
+    case ValueType::INT32: {
+        auto values = fixed_values<int32_t>(DECODER_ROWS);
+        page.value_width = sizeof(int32_t);
+        page.data = scenario.encoding == Encoding::DELTA_BINARY_PACKED
+                            ? encode_delta_fixed<::parquet::Int32Type>(values)
+                            : encode_plain_fixed(values);
+        break;
+    }
+    case ValueType::INT64: {
+        auto values = fixed_values<int64_t>(DECODER_ROWS);
+        page.value_width = sizeof(int64_t);
+        page.data = scenario.encoding == Encoding::DELTA_BINARY_PACKED
+                            ? encode_delta_fixed<::parquet::Int64Type>(values)
+                            : encode_plain_fixed(values);
+        break;
+    }
+    case ValueType::FLOAT: {
+        auto plain = encode_plain_fixed(fixed_values<float>(DECODER_ROWS));
+        page.value_width = sizeof(float);
+        page.data = scenario.encoding == Encoding::BYTE_STREAM_SPLIT
+                            ? encode_byte_stream_split(plain, page.value_width)
+                            : std::move(plain);
+        break;
+    }
+    case ValueType::DOUBLE: {
+        auto plain = encode_plain_fixed(fixed_values<double>(DECODER_ROWS));
+        page.value_width = sizeof(double);
+        page.data = scenario.encoding == Encoding::BYTE_STREAM_SPLIT
+                            ? encode_byte_stream_split(plain, page.value_width)
+                            : std::move(plain);
+        break;
+    }
+    case ValueType::BYTE_ARRAY: {
+        auto values = binary_values(DECODER_ROWS);
+        page.value_width = FIXED_BINARY_WIDTH;
+        if (scenario.encoding == Encoding::DELTA_LENGTH_BYTE_ARRAY) {
+            page.data = encode_delta_binary(values, 
::parquet::Encoding::DELTA_LENGTH_BYTE_ARRAY);
+        } else if (scenario.encoding == Encoding::DELTA_BYTE_ARRAY) {
+            page.data = encode_delta_binary(values, 
::parquet::Encoding::DELTA_BYTE_ARRAY);
+        } else {
+            page.data = encode_plain_binary(values);
+        }
+        break;
+    }
+    case ValueType::FIXED_LEN_BYTE_ARRAY: {
+        auto plain = encode_fixed_binary(binary_values(DECODER_ROWS));
+        page.value_width = FIXED_BINARY_WIDTH;
+        page.data = scenario.encoding == Encoding::BYTE_STREAM_SPLIT
+                            ? encode_byte_stream_split(plain, page.value_width)
+                            : std::move(plain);
+        break;
+    }
+    }
+    return page;
+}
+
+class FixedSink final : public ParquetFixedValueConsumer {
+public:
+    Status consume(const uint8_t* values, size_t num_values, size_t 
value_width) override {
+        benchmark::DoNotOptimize(values);
+        benchmark::DoNotOptimize(num_values);
+        benchmark::DoNotOptimize(value_width);
+        consumed += num_values;
+        return Status::OK();
+    }
+
+    size_t consumed = 0;
+};
+
+class BinarySink final : public ParquetBinaryValueConsumer {
+public:
+    Status consume(const StringRef* values, size_t num_values) override {
+        benchmark::DoNotOptimize(values);
+        benchmark::DoNotOptimize(num_values);
+        consumed += num_values;
+        return Status::OK();
+    }
+
+    Status consume_plain_byte_array(
+            const char* encoded_data, const uint32_t* payload_offsets,
+            const uint32_t* value_offsets, size_t num_values,
+            const std::vector<ParquetSelectionRange>& value_spans) override {
+        benchmark::DoNotOptimize(encoded_data);
+        benchmark::DoNotOptimize(payload_offsets);
+        benchmark::DoNotOptimize(value_offsets);
+        auto value_spans_data = value_spans.data();
+        benchmark::DoNotOptimize(value_spans_data);
+        consumed += num_values;
+        return Status::OK();
+    }
+
+    size_t consumed = 0;
+};
+
+class DictionarySink final : public ParquetDictionaryValueConsumer {
+public:
+    DictionarySink(const uint8_t* dictionary, size_t value_width)
+            : _dictionary(dictionary), _value_width(value_width) {}
+
+    Status consume_indices(const uint32_t* indices, size_t num_values) 
override {
+        for (size_t row = 0; row < num_values; ++row) {
+            auto* value = _dictionary + indices[row] * _value_width;
+            benchmark::DoNotOptimize(value);
+        }
+        consumed += num_values;
+        return Status::OK();
+    }
+
+    Status consume_repeated(uint32_t index, size_t num_values) override {
+        auto* value = _dictionary + index * _value_width;
+        benchmark::DoNotOptimize(value);
+        consumed += num_values;
+        return Status::OK();
+    }
+
+    size_t consumed = 0;
+
+private:
+    const uint8_t* _dictionary;
+    const size_t _value_width;
+};
+
+inline ParquetSelection native_selection(const SelectionPlan& plan) {
+    ParquetSelection selection {
+            .total_values = plan.total_rows, .selected_values = 
plan.selected_rows, .ranges = {}};
+    selection.ranges.reserve(plan.ranges.size());
+    for (const auto& range : plan.ranges) {
+        selection.ranges.push_back({.first = range.first, .count = 
range.count});
+    }
+    return selection;
+}
+
+inline void run_decoder(benchmark::State& state, DecoderScenario scenario, int 
selectivity,
+                        Pattern pattern) {
+    auto page = encoded_page(scenario);
+    const auto plan = make_selection_plan(DECODER_ROWS, selectivity, pattern);
+    const auto selection = native_selection(plan);
+    std::unique_ptr<format::parquet::native::Decoder> decoder;
+    auto status = format::parquet::native::Decoder::get_decoder(
+            physical_type(scenario.value_type), 
parquet_encoding(scenario.encoding), decoder);
+    if (!status.ok()) {
+        state.SkipWithError(status.to_string().c_str());
+        return;
+    }
+    decoder->set_type_length(static_cast<int32_t>(page.value_width));
+    decoder->set_expected_values(DECODER_ROWS);
+
+    std::vector<uint8_t> dictionary_for_sink;
+    if (scenario.encoding == Encoding::DICTIONARY) {
+        dictionary_for_sink = page.dictionary;
+        auto dictionary = make_unique_buffer<uint8_t>(page.dictionary.size());
+        memcpy(dictionary.get(), page.dictionary.data(), 
page.dictionary.size());
+        status = decoder->set_dict(dictionary, 
static_cast<int32_t>(page.dictionary.size()),
+                                   page.dictionary_entries);
+        if (!status.ok()) {
+            state.SkipWithError(status.to_string().c_str());
+            return;
+        }
+    }
+
+    Slice encoded(page.data.data(), page.data.size());
+    FixedSink fixed_sink;
+    BinarySink binary_sink;
+    DictionarySink dictionary_sink(dictionary_for_sink.data(), 
page.value_width);
+    for (auto _ : state) {
+        state.PauseTiming();
+        status = decoder->set_data(&encoded);
+        fixed_sink.consumed = 0;
+        binary_sink.consumed = 0;
+        dictionary_sink.consumed = 0;
+        state.ResumeTiming();
+        if (scenario.encoding == Encoding::DICTIONARY) {
+            status = decoder->decode_selected_dictionary_values(selection, 
dictionary_sink);
+        } else if (page.binary) {
+            status = decoder->decode_selected_binary_values(selection, 
binary_sink);
+        } else {
+            status = decoder->decode_selected_fixed_values(selection, 
fixed_sink);
+        }
+        if (!status.ok()) {
+            state.SkipWithError(status.to_string().c_str());
+            break;
+        }
+        benchmark::ClobberMemory();
+    }
+
+    state.SetItemsProcessed(static_cast<int64_t>(state.iterations()) *
+                            static_cast<int64_t>(plan.selected_rows));
+    state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
+                            static_cast<int64_t>(page.data.size()));
+    state.counters["raw_rows"] = static_cast<double>(DECODER_ROWS);
+    state.counters["selected_rows"] = static_cast<double>(plan.selected_rows);
+    state.counters["selection_ranges"] = 
static_cast<double>(plan.ranges.size());
+    state.counters["encoded_bytes"] = static_cast<double>(page.data.size());
+    state.counters["ns/raw_row"] = benchmark::Counter(
+            static_cast<double>(DECODER_ROWS),
+            benchmark::Counter::kIsIterationInvariantRate | 
benchmark::Counter::kInvert);
+    if (plan.selected_rows > 0) {
+        state.counters["ns/selected_row"] = benchmark::Counter(
+                static_cast<double>(plan.selected_rows),
+                benchmark::Counter::kIsIterationInvariantRate | 
benchmark::Counter::kInvert);
+    }
+}
+
+inline bool register_decoder_benchmarks() {
+    for (const auto& scenario : decoder_scenarios()) {
+        for (const int selectivity : {1, 10, 50, 100}) {
+            for (const auto pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                const std::string name = "ParquetDecoder/" + 
to_string(scenario.encoding) + "/" +
+                                         to_string(scenario.value_type) + 
"/sel_" +
+                                         std::to_string(selectivity) + "/" + 
to_string(pattern);
+                benchmark::RegisterBenchmark(name.c_str(), 
[=](benchmark::State& state) {
+                    run_decoder(state, scenario, selectivity, pattern);
+                })->Unit(benchmark::kNanosecond);
+            }
+        }
+    }
+    return true;
+}
+
+inline const bool DECODER_BENCHMARKS_REGISTERED = 
register_decoder_benchmarks();
+
+} // namespace detail
+} // namespace doris::parquet_benchmark
diff --git a/be/benchmark/parquet/benchmark_parquet_reader.hpp 
b/be/benchmark/parquet/benchmark_parquet_reader.hpp
new file mode 100644
index 00000000000..ae3e14c0f66
--- /dev/null
+++ b/be/benchmark/parquet/benchmark_parquet_reader.hpp
@@ -0,0 +1,448 @@
+// 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.
+
+#pragma once
+
+#include <arrow/api.h>
+#include <arrow/io/api.h>
+#include <benchmark/benchmark.h>
+#include <parquet/api/reader.h>
+#include <parquet/arrow/writer.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <filesystem>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "exprs/vexpr.h"
+#include "exprs/vexpr_context.h"
+#include "format_v2/file_reader.h"
+#include "format_v2/parquet/parquet_reader.h"
+#include "gen_cpp/Types_types.h"
+#include "io/io_common.h"
+#include "parquet_benchmark_scenarios.h"
+#include "runtime/runtime_state.h"
+#include "storage/index/zone_map/zonemap_eval_context.h"
+#include "storage/index/zone_map/zonemap_filter_result.h"
+
+namespace doris::parquet_benchmark {
+namespace reader_detail {
+
+constexpr size_t READER_ROWS = 1UL << 14;
+constexpr size_t READER_ROW_GROUP_ROWS = 1UL << 12;
+
+inline void throw_if_error(const Status& status) {
+    if (!status.ok()) {
+        throw std::runtime_error(status.to_string());
+    }
+}
+
+inline bool is_null_row(size_t row, int null_percent, Pattern pattern) {
+    if (null_percent <= 0) {
+        return false;
+    }
+    if (pattern == Pattern::ALTERNATING) {
+        constexpr size_t NULL_PERIOD = 101;
+        const size_t null_slots = (static_cast<size_t>(null_percent) * 
NULL_PERIOD + 99) / 100;
+        return (row * 37) % NULL_PERIOD < null_slots;
+    }
+    constexpr size_t CLUSTER_ROWS = 1024;
+    return row % CLUSTER_ROWS < CLUSTER_ROWS * 
static_cast<size_t>(null_percent) / 100;
+}
+
+inline std::shared_ptr<arrow::Array> build_int32_array(int null_percent, 
Pattern pattern) {
+    arrow::Int32Builder builder;
+    PARQUET_THROW_NOT_OK(builder.Reserve(READER_ROWS));
+    for (size_t row = 0; row < READER_ROWS; ++row) {
+        if (is_null_row(row, null_percent, pattern)) {
+            PARQUET_THROW_NOT_OK(builder.AppendNull());
+        } else {
+            PARQUET_THROW_NOT_OK(builder.Append(static_cast<int32_t>(row % 
100)));
+        }
+    }
+    return builder.Finish().ValueOrDie();
+}
+
+inline ::parquet::Encoding::type file_encoding(Encoding encoding) {
+    switch (encoding) {
+    case Encoding::PLAIN:
+        return ::parquet::Encoding::PLAIN;
+    case Encoding::BYTE_STREAM_SPLIT:
+        return ::parquet::Encoding::BYTE_STREAM_SPLIT;
+    case Encoding::DELTA_BINARY_PACKED:
+        return ::parquet::Encoding::DELTA_BINARY_PACKED;
+    case Encoding::DICTIONARY:
+        return ::parquet::Encoding::RLE_DICTIONARY;
+    case Encoding::DELTA_LENGTH_BYTE_ARRAY:
+        return ::parquet::Encoding::DELTA_LENGTH_BYTE_ARRAY;
+    case Encoding::DELTA_BYTE_ARRAY:
+        return ::parquet::Encoding::DELTA_BYTE_ARRAY;
+    }
+    throw std::logic_error("unknown Parquet benchmark encoding");
+}
+
+inline std::string fixture_name(const ReaderScenario& scenario) {
+    return "v2_" + to_string(scenario.encoding) + "_null" + 
std::to_string(scenario.null_percent) +
+           "_" + to_string(scenario.null_pattern) + "_w" + 
std::to_string(scenario.schema_width) +
+           "_p" + std::to_string(scenario.predicate_position) + ".parquet";
+}
+
+inline void verify_fixture_encoding(const std::filesystem::path& path,
+                                    const ReaderScenario& scenario) {
+    auto reader = ::parquet::ParquetFileReader::OpenFile(path.string(), false);
+    const auto metadata = reader->metadata();
+    if (metadata->num_rows() != static_cast<int64_t>(READER_ROWS) ||
+        metadata->num_columns() != scenario.schema_width) {
+        throw std::runtime_error("Parquet benchmark fixture has unexpected 
shape: " +
+                                 path.string());
+    }
+    const auto expected = file_encoding(scenario.encoding);
+    for (int row_group = 0; row_group < metadata->num_row_groups(); 
++row_group) {
+        for (int column = 0; column < metadata->num_columns(); ++column) {
+            const auto encodings = 
metadata->RowGroup(row_group)->ColumnChunk(column)->encodings();
+            if (std::ranges::find(encodings, expected) == encodings.end()) {
+                throw std::runtime_error("Parquet benchmark fixture did not 
use " +
+                                         to_string(scenario.encoding) +
+                                         " encoding: " + path.string());
+            }
+        }
+    }
+}
+
+inline std::filesystem::path ensure_fixture(const ReaderScenario& scenario) {
+    static std::mutex fixture_mutex;
+    const auto directory =
+            std::filesystem::temp_directory_path() / 
"doris_parquet_reader_benchmark";
+    const auto path = directory / fixture_name(scenario);
+    std::lock_guard guard(fixture_mutex);
+    if (std::filesystem::exists(path)) {
+        verify_fixture_encoding(path, scenario);
+        return path;
+    }
+
+    std::filesystem::create_directories(directory);
+    const auto temporary_path = path.string() + ".tmp";
+    std::filesystem::remove(temporary_path);
+    const auto values = build_int32_array(scenario.null_percent, 
scenario.null_pattern);
+    std::vector<std::shared_ptr<arrow::Field>> fields;
+    std::vector<std::shared_ptr<arrow::ChunkedArray>> columns;
+    fields.reserve(scenario.schema_width);
+    columns.reserve(scenario.schema_width);
+    for (int column = 0; column < scenario.schema_width; ++column) {
+        fields.push_back(arrow::field("c" + std::to_string(column), 
arrow::int32(), true));
+        columns.push_back(std::make_shared<arrow::ChunkedArray>(values));
+    }
+    const auto table = arrow::Table::Make(arrow::schema(std::move(fields)), 
std::move(columns));
+
+    const auto output_result = 
arrow::io::FileOutputStream::Open(temporary_path);
+    if (!output_result.ok()) {
+        throw std::runtime_error(output_result.status().ToString());
+    }
+    const auto output = *output_result;
+    ::parquet::WriterProperties::Builder properties;
+    properties.version(::parquet::ParquetVersion::PARQUET_2_6);
+    properties.data_page_version(::parquet::ParquetDataPageVersion::V2);
+    properties.compression(::parquet::Compression::UNCOMPRESSED);
+    properties.disable_statistics();
+    if (scenario.encoding == Encoding::DICTIONARY) {
+        properties.enable_dictionary();
+    } else {
+        properties.disable_dictionary();
+        properties.encoding(file_encoding(scenario.encoding));
+    }
+    PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, 
arrow::default_memory_pool(), output,
+                                                      READER_ROW_GROUP_ROWS, 
properties.build()));
+    PARQUET_THROW_NOT_OK(output->Close());
+    std::filesystem::rename(temporary_path, path);
+    verify_fixture_encoding(path, scenario);
+    return path;
+}
+
+class Int32LessThanExpr final : public VExpr {
+public:
+    Int32LessThanExpr(int column_id, int32_t upper_bound)
+            : VExpr(std::make_shared<DataTypeUInt8>(), false),
+              _column_id(column_id),
+              _upper_bound(upper_bound) {}
+
+    Status execute_column_impl(VExprContext*, const Block* block, const 
Selector* selector,
+                               size_t count, ColumnPtr& result_column) const 
override {
+        DORIS_CHECK(block != nullptr);
+        const auto& nullable =
+                assert_cast<const 
ColumnNullable&>(*block->get_by_position(_column_id).column);
+        const auto& values = assert_cast<const 
ColumnInt32&>(nullable.get_nested_column());
+        const auto& nulls = nullable.get_null_map_data();
+        auto result = ColumnUInt8::create(count, 0);
+        auto& matches = result->get_data();
+        for (size_t row = 0; row < count; ++row) {
+            const size_t input_row = selector == nullptr ? row : 
(*selector)[row];
+            matches[row] = !nulls[input_row] && values.get_element(input_row) 
< _upper_bound;
+        }
+        result_column = std::move(result);
+        return Status::OK();
+    }
+
+    bool can_execute_on_raw_fixed_values(const DataTypePtr& data_type,
+                                         int column_id) const override {
+        return column_id == _column_id &&
+               remove_nullable(data_type)->get_primitive_type() == TYPE_INT;
+    }
+
+    Status execute_on_raw_fixed_values(const uint8_t* values, size_t 
num_values, size_t value_width,
+                                       const DataTypePtr&, int column_id,
+                                       uint8_t* matches) const override {
+        DORIS_CHECK(column_id == _column_id);
+        DORIS_CHECK(value_width == sizeof(int32_t));
+        const auto* typed_values = reinterpret_cast<const int32_t*>(values);
+        for (size_t row = 0; row < num_values; ++row) {
+            matches[row] &= typed_values[row] < _upper_bound;
+        }
+        return Status::OK();
+    }
+
+    bool can_evaluate_zonemap_filter() const override { return true; }
+
+    ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) 
const override {
+        const auto zone_map = ctx.zone_map(_column_id);
+        if (zone_map == nullptr) {
+            return unsupported_zonemap_filter(ctx);
+        }
+        if (!zone_map->has_not_null) {
+            return ZoneMapFilterResult::kNoMatch;
+        }
+        const auto upper_bound = Field::create_field<TYPE_INT>(_upper_bound);
+        return zone_map->min_value >= upper_bound ? 
ZoneMapFilterResult::kNoMatch
+                                                  : 
ZoneMapFilterResult::kMayMatch;
+    }
+
+    bool can_evaluate_dictionary_filter() const override { return true; }
+
+    ZoneMapFilterResult evaluate_dictionary_filter(
+            const DictionaryEvalContext& ctx) const override {
+        const auto* dictionary = ctx.slot(_column_id);
+        if (dictionary == nullptr) {
+            return ZoneMapFilterResult::kUnsupported;
+        }
+        const auto upper_bound = Field::create_field<TYPE_INT>(_upper_bound);
+        return std::ranges::any_of(dictionary->values,
+                                   [&](const Field& value) { return value < 
upper_bound; })
+                       ? ZoneMapFilterResult::kMayMatch
+                       : ZoneMapFilterResult::kNoMatch;
+    }
+
+    void collect_slot_column_ids(std::set<int>& column_ids) const override {
+        column_ids.insert(_column_id);
+    }
+
+    const std::string& expr_name() const override { return _expr_name; }
+
+private:
+    const int _column_id;
+    const int32_t _upper_bound;
+    const std::string _expr_name = "ParquetBenchmarkInt32LessThan";
+};
+
+inline VExprContextSPtr make_predicate(int column_position, int 
selectivity_percent) {
+    auto context = VExprContext::create_shared(
+            std::make_shared<Int32LessThanExpr>(column_position, 
selectivity_percent));
+    context->_prepared = true;
+    context->_opened = true;
+    return context;
+}
+
+inline Block make_block(const std::vector<format::ColumnDefinition>& schema) {
+    Block block;
+    for (const auto& column : schema) {
+        block.insert({column.type->create_column(), column.type, column.name});
+    }
+    return block;
+}
+
+struct ReaderSession {
+    RuntimeState runtime_state {TQueryOptions(), TQueryGlobals()};
+    std::unique_ptr<format::parquet::ParquetReader> reader;
+    std::vector<format::ColumnDefinition> schema;
+    std::shared_ptr<format::FileScanRequest> request;
+};
+
+inline std::unique_ptr<ReaderSession> open_reader(const std::filesystem::path& 
path,
+                                                  const ReaderScenario& 
scenario) {
+    auto session = std::make_unique<ReaderSession>();
+    auto properties = std::make_shared<io::FileSystemProperties>();
+    properties->system_type = TFileType::FILE_LOCAL;
+    auto description = std::make_unique<io::FileDescription>();
+    description->path = path.string();
+    description->file_size = 
static_cast<int64_t>(std::filesystem::file_size(path));
+    description->range_start_offset = 0;
+    description->range_size = -1;
+    session->reader = 
std::make_unique<format::parquet::ParquetReader>(properties, description,
+                                                                       
nullptr, nullptr);
+    throw_if_error(session->reader->init(&session->runtime_state));
+    throw_if_error(session->reader->get_schema(&session->schema));
+
+    session->request = std::make_shared<format::FileScanRequest>();
+    format::FileScanRequestBuilder request_builder(session->request.get());
+    if (scenario.operation == ReaderOperation::FULL_SCAN) {
+        for (int column = 0; column < scenario.schema_width; ++column) {
+            
throw_if_error(request_builder.add_non_predicate_column(format::LocalColumnId(column)));
+        }
+    } else if (scenario.operation == ReaderOperation::PREDICATE_SCAN) {
+        const auto predicate_id = 
format::LocalColumnId(scenario.predicate_position);
+        throw_if_error(request_builder.add_predicate_column(predicate_id));
+        if (scenario.projection == Projection::PREDICATE_ONLY) {
+            session->request->predicate_only_columns.push_back(predicate_id);
+        } else {
+            const int payload = scenario.predicate_position == 0 ? 
scenario.schema_width - 1 : 0;
+            throw_if_error(
+                    
request_builder.add_non_predicate_column(format::LocalColumnId(payload)));
+        }
+        const auto predicate_position = 
session->request->local_positions.at(predicate_id).value();
+        session->request->conjuncts.push_back(
+                make_predicate(static_cast<int>(predicate_position), 
scenario.selectivity_percent));
+    } else {
+        
throw_if_error(request_builder.add_non_predicate_column(format::LocalColumnId(0)));
+        if (scenario.schema_width > 1) {
+            
throw_if_error(request_builder.add_non_predicate_column(format::LocalColumnId(1)));
+        }
+    }
+
+    if (scenario.operation == ReaderOperation::LIMIT_1) {
+        session->reader->set_batch_size(1);
+    } else if (scenario.operation == ReaderOperation::LIMIT_1000) {
+        session->reader->set_batch_size(1000);
+    }
+    throw_if_error(session->reader->open(session->request));
+    return session;
+}
+
+inline size_t scan_reader(ReaderSession* session, const ReaderScenario& 
scenario) {
+    size_t output_rows = 0;
+    bool eof = false;
+    const size_t limit = scenario.operation == ReaderOperation::LIMIT_1      ? 
1
+                         : scenario.operation == ReaderOperation::LIMIT_1000 ? 
1000
+                                                                             : 
0;
+    while (!eof) {
+        auto block = make_block(session->schema);
+        size_t rows = 0;
+        throw_if_error(session->reader->get_block(&block, &rows, &eof));
+        output_rows += rows;
+        benchmark::DoNotOptimize(block);
+        if (scenario.operation == ReaderOperation::OPEN_TO_FIRST_BLOCK) {
+            break;
+        }
+        if (limit != 0 && output_rows >= limit) {
+            break;
+        }
+    }
+    return output_rows;
+}
+
+inline size_t raw_rows_per_iteration(const ReaderScenario& scenario) {
+    if (scenario.operation == ReaderOperation::LIMIT_1) {
+        return 1;
+    }
+    if (scenario.operation == ReaderOperation::LIMIT_1000) {
+        return 1000;
+    }
+    if (scenario.operation == ReaderOperation::OPEN_TO_FIRST_BLOCK) {
+        return format::parquet::ParquetScanScheduler::DEFAULT_READ_BATCH_SIZE;
+    }
+    return READER_ROWS;
+}
+
+inline int projected_columns(const ReaderScenario& scenario) {
+    if (scenario.operation == ReaderOperation::FULL_SCAN) {
+        return scenario.schema_width;
+    }
+    if (scenario.operation == ReaderOperation::PREDICATE_SCAN &&
+        scenario.projection == Projection::PREDICATE_ONLY) {
+        return 1;
+    }
+    return std::min(2, scenario.schema_width);
+}
+
+inline void run_reader(benchmark::State& state, ReaderScenario scenario) {
+    std::filesystem::path fixture;
+    try {
+        fixture = ensure_fixture(scenario);
+        size_t selected_rows = 0;
+        for (auto _ : state) {
+            if (scenario.operation != ReaderOperation::OPEN_TO_FIRST_BLOCK) {
+                state.PauseTiming();
+            }
+            auto session = open_reader(fixture, scenario);
+            if (scenario.operation != ReaderOperation::OPEN_TO_FIRST_BLOCK) {
+                state.ResumeTiming();
+            }
+            selected_rows = scan_reader(session.get(), scenario);
+            state.PauseTiming();
+            throw_if_error(session->reader->close());
+            state.ResumeTiming();
+            benchmark::ClobberMemory();
+        }
+
+        const auto raw_rows = raw_rows_per_iteration(scenario);
+        state.SetItemsProcessed(static_cast<int64_t>(state.iterations() * 
selected_rows));
+        state.SetBytesProcessed(static_cast<int64_t>(
+                state.iterations() * raw_rows * projected_columns(scenario) * 
sizeof(int32_t)));
+        state.counters["raw_rows"] = static_cast<double>(raw_rows);
+        state.counters["selected_rows"] = static_cast<double>(selected_rows);
+        state.counters["fixture_bytes"] = 
static_cast<double>(std::filesystem::file_size(fixture));
+        state.counters["ns/raw_row"] = benchmark::Counter(
+                static_cast<double>(raw_rows),
+                benchmark::Counter::kIsIterationInvariantRate | 
benchmark::Counter::kInvert);
+        if (selected_rows > 0) {
+            state.counters["ns/selected_row"] = benchmark::Counter(
+                    static_cast<double>(selected_rows),
+                    benchmark::Counter::kIsIterationInvariantRate | 
benchmark::Counter::kInvert);
+        }
+    } catch (const std::exception& error) {
+        state.SkipWithError(error.what());
+    }
+}
+
+inline bool register_reader_benchmarks() {
+    for (const auto& scenario : reader_scenarios()) {
+        std::string name =
+                "ParquetReader/" + to_string(scenario.operation) + "/" +
+                to_string(scenario.encoding) + "/null_" + 
std::to_string(scenario.null_percent) +
+                "/" + to_string(scenario.null_pattern) + "/sel_" +
+                std::to_string(scenario.selectivity_percent) + "/" +
+                to_string(scenario.projection) + "/width_" + 
std::to_string(scenario.schema_width) +
+                "/predicate_" + std::to_string(scenario.predicate_position);
+        benchmark::RegisterBenchmark(name.c_str(), [=](benchmark::State& 
state) {
+            run_reader(state, scenario);
+        })->Unit(benchmark::kNanosecond);
+    }
+    return true;
+}
+
+inline const bool READER_BENCHMARKS_REGISTERED = register_reader_benchmarks();
+
+} // namespace reader_detail
+} // namespace doris::parquet_benchmark
diff --git a/be/benchmark/parquet/parquet_benchmark_scenarios.h 
b/be/benchmark/parquet/parquet_benchmark_scenarios.h
new file mode 100644
index 00000000000..d420d9ccdd5
--- /dev/null
+++ b/be/benchmark/parquet/parquet_benchmark_scenarios.h
@@ -0,0 +1,250 @@
+// 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.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <set>
+#include <string>
+#include <tuple>
+#include <vector>
+
+namespace doris::parquet_benchmark {
+
+enum class Encoding {
+    PLAIN,
+    DICTIONARY,
+    BYTE_STREAM_SPLIT,
+    DELTA_BINARY_PACKED,
+    DELTA_LENGTH_BYTE_ARRAY,
+    DELTA_BYTE_ARRAY
+};
+enum class ValueType { INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY, 
FIXED_LEN_BYTE_ARRAY };
+enum class Pattern { CLUSTERED, ALTERNATING };
+enum class Projection { PREDICATE_ONLY, PREDICATE_PROJECTED };
+enum class ReaderOperation { OPEN_TO_FIRST_BLOCK, FULL_SCAN, PREDICATE_SCAN, 
LIMIT_1, LIMIT_1000 };
+
+struct DecoderScenario {
+    Encoding encoding;
+    ValueType value_type;
+};
+
+struct ReaderScenario {
+    ReaderOperation operation;
+    Encoding encoding;
+    int null_percent;
+    Pattern null_pattern;
+    int selectivity_percent;
+    Projection projection;
+    int schema_width;
+    int predicate_position;
+};
+
+struct SelectionRange {
+    size_t first;
+    size_t count;
+};
+
+struct SelectionPlan {
+    size_t total_rows = 0;
+    size_t selected_rows = 0;
+    std::vector<SelectionRange> ranges;
+};
+
+inline std::vector<DecoderScenario> decoder_scenarios() {
+    return {
+            {Encoding::PLAIN, ValueType::INT32},
+            {Encoding::PLAIN, ValueType::INT64},
+            {Encoding::PLAIN, ValueType::FLOAT},
+            {Encoding::PLAIN, ValueType::DOUBLE},
+            {Encoding::PLAIN, ValueType::BYTE_ARRAY},
+            {Encoding::PLAIN, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::DICTIONARY, ValueType::INT32},
+            {Encoding::DICTIONARY, ValueType::INT64},
+            {Encoding::DICTIONARY, ValueType::FLOAT},
+            {Encoding::DICTIONARY, ValueType::DOUBLE},
+            {Encoding::DICTIONARY, ValueType::BYTE_ARRAY},
+            {Encoding::DICTIONARY, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::FLOAT},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::DOUBLE},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::DELTA_BINARY_PACKED, ValueType::INT32},
+            {Encoding::DELTA_BINARY_PACKED, ValueType::INT64},
+            {Encoding::DELTA_LENGTH_BYTE_ARRAY, ValueType::BYTE_ARRAY},
+            {Encoding::DELTA_BYTE_ARRAY, ValueType::BYTE_ARRAY},
+    };
+}
+
+inline std::vector<ReaderScenario> reader_scenarios() {
+    std::vector<ReaderScenario> scenarios;
+    std::set<std::tuple<ReaderOperation, Encoding, int, Pattern, int, 
Projection, int, int>> seen;
+    const auto add = [&](ReaderScenario scenario) {
+        const auto key = std::make_tuple(scenario.operation, scenario.encoding,
+                                         scenario.null_percent, 
scenario.null_pattern,
+                                         scenario.selectivity_percent, 
scenario.projection,
+                                         scenario.schema_width, 
scenario.predicate_position);
+        if (seen.insert(key).second) {
+            scenarios.push_back(scenario);
+        }
+    };
+
+    const ReaderScenario baseline {.operation = ReaderOperation::FULL_SCAN,
+                                   .encoding = Encoding::PLAIN,
+                                   .null_percent = 10,
+                                   .null_pattern = Pattern::ALTERNATING,
+                                   .selectivity_percent = 10,
+                                   .projection = 
Projection::PREDICATE_PROJECTED,
+                                   .schema_width = 32,
+                                   .predicate_position = 0};
+    for (const auto operation :
+         {ReaderOperation::OPEN_TO_FIRST_BLOCK, ReaderOperation::FULL_SCAN,
+          ReaderOperation::PREDICATE_SCAN, ReaderOperation::LIMIT_1, 
ReaderOperation::LIMIT_1000}) {
+        auto scenario = baseline;
+        scenario.operation = operation;
+        add(scenario);
+    }
+    for (const auto encoding : {Encoding::PLAIN, Encoding::DICTIONARY, 
Encoding::BYTE_STREAM_SPLIT,
+                                Encoding::DELTA_BINARY_PACKED}) {
+        auto scenario = baseline;
+        scenario.encoding = encoding;
+        add(scenario);
+        scenario.operation = ReaderOperation::PREDICATE_SCAN;
+        add(scenario);
+    }
+    for (const int width : {4, 32, 128, 512}) {
+        for (const int predicate_position : {0, width - 1}) {
+            auto scenario = baseline;
+            scenario.operation = ReaderOperation::PREDICATE_SCAN;
+            scenario.schema_width = width;
+            scenario.predicate_position = predicate_position;
+            add(scenario);
+        }
+    }
+    for (const int null_percent : {0, 1, 10, 50, 90}) {
+        for (const auto pattern : {Pattern::CLUSTERED, Pattern::ALTERNATING}) {
+            for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+                for (const auto projection :
+                     {Projection::PREDICATE_ONLY, 
Projection::PREDICATE_PROJECTED}) {
+                    auto scenario = baseline;
+                    scenario.operation = ReaderOperation::PREDICATE_SCAN;
+                    scenario.null_percent = null_percent;
+                    scenario.null_pattern = pattern;
+                    scenario.selectivity_percent = selectivity;
+                    scenario.projection = projection;
+                    add(scenario);
+                }
+            }
+        }
+    }
+    return scenarios;
+}
+
+inline SelectionPlan make_selection_plan(size_t total_rows, int 
selectivity_percent,
+                                         Pattern pattern) {
+    SelectionPlan plan {.total_rows = total_rows, .selected_rows = 0, .ranges 
= {}};
+    if (total_rows == 0 || selectivity_percent <= 0) {
+        return plan;
+    }
+    if (selectivity_percent >= 100) {
+        plan.selected_rows = total_rows;
+        plan.ranges.push_back({.first = 0, .count = total_rows});
+        return plan;
+    }
+    plan.selected_rows = total_rows * static_cast<size_t>(selectivity_percent) 
/ 100;
+    if (plan.selected_rows == 0) {
+        plan.selected_rows = 1;
+    }
+    if (pattern == Pattern::CLUSTERED) {
+        plan.ranges.push_back({.first = 0, .count = plan.selected_rows});
+        return plan;
+    }
+
+    // Evenly spaced rows deliberately maximize the number of physical ranges. 
This is the
+    // adversarial sparse shape that exposes per-run decoder and cursor 
overhead.
+    for (size_t selected = 0; selected < plan.selected_rows; ++selected) {
+        const size_t row = selected * total_rows / plan.selected_rows;
+        if (!plan.ranges.empty() && plan.ranges.back().first + 
plan.ranges.back().count == row) {
+            ++plan.ranges.back().count;
+        } else {
+            plan.ranges.push_back({.first = row, .count = 1});
+        }
+    }
+    return plan;
+}
+
+inline std::string to_string(Encoding value) {
+    switch (value) {
+    case Encoding::PLAIN:
+        return "plain";
+    case Encoding::DICTIONARY:
+        return "dictionary";
+    case Encoding::BYTE_STREAM_SPLIT:
+        return "byte_stream_split";
+    case Encoding::DELTA_BINARY_PACKED:
+        return "delta_binary_packed";
+    case Encoding::DELTA_LENGTH_BYTE_ARRAY:
+        return "delta_length_byte_array";
+    case Encoding::DELTA_BYTE_ARRAY:
+        return "delta_byte_array";
+    }
+    return "unknown";
+}
+
+inline std::string to_string(ValueType value) {
+    switch (value) {
+    case ValueType::INT32:
+        return "int32";
+    case ValueType::INT64:
+        return "int64";
+    case ValueType::FLOAT:
+        return "float";
+    case ValueType::DOUBLE:
+        return "double";
+    case ValueType::BYTE_ARRAY:
+        return "byte_array";
+    case ValueType::FIXED_LEN_BYTE_ARRAY:
+        return "fixed_len_byte_array";
+    }
+    return "unknown";
+}
+
+inline std::string to_string(Pattern value) {
+    return value == Pattern::CLUSTERED ? "clustered" : "alternating";
+}
+
+inline std::string to_string(Projection value) {
+    return value == Projection::PREDICATE_ONLY ? "predicate_only" : 
"predicate_projected";
+}
+
+inline std::string to_string(ReaderOperation value) {
+    switch (value) {
+    case ReaderOperation::OPEN_TO_FIRST_BLOCK:
+        return "open_to_first_block";
+    case ReaderOperation::FULL_SCAN:
+        return "full_scan";
+    case ReaderOperation::PREDICATE_SCAN:
+        return "predicate_scan";
+    case ReaderOperation::LIMIT_1:
+        return "limit_1";
+    case ReaderOperation::LIMIT_1000:
+        return "limit_1000";
+    }
+    return "unknown";
+}
+
+} // namespace doris::parquet_benchmark
diff --git a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp 
b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
new file mode 100644
index 00000000000..c62d3566e15
--- /dev/null
+++ b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
@@ -0,0 +1,129 @@
+// 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.
+
+#include "../../../benchmark/parquet/parquet_benchmark_scenarios.h"
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <array>
+#include <set>
+#include <tuple>
+
+namespace doris::parquet_benchmark {
+namespace {
+
+TEST(ParquetBenchmarkScenariosTest, 
DecoderMatrixCoversNativeEncodingAndTypeFamilies) {
+    const auto scenarios = decoder_scenarios();
+    const std::set<std::pair<Encoding, ValueType>> actual = [&] {
+        std::set<std::pair<Encoding, ValueType>> values;
+        for (const auto& scenario : scenarios) {
+            values.emplace(scenario.encoding, scenario.value_type);
+        }
+        return values;
+    }();
+
+    const std::set<std::pair<Encoding, ValueType>> expected {
+            {Encoding::PLAIN, ValueType::INT32},
+            {Encoding::PLAIN, ValueType::INT64},
+            {Encoding::PLAIN, ValueType::FLOAT},
+            {Encoding::PLAIN, ValueType::DOUBLE},
+            {Encoding::PLAIN, ValueType::BYTE_ARRAY},
+            {Encoding::PLAIN, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::DICTIONARY, ValueType::INT32},
+            {Encoding::DICTIONARY, ValueType::INT64},
+            {Encoding::DICTIONARY, ValueType::FLOAT},
+            {Encoding::DICTIONARY, ValueType::DOUBLE},
+            {Encoding::DICTIONARY, ValueType::BYTE_ARRAY},
+            {Encoding::DICTIONARY, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::FLOAT},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::DOUBLE},
+            {Encoding::BYTE_STREAM_SPLIT, ValueType::FIXED_LEN_BYTE_ARRAY},
+            {Encoding::DELTA_BINARY_PACKED, ValueType::INT32},
+            {Encoding::DELTA_BINARY_PACKED, ValueType::INT64},
+            {Encoding::DELTA_LENGTH_BYTE_ARRAY, ValueType::BYTE_ARRAY},
+            {Encoding::DELTA_BYTE_ARRAY, ValueType::BYTE_ARRAY},
+    };
+    EXPECT_EQ(actual, expected);
+}
+
+TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixCoversNullableSparseAndProjectionAxes) {
+    const auto scenarios = reader_scenarios();
+    for (const int null_percent : {0, 1, 10, 50, 90}) {
+        for (const auto pattern : {Pattern::CLUSTERED, Pattern::ALTERNATING}) {
+            for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+                for (const auto projection :
+                     {Projection::PREDICATE_ONLY, 
Projection::PREDICATE_PROJECTED}) {
+                    EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const 
ReaderScenario& scenario) {
+                        return scenario.operation == 
ReaderOperation::PREDICATE_SCAN &&
+                               scenario.encoding == Encoding::PLAIN &&
+                               scenario.null_percent == null_percent &&
+                               scenario.null_pattern == pattern &&
+                               scenario.selectivity_percent == selectivity &&
+                               scenario.projection == projection && 
scenario.schema_width == 32;
+                    })) << "missing nullable sparse axis combination";
+                }
+            }
+        }
+    }
+}
+
+TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixCoversOperationsEncodingsAndSchemaWidths) {
+    const auto scenarios = reader_scenarios();
+    for (const auto operation :
+         {ReaderOperation::OPEN_TO_FIRST_BLOCK, ReaderOperation::FULL_SCAN,
+          ReaderOperation::PREDICATE_SCAN, ReaderOperation::LIMIT_1, 
ReaderOperation::LIMIT_1000}) {
+        EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const ReaderScenario& 
scenario) {
+            return scenario.operation == operation;
+        }));
+    }
+    for (const auto encoding : {Encoding::PLAIN, Encoding::DICTIONARY, 
Encoding::BYTE_STREAM_SPLIT,
+                                Encoding::DELTA_BINARY_PACKED}) {
+        for (const auto operation : {ReaderOperation::FULL_SCAN, 
ReaderOperation::PREDICATE_SCAN}) {
+            EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const 
ReaderScenario& scenario) {
+                return scenario.encoding == encoding && scenario.operation == 
operation;
+            }));
+        }
+    }
+    for (const int width : {4, 32, 128, 512}) {
+        for (const int predicate_position : {0, width - 1}) {
+            EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const 
ReaderScenario& scenario) {
+                return scenario.schema_width == width &&
+                       scenario.predicate_position == predicate_position;
+            }));
+        }
+    }
+}
+
+TEST(ParquetBenchmarkScenariosTest, 
SelectionPlanDistinguishesClusteredAndSparseRuns) {
+    const auto clustered = make_selection_plan(1000, 10, Pattern::CLUSTERED);
+    EXPECT_EQ(clustered.total_rows, 1000);
+    EXPECT_EQ(clustered.selected_rows, 100);
+    ASSERT_EQ(clustered.ranges.size(), 1);
+    EXPECT_EQ(clustered.ranges.front().count, 100);
+
+    const auto alternating = make_selection_plan(1000, 10, 
Pattern::ALTERNATING);
+    EXPECT_EQ(alternating.total_rows, 1000);
+    EXPECT_EQ(alternating.selected_rows, 100);
+    EXPECT_EQ(alternating.ranges.size(), 100);
+
+    EXPECT_EQ(make_selection_plan(1000, 0, 
Pattern::ALTERNATING).ranges.size(), 0);
+    EXPECT_EQ(make_selection_plan(1000, 100, 
Pattern::ALTERNATING).ranges.size(), 1);
+}
+
+} // namespace
+} // namespace doris::parquet_benchmark


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to