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

Fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ab0dd07a GH-3530: Optimize BYTE_STREAM_SPLIT (#3569)
2ab0dd07a is described below

commit 2ab0dd07a0df48fced918f9498fcb02db5f10359
Author: Ismaël Mejía <[email protected]>
AuthorDate: Tue Aug 11 22:36:25 2026 +0200

    GH-3530: Optimize BYTE_STREAM_SPLIT (#3569)
    
    * GH-3530: Optimize BYTE_STREAM_SPLIT encoding/decoding
    
    Reader: replace generic ByteBuffer.get() transpose loop in decodeData()
    with specialized single-pass loops for element sizes 2/4/8/12/16 bytes
    plus a stream-oriented generic fallback. Bulk-access the backing array
    directly when available, falling back to a single bulk copy for direct
    buffers.
    
    Writer: replace per-value scatterBytes() (which allocates a temp byte[]
    and issues N single-byte stream writes) with batched scatter buffers.
    Int/Long values accumulate in int[]/long[] batches of 64 and flush as
    bulk write(byte[], off, len) calls -- one per stream. FLBA uses
    per-stream byte[][] scratch buffers with the same batching strategy.
    getBufferedSize() now accounts for unflushed batch values.
    
    Add JMH benchmarks for scalar encode/decode of all 5 BSS types (FLOAT,
    DOUBLE, INT32, INT64, FIXED_LEN_BYTE_ARRAY). Add TestDataFactory for
    deterministic FLBA benchmark data generation. Add unit tests for
    transpose specializations, batch-boundary crossing, getBufferedSize
    with partial batches, direct ByteBuffer decode paths, and close/reset
    with pending unflushed batches.
    
    * GH-3569: Address review comments on ByteStreamSplit
    
    - Use exact assert (== instead of >=) for encoded.remaining() check
    - Extract package-private writeToStream() helper used consistently by
      all three flush methods (flushIntBatch, flushLongBatch, flushFlbaBatch)
    - Add else-throw in flushBatch() for defensive fail-fast when batchCount
      > 0 but no batch buffer is initialized
    
    Assisted-by: GitHub Copilot:claude-opus-4.6
---
 .../ByteStreamSplitDecodingBenchmark.java          | 273 ++++++++++++
 .../ByteStreamSplitEncodingBenchmark.java          | 166 ++++++++
 .../ByteStreamSplitValuesReader.java               | 137 +++++-
 .../ByteStreamSplitValuesWriter.java               | 152 ++++++-
 .../ByteStreamSplitScalarOptTest.java              | 462 +++++++++++++++++++++
 pom.xml                                            |   1 +
 6 files changed, 1168 insertions(+), 23 deletions(-)

diff --git 
a/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitDecodingBenchmark.java
 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitDecodingBenchmark.java
new file mode 100644
index 000000000..f0eeca440
--- /dev/null
+++ 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitDecodingBenchmark.java
@@ -0,0 +1,273 @@
+/*
+ * 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.
+ */
+package org.apache.parquet.benchmarks;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.column.values.ValuesWriter;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReader;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReaderForDouble;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReaderForFLBA;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReaderForFloat;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReaderForInteger;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReaderForLong;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesWriter;
+import org.apache.parquet.io.api.Binary;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OperationsPerInvocation;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Decoding-level micro-benchmarks for the BYTE_STREAM_SPLIT encoding across 
all
+ * Parquet types that support it: {@code FLOAT}, {@code DOUBLE}, {@code INT32},
+ * {@code INT64}, and {@code FIXED_LEN_BYTE_ARRAY}.
+ *
+ * <p>Fixed-width numeric types are benchmarked directly by top-level methods.
+ * {@code FIXED_LEN_BYTE_ARRAY} uses an inner {@link FlbaState} parameterised 
by
+ * {@code fixedLength} to avoid cross-product pollution with the numeric 
benchmarks.
+ *
+ * <p>Each invocation decodes {@value #VALUE_COUNT} values; throughput is 
reported
+ * per-value via {@link OperationsPerInvocation}. The cost includes both
+ * {@code initFromPage} (which eagerly transposes the entire page) and the 
per-value
+ * read calls. Page transposition is the part this benchmark is primarily 
designed
+ * to exercise.
+ */
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class ByteStreamSplitDecodingBenchmark {
+
+  static final int VALUE_COUNT = 100_000;
+  private static final int INIT_SLAB_SIZE = 64 * 1024;
+  private static final int PAGE_SIZE = 4 * 1024 * 1024;
+
+  private byte[] floatPage;
+  private byte[] doublePage;
+  private byte[] intPage;
+  private byte[] longPage;
+
+  @Setup(Level.Trial)
+  public void setup() throws IOException {
+    Random random = new Random(42);
+    int[] intData = new int[VALUE_COUNT];
+    long[] longData = new long[VALUE_COUNT];
+    float[] floatData = new float[VALUE_COUNT];
+    double[] doubleData = new double[VALUE_COUNT];
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      intData[i] = random.nextInt();
+      longData[i] = random.nextLong();
+      floatData[i] = random.nextFloat();
+      doubleData[i] = random.nextDouble();
+    }
+
+    {
+      ValuesWriter w = new 
ByteStreamSplitValuesWriter.FloatByteStreamSplitValuesWriter(
+          INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+      for (float v : floatData) {
+        w.writeFloat(v);
+      }
+      floatPage = w.getBytes().toByteArray();
+      w.close();
+    }
+    {
+      ValuesWriter w = new 
ByteStreamSplitValuesWriter.DoubleByteStreamSplitValuesWriter(
+          INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+      for (double v : doubleData) {
+        w.writeDouble(v);
+      }
+      doublePage = w.getBytes().toByteArray();
+      w.close();
+    }
+    {
+      ValuesWriter w = new 
ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter(
+          INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+      for (int v : intData) {
+        w.writeInteger(v);
+      }
+      intPage = w.getBytes().toByteArray();
+      w.close();
+    }
+    {
+      ValuesWriter w = new 
ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+          INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+      for (long v : longData) {
+        w.writeLong(v);
+      }
+      longPage = w.getBytes().toByteArray();
+      w.close();
+    }
+  }
+
+  private static void init(ByteStreamSplitValuesReader r, byte[] page) throws 
IOException {
+    r.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(page)));
+  }
+
+  private static void initDirect(ByteStreamSplitValuesReader r, ByteBuffer 
directPage) throws IOException {
+    directPage.clear(); // reset position to 0 for each invocation
+    r.initFromPage(VALUE_COUNT, ByteBufferInputStream.wrap(directPage));
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeFloat(Blackhole bh) throws IOException {
+    ByteStreamSplitValuesReaderForFloat r = new 
ByteStreamSplitValuesReaderForFloat();
+    init(r, floatPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readFloat());
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeDouble(Blackhole bh) throws IOException {
+    ByteStreamSplitValuesReaderForDouble r = new 
ByteStreamSplitValuesReaderForDouble();
+    init(r, doublePage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readDouble());
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeInt(Blackhole bh) throws IOException {
+    ByteStreamSplitValuesReaderForInteger r = new 
ByteStreamSplitValuesReaderForInteger();
+    init(r, intPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readInteger());
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeLong(Blackhole bh) throws IOException {
+    ByteStreamSplitValuesReaderForLong r = new 
ByteStreamSplitValuesReaderForLong();
+    init(r, longPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readLong());
+    }
+  }
+
+  // ---- FIXED_LEN_BYTE_ARRAY (parameterised by fixedLength) ----
+
+  @State(Scope.Thread)
+  public static class FlbaState {
+    @Param({"2", "7", "12", "16"})
+    public int fixedLength;
+
+    byte[] flbaPage;
+
+    @Setup(Level.Trial)
+    public void setup() throws IOException {
+      Binary[] data = TestDataFactory.generateFixedLenByteArrays(
+          VALUE_COUNT, fixedLength, 0, TestDataFactory.DEFAULT_SEED);
+      ValuesWriter w = new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+          fixedLength, INIT_SLAB_SIZE, PAGE_SIZE, new 
HeapByteBufferAllocator());
+      for (Binary v : data) {
+        w.writeBytes(v);
+      }
+      flbaPage = w.getBytes().toByteArray();
+      w.close();
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeFlba(FlbaState state, Blackhole bh) throws IOException {
+    ByteStreamSplitValuesReaderForFLBA r = new 
ByteStreamSplitValuesReaderForFLBA(state.fixedLength);
+    init(r, state.flbaPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readBytes());
+    }
+  }
+
+  // ---- Direct ByteBuffer decode (exercises the !hasArray() path in 
decodeData) ----
+
+  @State(Scope.Thread)
+  public static class DirectBufferState {
+    ByteBuffer directFloatPage;
+    ByteBuffer directLongPage;
+
+    @Setup(Level.Trial)
+    public void setup() throws IOException {
+      Random random = new Random(42);
+      {
+        ValuesWriter w = new 
ByteStreamSplitValuesWriter.FloatByteStreamSplitValuesWriter(
+            INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+        for (int i = 0; i < VALUE_COUNT; i++) {
+          w.writeFloat(random.nextFloat());
+        }
+        byte[] page = w.getBytes().toByteArray();
+        w.close();
+        directFloatPage = ByteBuffer.allocateDirect(page.length);
+        directFloatPage.put(page);
+        directFloatPage.flip();
+      }
+      {
+        ValuesWriter w = new 
ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+            INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+        for (int i = 0; i < VALUE_COUNT; i++) {
+          w.writeLong(random.nextLong());
+        }
+        byte[] page = w.getBytes().toByteArray();
+        w.close();
+        directLongPage = ByteBuffer.allocateDirect(page.length);
+        directLongPage.put(page);
+        directLongPage.flip();
+      }
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeFloatDirect(DirectBufferState state, Blackhole bh) throws 
IOException {
+    ByteStreamSplitValuesReaderForFloat r = new 
ByteStreamSplitValuesReaderForFloat();
+    initDirect(r, state.directFloatPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readFloat());
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeLongDirect(DirectBufferState state, Blackhole bh) throws 
IOException {
+    ByteStreamSplitValuesReaderForLong r = new 
ByteStreamSplitValuesReaderForLong();
+    initDirect(r, state.directLongPage);
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(r.readLong());
+    }
+  }
+}
diff --git 
a/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitEncodingBenchmark.java
 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitEncodingBenchmark.java
new file mode 100644
index 000000000..fa1516d1d
--- /dev/null
+++ 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/ByteStreamSplitEncodingBenchmark.java
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+package org.apache.parquet.benchmarks;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.column.values.ValuesWriter;
+import 
org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesWriter;
+import org.apache.parquet.io.api.Binary;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OperationsPerInvocation;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Encoding-level micro-benchmarks for the BYTE_STREAM_SPLIT encoding across 
all
+ * Parquet types that support it: {@code FLOAT}, {@code DOUBLE}, {@code INT32},
+ * {@code INT64}, and {@code FIXED_LEN_BYTE_ARRAY}.
+ *
+ * <p>Fixed-width numeric types are benchmarked directly by top-level methods.
+ * {@code FIXED_LEN_BYTE_ARRAY} uses an inner {@link FlbaState} parameterised 
by
+ * {@code fixedLength} to avoid cross-product pollution with the numeric 
benchmarks.
+ *
+ * <p>Each invocation encodes {@value #VALUE_COUNT} values; throughput is 
reported
+ * per-value via {@link OperationsPerInvocation}.
+ */
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class ByteStreamSplitEncodingBenchmark {
+
+  static final int VALUE_COUNT = 100_000;
+  private static final int INIT_SLAB_SIZE = 64 * 1024;
+  private static final int PAGE_SIZE = 4 * 1024 * 1024;
+
+  private int[] intData;
+  private long[] longData;
+  private float[] floatData;
+  private double[] doubleData;
+
+  @Setup(Level.Trial)
+  public void setup() {
+    Random random = new Random(42);
+    intData = new int[VALUE_COUNT];
+    longData = new long[VALUE_COUNT];
+    floatData = new float[VALUE_COUNT];
+    doubleData = new double[VALUE_COUNT];
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      intData[i] = random.nextInt();
+      longData[i] = random.nextLong();
+      floatData[i] = random.nextFloat();
+      doubleData[i] = random.nextDouble();
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeFloat() throws IOException {
+    ValuesWriter w = new 
ByteStreamSplitValuesWriter.FloatByteStreamSplitValuesWriter(
+        INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+    for (float v : floatData) {
+      w.writeFloat(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeDouble() throws IOException {
+    ValuesWriter w = new 
ByteStreamSplitValuesWriter.DoubleByteStreamSplitValuesWriter(
+        INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+    for (double v : doubleData) {
+      w.writeDouble(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeInt() throws IOException {
+    ValuesWriter w = new 
ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter(
+        INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+    for (int v : intData) {
+      w.writeInteger(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeLong() throws IOException {
+    ValuesWriter w = new 
ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+        INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+    for (long v : longData) {
+      w.writeLong(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- FIXED_LEN_BYTE_ARRAY (parameterised by fixedLength) ----
+
+  @State(Scope.Thread)
+  public static class FlbaState {
+    @Param({"2", "7", "12", "16"})
+    public int fixedLength;
+
+    Binary[] data;
+
+    @Setup(Level.Trial)
+    public void setup() {
+      data = TestDataFactory.generateFixedLenByteArrays(
+          VALUE_COUNT, fixedLength, 0, TestDataFactory.DEFAULT_SEED);
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeFlba(FlbaState state) throws IOException {
+    ValuesWriter w = new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+        state.fixedLength, INIT_SLAB_SIZE, PAGE_SIZE, new 
HeapByteBufferAllocator());
+    for (Binary v : state.data) {
+      w.writeBytes(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+}
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesReader.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesReader.java
index c8ab3043b..6f89c4ce6 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesReader.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesReader.java
@@ -49,17 +49,138 @@ public abstract class ByteStreamSplitValuesReader extends 
ValuesReader {
     return offset;
   }
 
-  // Decode an entire data page
+  // Decode an entire data page by transposing from stream-split layout to 
interleaved layout.
   private byte[] decodeData(ByteBuffer encoded, int valuesCount) {
-    assert encoded.limit() == valuesCount * elementSizeInBytes;
-    byte[] decoded = new byte[encoded.limit()];
-    int destByteIndex = 0;
-    for (int srcValueIndex = 0; srcValueIndex < valuesCount; ++srcValueIndex) {
-      for (int stream = 0; stream < elementSizeInBytes; ++stream, 
++destByteIndex) {
-        decoded[destByteIndex] = encoded.get(srcValueIndex + stream * 
valuesCount);
+    int totalBytes = valuesCount * elementSizeInBytes;
+    assert encoded.remaining() == totalBytes;
+
+    // Bulk access: use the backing array directly if available, otherwise 
copy once.
+    byte[] src;
+    int srcBase;
+    if (encoded.hasArray()) {
+      src = encoded.array();
+      srcBase = encoded.arrayOffset() + encoded.position();
+    } else {
+      src = new byte[totalBytes];
+      encoded.get(src);
+      srcBase = 0;
+    }
+
+    byte[] decoded = new byte[totalBytes];
+
+    // Specialized single-pass loops for common element sizes.
+    if (elementSizeInBytes == 2) {
+      int s0 = srcBase, s1 = srcBase + valuesCount;
+      for (int i = 0; i < valuesCount; ++i) {
+        int di = i * 2;
+        decoded[di] = src[s0 + i];
+        decoded[di + 1] = src[s1 + i];
+      }
+    } else if (elementSizeInBytes == 4) {
+      int s0 = srcBase,
+          s1 = srcBase + valuesCount,
+          s2 = srcBase + 2 * valuesCount,
+          s3 = srcBase + 3 * valuesCount;
+      for (int i = 0; i < valuesCount; ++i) {
+        int di = i * 4;
+        decoded[di] = src[s0 + i];
+        decoded[di + 1] = src[s1 + i];
+        decoded[di + 2] = src[s2 + i];
+        decoded[di + 3] = src[s3 + i];
+      }
+    } else if (elementSizeInBytes == 8) {
+      int s0 = srcBase,
+          s1 = srcBase + valuesCount,
+          s2 = srcBase + 2 * valuesCount,
+          s3 = srcBase + 3 * valuesCount,
+          s4 = srcBase + 4 * valuesCount,
+          s5 = srcBase + 5 * valuesCount,
+          s6 = srcBase + 6 * valuesCount,
+          s7 = srcBase + 7 * valuesCount;
+      for (int i = 0; i < valuesCount; ++i) {
+        int di = i * 8;
+        decoded[di] = src[s0 + i];
+        decoded[di + 1] = src[s1 + i];
+        decoded[di + 2] = src[s2 + i];
+        decoded[di + 3] = src[s3 + i];
+        decoded[di + 4] = src[s4 + i];
+        decoded[di + 5] = src[s5 + i];
+        decoded[di + 6] = src[s6 + i];
+        decoded[di + 7] = src[s7 + i];
+      }
+    } else if (elementSizeInBytes == 12) {
+      int s0 = srcBase,
+          s1 = srcBase + valuesCount,
+          s2 = srcBase + 2 * valuesCount,
+          s3 = srcBase + 3 * valuesCount,
+          s4 = srcBase + 4 * valuesCount,
+          s5 = srcBase + 5 * valuesCount,
+          s6 = srcBase + 6 * valuesCount,
+          s7 = srcBase + 7 * valuesCount,
+          s8 = srcBase + 8 * valuesCount,
+          s9 = srcBase + 9 * valuesCount,
+          s10 = srcBase + 10 * valuesCount,
+          s11 = srcBase + 11 * valuesCount;
+      for (int i = 0; i < valuesCount; ++i) {
+        int di = i * 12;
+        decoded[di] = src[s0 + i];
+        decoded[di + 1] = src[s1 + i];
+        decoded[di + 2] = src[s2 + i];
+        decoded[di + 3] = src[s3 + i];
+        decoded[di + 4] = src[s4 + i];
+        decoded[di + 5] = src[s5 + i];
+        decoded[di + 6] = src[s6 + i];
+        decoded[di + 7] = src[s7 + i];
+        decoded[di + 8] = src[s8 + i];
+        decoded[di + 9] = src[s9 + i];
+        decoded[di + 10] = src[s10 + i];
+        decoded[di + 11] = src[s11 + i];
+      }
+    } else if (elementSizeInBytes == 16) {
+      int s0 = srcBase,
+          s1 = srcBase + valuesCount,
+          s2 = srcBase + 2 * valuesCount,
+          s3 = srcBase + 3 * valuesCount,
+          s4 = srcBase + 4 * valuesCount,
+          s5 = srcBase + 5 * valuesCount,
+          s6 = srcBase + 6 * valuesCount,
+          s7 = srcBase + 7 * valuesCount,
+          s8 = srcBase + 8 * valuesCount,
+          s9 = srcBase + 9 * valuesCount,
+          s10 = srcBase + 10 * valuesCount,
+          s11 = srcBase + 11 * valuesCount,
+          s12 = srcBase + 12 * valuesCount,
+          s13 = srcBase + 13 * valuesCount,
+          s14 = srcBase + 14 * valuesCount,
+          s15 = srcBase + 15 * valuesCount;
+      for (int i = 0; i < valuesCount; ++i) {
+        int di = i * 16;
+        decoded[di] = src[s0 + i];
+        decoded[di + 1] = src[s1 + i];
+        decoded[di + 2] = src[s2 + i];
+        decoded[di + 3] = src[s3 + i];
+        decoded[di + 4] = src[s4 + i];
+        decoded[di + 5] = src[s5 + i];
+        decoded[di + 6] = src[s6 + i];
+        decoded[di + 7] = src[s7 + i];
+        decoded[di + 8] = src[s8 + i];
+        decoded[di + 9] = src[s9 + i];
+        decoded[di + 10] = src[s10 + i];
+        decoded[di + 11] = src[s11 + i];
+        decoded[di + 12] = src[s12 + i];
+        decoded[di + 13] = src[s13 + i];
+        decoded[di + 14] = src[s14 + i];
+        decoded[di + 15] = src[s15 + i];
+      }
+    } else {
+      // Generic fallback for arbitrary element sizes
+      for (int stream = 0; stream < elementSizeInBytes; ++stream) {
+        int srcOffset = srcBase + stream * valuesCount;
+        for (int i = 0; i < valuesCount; ++i) {
+          decoded[i * elementSizeInBytes + stream] = src[srcOffset + i];
+        }
       }
     }
-    assert destByteIndex == decoded.length;
     return decoded;
   }
 
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesWriter.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesWriter.java
index 077be9f65..3779c5607 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesWriter.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitValuesWriter.java
@@ -20,7 +20,6 @@ package org.apache.parquet.column.values.bytestreamsplit;
 
 import org.apache.parquet.bytes.ByteBufferAllocator;
 import org.apache.parquet.bytes.BytesInput;
-import org.apache.parquet.bytes.BytesUtils;
 import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
 import org.apache.parquet.column.Encoding;
 import org.apache.parquet.column.values.ValuesWriter;
@@ -29,9 +28,23 @@ import org.apache.parquet.io.api.Binary;
 
 public abstract class ByteStreamSplitValuesWriter extends ValuesWriter {
 
+  /**
+   * Batch size for buffered scatter writes. Values are accumulated in a batch 
buffer
+   * and flushed as bulk {@code write(byte[], off, len)} calls to each stream, 
replacing
+   * N individual single-byte writes with one bulk write per stream per flush.
+   */
+  private static final int BATCH_SIZE = 64;
+
   protected final int numStreams;
   protected final int elementSizeInBytes;
-  private final CapacityByteArrayOutputStream[] byteStreams;
+  protected final CapacityByteArrayOutputStream[] byteStreams;
+
+  // Batch buffers for int (4-byte) and long (8-byte) scatter writes.
+  // Only one of these is ever non-null per instance.
+  private int[] intBatch;
+  private long[] longBatch;
+  private byte[] scatterBuf;
+  private int batchCount;
 
   public ByteStreamSplitValuesWriter(
       int elementSizeInBytes, int initialCapacity, int pageSize, 
ByteBufferAllocator allocator) {
@@ -53,7 +66,8 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
   @Override
   public long getBufferedSize() {
-    long totalSize = 0;
+    // Include unflushed batch values without triggering a flush
+    long totalSize = (long) batchCount * elementSizeInBytes;
     for (CapacityByteArrayOutputStream stream : this.byteStreams) {
       totalSize += stream.size();
     }
@@ -62,6 +76,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
   @Override
   public BytesInput getBytes() {
+    flushBatch();
     BytesInput[] allInputs = new BytesInput[this.numStreams];
     for (int i = 0; i < this.numStreams; ++i) {
       allInputs[i] = BytesInput.from(this.byteStreams[i]);
@@ -76,6 +91,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
   @Override
   public void reset() {
+    batchCount = 0;
     for (CapacityByteArrayOutputStream stream : this.byteStreams) {
       stream.reset();
     }
@@ -83,20 +99,82 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
   @Override
   public void close() {
+    batchCount = 0;
     for (CapacityByteArrayOutputStream stream : byteStreams) {
       stream.close();
     }
   }
 
-  protected void scatterBytes(byte[] bytes) {
-    if (bytes.length != this.numStreams) {
-      throw new ParquetEncodingException(String.format(
-          "Number of bytes doesn't match the number of streams. Num butes: %d, 
Num streams: %d",
-          bytes.length, this.numStreams));
+  /**
+   * Buffer a 4-byte integer value for batched scatter to the byte streams.
+   * Values are accumulated until the batch is full, then flushed as bulk
+   * {@code write(byte[], off, len)} calls -- one per stream.
+   */
+  protected void bufferInt(int v) {
+    if (intBatch == null) {
+      intBatch = new int[BATCH_SIZE];
+      scatterBuf = new byte[BATCH_SIZE];
+    }
+    intBatch[batchCount++] = v;
+    if (batchCount == BATCH_SIZE) {
+      flushIntBatch();
+    }
+  }
+
+  /**
+   * Buffer an 8-byte long value for batched scatter to the byte streams.
+   */
+  protected void bufferLong(long v) {
+    if (longBatch == null) {
+      longBatch = new long[BATCH_SIZE];
+      scatterBuf = new byte[BATCH_SIZE];
+    }
+    longBatch[batchCount++] = v;
+    if (batchCount == BATCH_SIZE) {
+      flushLongBatch();
+    }
+  }
+
+  private void flushBatch() {
+    if (batchCount == 0) return;
+    if (intBatch != null) {
+      flushIntBatch();
+    } else if (longBatch != null) {
+      flushLongBatch();
+    } else {
+      throw new ParquetEncodingException(
+          "Batch count is " + batchCount + " but no batch buffer has been 
initialized");
+    }
+  }
+
+  private void flushIntBatch() {
+    if (batchCount == 0) return;
+    final int count = batchCount;
+    for (int stream = 0; stream < 4; stream++) {
+      final int shift = stream << 3; // stream * 8
+      for (int i = 0; i < count; i++) {
+        scatterBuf[i] = (byte) (intBatch[i] >>> shift);
+      }
+      writeToStream(stream, scatterBuf, 0, count);
     }
-    for (int i = 0; i < bytes.length; ++i) {
-      this.byteStreams[i].write(bytes[i]);
+    batchCount = 0;
+  }
+
+  private void flushLongBatch() {
+    if (batchCount == 0) return;
+    final int count = batchCount;
+    for (int stream = 0; stream < 8; stream++) {
+      final int shift = stream << 3; // stream * 8
+      for (int i = 0; i < count; i++) {
+        scatterBuf[i] = (byte) (longBatch[i] >>> shift);
+      }
+      writeToStream(stream, scatterBuf, 0, count);
     }
+    batchCount = 0;
+  }
+
+  void writeToStream(int stream, byte[] buf, int off, int len) {
+    byteStreams[stream].write(buf, off, len);
   }
 
   @Override
@@ -116,7 +194,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
     @Override
     public void writeFloat(float v) {
-      super.scatterBytes(BytesUtils.intToBytes(Float.floatToRawIntBits(v)));
+      bufferInt(Float.floatToRawIntBits(v));
     }
 
     @Override
@@ -133,7 +211,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
     @Override
     public void writeDouble(double v) {
-      
super.scatterBytes(BytesUtils.longToBytes(Double.doubleToRawLongBits(v)));
+      bufferLong(Double.doubleToRawLongBits(v));
     }
 
     @Override
@@ -149,7 +227,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
     @Override
     public void writeInteger(int v) {
-      super.scatterBytes(BytesUtils.intToBytes(v));
+      bufferInt(v);
     }
 
     @Override
@@ -165,7 +243,7 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
     @Override
     public void writeLong(long v) {
-      super.scatterBytes(BytesUtils.longToBytes(v));
+      bufferLong(v);
     }
 
     @Override
@@ -176,6 +254,8 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
 
   public static class FixedLenByteArrayByteStreamSplitValuesWriter extends 
ByteStreamSplitValuesWriter {
     private final int length;
+    private byte[][] batchBufs; // [stream][batchIndex] scratch buffers
+    private int flbaBatchCount;
 
     public FixedLenByteArrayByteStreamSplitValuesWriter(
         int length, int initialCapacity, int pageSize, ByteBufferAllocator 
allocator) {
@@ -187,7 +267,49 @@ public abstract class ByteStreamSplitValuesWriter extends 
ValuesWriter {
     public final void writeBytes(Binary v) {
       assert (v.length() == length)
           : ("Fixed Binary size " + v.length() + " does not match field type 
length " + length);
-      super.scatterBytes(v.getBytesUnsafe());
+      if (batchBufs == null) {
+        batchBufs = new byte[length][BATCH_SIZE];
+      }
+      byte[] bytes = v.getBytesUnsafe();
+      for (int stream = 0; stream < length; stream++) {
+        batchBufs[stream][flbaBatchCount] = bytes[stream];
+      }
+      flbaBatchCount++;
+      if (flbaBatchCount == BATCH_SIZE) {
+        flushFlbaBatch();
+      }
+    }
+
+    private void flushFlbaBatch() {
+      if (flbaBatchCount == 0) return;
+      final int count = flbaBatchCount;
+      for (int stream = 0; stream < length; stream++) {
+        writeToStream(stream, batchBufs[stream], 0, count);
+      }
+      flbaBatchCount = 0;
+    }
+
+    @Override
+    public BytesInput getBytes() {
+      flushFlbaBatch();
+      return super.getBytes();
+    }
+
+    @Override
+    public void reset() {
+      flbaBatchCount = 0;
+      super.reset();
+    }
+
+    @Override
+    public void close() {
+      flbaBatchCount = 0;
+      super.close();
+    }
+
+    @Override
+    public long getBufferedSize() {
+      return super.getBufferedSize() + (long) flbaBatchCount * length;
     }
 
     @Override
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitScalarOptTest.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitScalarOptTest.java
new file mode 100644
index 000000000..af2bcd7cd
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/bytestreamsplit/ByteStreamSplitScalarOptTest.java
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.parquet.column.values.bytestreamsplit;
+
+import static org.junit.Assert.assertEquals;
+
+import java.nio.ByteBuffer;
+import java.util.Random;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.BytesInput;
+import org.apache.parquet.bytes.DirectByteBufferAllocator;
+import org.apache.parquet.io.api.Binary;
+import org.junit.Test;
+
+/**
+ * Tests for the BYTE_STREAM_SPLIT scalar performance optimizations:
+ * internal scatter-buffer batching, decodeData transpose specializations,
+ * direct ByteBuffer decode path, getBufferedSize accuracy, and close/reset
+ * edge cases.
+ */
+public class ByteStreamSplitScalarOptTest {
+
+  private static final int BATCH_SIZE = 64; // matches 
ByteStreamSplitValuesWriter.BATCH_SIZE
+
+  // 
---------------------------------------------------------------------------
+  // decodeData transpose specializations: element sizes 2, 12, 16
+  // (sizes 4 and 8 are already covered by int/float and long/double tests)
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testFlbaTransposeSize2() throws Exception {
+    flbaRoundTrip(2, 512);
+  }
+
+  @Test
+  public void testFlbaTransposeSize12() throws Exception {
+    flbaRoundTrip(12, 256);
+  }
+
+  @Test
+  public void testFlbaTransposeSize16() throws Exception {
+    flbaRoundTrip(16, 256);
+  }
+
+  /** Generic FLBA round-trip used to exercise specific element-size transpose 
paths. */
+  private void flbaRoundTrip(int typeLength, int numElements) throws Exception 
{
+    Random rand = new Random(42);
+    Binary[] values = new Binary[numElements];
+    for (int i = 0; i < numElements; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      values[i] = Binary.fromConstantByteArray(bytes);
+    }
+
+    ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter 
writer =
+        new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+            typeLength,
+            numElements * typeLength,
+            numElements * typeLength,
+            new DirectByteBufferAllocator());
+    // Use scalar writes to test the writer's scatter path independently from 
batch writes.
+    for (Binary v : values) {
+      writer.writeBytes(v);
+    }
+    BytesInput input = writer.getBytes();
+    assertEquals(numElements * typeLength, input.size());
+
+    ByteStreamSplitValuesReaderForFLBA reader = new 
ByteStreamSplitValuesReaderForFLBA(typeLength);
+    reader.initFromPage(numElements, 
ByteBufferInputStream.wrap(input.toByteBuffer()));
+
+    // Scalar read to verify each value
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readBytes());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  /** Also test the generic fallback path with an odd element size (e.g. 5, 
7). */
+  @Test
+  public void testFlbaTransposeGenericFallback() throws Exception {
+    flbaRoundTrip(5, 256);
+    flbaRoundTrip(7, 256);
+  }
+
+  // 
---------------------------------------------------------------------------
+  // BATCH_SIZE boundary crossing: tests that internal batch flush works 
correctly
+  // when writing exactly BATCH_SIZE, BATCH_SIZE+1, and multi-BATCH_SIZE counts
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testIntegerWriteExactBatchSize() throws Exception {
+    intScalarRoundTrip(BATCH_SIZE);
+  }
+
+  @Test
+  public void testIntegerWriteBatchSizePlusOne() throws Exception {
+    intScalarRoundTrip(BATCH_SIZE + 1);
+  }
+
+  @Test
+  public void testIntegerWriteMultipleBatches() throws Exception {
+    intScalarRoundTrip(BATCH_SIZE * 3 + 17);
+  }
+
+  @Test
+  public void testLongWriteExactBatchSize() throws Exception {
+    longScalarRoundTrip(BATCH_SIZE);
+  }
+
+  @Test
+  public void testLongWriteBatchSizePlusOne() throws Exception {
+    longScalarRoundTrip(BATCH_SIZE + 1);
+  }
+
+  @Test
+  public void testLongWriteMultipleBatches() throws Exception {
+    longScalarRoundTrip(BATCH_SIZE * 3 + 17);
+  }
+
+  private void intScalarRoundTrip(int numElements) throws Exception {
+    Random rand = new Random(42);
+    int[] values = rand.ints(numElements).toArray();
+
+    ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter(
+            numElements * 4, numElements * 4, new DirectByteBufferAllocator());
+    for (int v : values) {
+      writer.writeInteger(v);
+    }
+    BytesInput input = writer.getBytes();
+
+    ByteStreamSplitValuesReaderForInteger reader = new 
ByteStreamSplitValuesReaderForInteger();
+    reader.initFromPage(numElements, 
ByteBufferInputStream.wrap(input.toByteBuffer()));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readInteger());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  private void longScalarRoundTrip(int numElements) throws Exception {
+    Random rand = new Random(42);
+    long[] values = rand.longs(numElements).toArray();
+
+    ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+            numElements * 8, numElements * 8, new DirectByteBufferAllocator());
+    for (long v : values) {
+      writer.writeLong(v);
+    }
+    BytesInput input = writer.getBytes();
+
+    ByteStreamSplitValuesReaderForLong reader = new 
ByteStreamSplitValuesReaderForLong();
+    reader.initFromPage(numElements, 
ByteBufferInputStream.wrap(input.toByteBuffer()));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readLong());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  // 
---------------------------------------------------------------------------
+  // getBufferedSize accounts for unflushed batch
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testGetBufferedSizeWithPartialBatch() throws Exception {
+    ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter(
+            256, 256, new DirectByteBufferAllocator());
+
+    // Write fewer than BATCH_SIZE values -- they sit in the internal batch 
buffer
+    for (int i = 0; i < 10; i++) {
+      writer.writeInteger(i);
+    }
+    assertEquals(10 * 4, writer.getBufferedSize());
+
+    // Write more to cross a batch boundary
+    for (int i = 0; i < BATCH_SIZE; i++) {
+      writer.writeInteger(i);
+    }
+    assertEquals((10 + BATCH_SIZE) * 4, writer.getBufferedSize());
+
+    writer.reset();
+    assertEquals(0, writer.getBufferedSize());
+    writer.close();
+  }
+
+  @Test
+  public void testGetBufferedSizeWithPartialLongBatch() throws Exception {
+    ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+            256, 256, new DirectByteBufferAllocator());
+
+    for (int i = 0; i < 10; i++) {
+      writer.writeLong(i);
+    }
+    assertEquals(10 * 8, writer.getBufferedSize());
+
+    writer.reset();
+    assertEquals(0, writer.getBufferedSize());
+    writer.close();
+  }
+
+  // 
---------------------------------------------------------------------------
+  // decodeData direct ByteBuffer path (no backing array)
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testDecodeFromDirectByteBuffer() throws Exception {
+    Random rand = new Random(42);
+    final int numElements = 256;
+    float[] values = new float[numElements];
+    for (int i = 0; i < numElements; i++) {
+      values[i] = rand.nextFloat();
+    }
+
+    // Encode using standard writer
+    ByteStreamSplitValuesWriter.FloatByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.FloatByteStreamSplitValuesWriter(
+            numElements * 4, numElements * 4, new DirectByteBufferAllocator());
+    for (float v : values) {
+      writer.writeFloat(v);
+    }
+    byte[] encoded = writer.getBytes().toByteArray();
+
+    // Copy into a direct ByteBuffer (no backing array) to exercise the else 
branch in decodeData
+    ByteBuffer direct = ByteBuffer.allocateDirect(encoded.length);
+    direct.put(encoded);
+    direct.flip();
+
+    ByteStreamSplitValuesReaderForFloat reader = new 
ByteStreamSplitValuesReaderForFloat();
+    reader.initFromPage(numElements, ByteBufferInputStream.wrap(direct));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readFloat(), 
0.0f);
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  @Test
+  public void testDecodeFromDirectByteBufferLong() throws Exception {
+    Random rand = new Random(42);
+    final int numElements = 256;
+    long[] values = rand.longs(numElements).toArray();
+
+    ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+            numElements * 8, numElements * 8, new DirectByteBufferAllocator());
+    for (long v : values) {
+      writer.writeLong(v);
+    }
+    byte[] encoded = writer.getBytes().toByteArray();
+
+    ByteBuffer direct = ByteBuffer.allocateDirect(encoded.length);
+    direct.put(encoded);
+    direct.flip();
+
+    ByteStreamSplitValuesReaderForLong reader = new 
ByteStreamSplitValuesReaderForLong();
+    reader.initFromPage(numElements, ByteBufferInputStream.wrap(direct));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readLong());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  @Test
+  public void testDecodeFromDirectByteBufferFlba() throws Exception {
+    Random rand = new Random(42);
+    final int numElements = 256;
+    final int typeLength = 12;
+    Binary[] values = new Binary[numElements];
+    for (int i = 0; i < numElements; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      values[i] = Binary.fromConstantByteArray(bytes);
+    }
+
+    ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter 
writer =
+        new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+            typeLength,
+            numElements * typeLength,
+            numElements * typeLength,
+            new DirectByteBufferAllocator());
+    for (Binary v : values) {
+      writer.writeBytes(v);
+    }
+    byte[] encoded = writer.getBytes().toByteArray();
+
+    ByteBuffer direct = ByteBuffer.allocateDirect(encoded.length);
+    direct.put(encoded);
+    direct.flip();
+
+    ByteStreamSplitValuesReaderForFLBA reader = new 
ByteStreamSplitValuesReaderForFLBA(typeLength);
+    reader.initFromPage(numElements, ByteBufferInputStream.wrap(direct));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readBytes());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+
+  // 
---------------------------------------------------------------------------
+  // FLBA getBufferedSize with partial batch
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testFlbaGetBufferedSizeWithPartialBatch() throws Exception {
+    final int typeLength = 12;
+    ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter 
writer =
+        new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+            typeLength, 256, 256, new DirectByteBufferAllocator());
+
+    Random rand = new Random(42);
+    // Write fewer than BATCH_SIZE values -- they sit in batchBufs
+    for (int i = 0; i < 10; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      writer.writeBytes(Binary.fromConstantByteArray(bytes));
+    }
+    assertEquals(10 * typeLength, writer.getBufferedSize());
+
+    // Write more to cross a batch boundary
+    for (int i = 0; i < BATCH_SIZE; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      writer.writeBytes(Binary.fromConstantByteArray(bytes));
+    }
+    assertEquals((10 + BATCH_SIZE) * typeLength, writer.getBufferedSize());
+
+    writer.reset();
+    assertEquals(0, writer.getBufferedSize());
+    writer.close();
+  }
+
+  // 
---------------------------------------------------------------------------
+  // close() with a pending (unflushed) batch resets batch state
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testIntCloseWithPendingBatch() throws Exception {
+    ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.IntegerByteStreamSplitValuesWriter(
+            256, 256, new DirectByteBufferAllocator());
+    // Write fewer than BATCH_SIZE values so the batch stays unflushed
+    for (int i = 0; i < 10; i++) {
+      writer.writeInteger(i);
+    }
+    assertEquals(10 * 4, writer.getBufferedSize());
+    writer.close();
+    assertEquals(0, writer.getBufferedSize());
+  }
+
+  @Test
+  public void testLongCloseWithPendingBatch() throws Exception {
+    ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter writer =
+        new ByteStreamSplitValuesWriter.LongByteStreamSplitValuesWriter(
+            256, 256, new DirectByteBufferAllocator());
+    for (int i = 0; i < 10; i++) {
+      writer.writeLong(i);
+    }
+    assertEquals(10 * 8, writer.getBufferedSize());
+    writer.close();
+    assertEquals(0, writer.getBufferedSize());
+  }
+
+  @Test
+  public void testFlbaCloseWithPendingBatch() throws Exception {
+    final int typeLength = 12;
+    ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter 
writer =
+        new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+            typeLength, 256, 256, new DirectByteBufferAllocator());
+    Random rand = new Random(42);
+    for (int i = 0; i < 10; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      writer.writeBytes(Binary.fromConstantByteArray(bytes));
+    }
+    assertEquals(10 * typeLength, writer.getBufferedSize());
+    writer.close();
+    assertEquals(0, writer.getBufferedSize());
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Direct ByteBuffer decode for 2-byte and 16-byte element sizes
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  public void testDecodeFromDirectByteBufferFlba2() throws Exception {
+    directByteBufferFlbaRoundTrip(2, 256);
+  }
+
+  @Test
+  public void testDecodeFromDirectByteBufferFlba16() throws Exception {
+    directByteBufferFlbaRoundTrip(16, 256);
+  }
+
+  private void directByteBufferFlbaRoundTrip(int typeLength, int numElements) 
throws Exception {
+    Random rand = new Random(42);
+    Binary[] values = new Binary[numElements];
+    for (int i = 0; i < numElements; i++) {
+      byte[] bytes = new byte[typeLength];
+      rand.nextBytes(bytes);
+      values[i] = Binary.fromConstantByteArray(bytes);
+    }
+
+    ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter 
writer =
+        new 
ByteStreamSplitValuesWriter.FixedLenByteArrayByteStreamSplitValuesWriter(
+            typeLength,
+            numElements * typeLength,
+            numElements * typeLength,
+            new DirectByteBufferAllocator());
+    for (Binary v : values) {
+      writer.writeBytes(v);
+    }
+    byte[] encoded = writer.getBytes().toByteArray();
+
+    // Use a direct ByteBuffer (no backing array) to exercise the else branch 
in decodeData
+    ByteBuffer direct = ByteBuffer.allocateDirect(encoded.length);
+    direct.put(encoded);
+    direct.flip();
+
+    ByteStreamSplitValuesReaderForFLBA reader = new 
ByteStreamSplitValuesReaderForFLBA(typeLength);
+    reader.initFromPage(numElements, ByteBufferInputStream.wrap(direct));
+
+    for (int i = 0; i < numElements; i++) {
+      assertEquals("Mismatch at index " + i, values[i], reader.readBytes());
+    }
+
+    writer.reset();
+    writer.close();
+  }
+}
diff --git a/pom.xml b/pom.xml
index b3344db73..89568390e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -643,6 +643,7 @@
               
<exclude>org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReader#gatherElementDataFromStreams(byte[])</exclude>
               <!-- Removal of a protected internal field that should not have 
been part of the public API -->
               
<exclude>org.apache.parquet.column.values.plain.PlainValuesReader#in</exclude>
+              
<exclude>org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesWriter#scatterBytes(byte[])</exclude>
               <!-- Due to the removal of deprecated methods -->
               
<exclude>org.apache.parquet.arrow.schema.SchemaMapping$TypeMappingVisitor#visit(org.apache.parquet.arrow.schema.SchemaMapping$MapTypeMapping)</exclude>
               <!-- Intentional removal of deprecated Pig support from 
parquet-thrift -->

Reply via email to