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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2e97c0fc07a HDDS-15485. Avoid ByteBuffer.wrap on 
ChunkBuffer.put(byte[]) path (#11147)
2e97c0fc07a is described below

commit 2e97c0fc07a11f2576b2bb5334438a05e9c0273c
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Fri Sep 18 19:28:50 2026 -0400

    HDDS-15485. Avoid ByteBuffer.wrap on ChunkBuffer.put(byte[]) path (#11147)
    
    Generated-by: Claude Code (Opus 4.8)
---
 .../apache/hadoop/ozone/common/ChunkBuffer.java    |  30 ++
 .../common/ChunkBufferImplWithByteBuffer.java      |  12 +
 .../common/ChunkBufferImplWithByteBufferList.java  |  31 +-
 .../ozone/common/IncrementalChunkBuffer.java       |  29 +-
 .../ozone/common/ChunkBufferPutBenchmark.java      | 407 +++++++++++++++++++++
 .../ozone/common/JfrByteBufferAllocations.java     | 133 +++++++
 .../hadoop/ozone/common/TestChunkBuffer.java       | 271 ++++++++++++++
 7 files changed, 896 insertions(+), 17 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBuffer.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBuffer.java
index 600a7b56797..6e06f502b2c 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBuffer.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBuffer.java
@@ -17,7 +17,9 @@
 
 package org.apache.hadoop.ozone.common;
 
+import com.google.common.base.Preconditions;
 import java.io.IOException;
+import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.channels.GatheringByteChannel;
 import java.util.List;
@@ -64,6 +66,34 @@ static ChunkBuffer wrap(List<ByteBuffer> buffers) {
     return new ChunkBufferImplWithByteBufferList(buffers);
   }
 
+  default void checkArgument(byte[] b, int offset, int length) {
+    Objects.requireNonNull(b, "b == null");
+    Preconditions.checkArgument(offset >= 0 && length >= 0,
+        "offset = %s, length = %s", offset, length);
+    Preconditions.checkArgument(length <= b.length - offset,
+        "length = %s out of range for array.length = %s, offset = %s",
+        length, b.length, offset);
+    if (length > remaining()) {
+      final BufferOverflowException boe = new BufferOverflowException();
+      boe.initCause(new IllegalArgumentException(
+          "Failed to put since length = " + length
+              + " > this.remaining() = " + remaining()));
+      throw boe;
+    }
+  }
+
+  default void checkArgument(ByteBuffer that) {
+    Objects.requireNonNull(that, "that == null");
+    final int thatRemaining = that.remaining();
+    if (thatRemaining > remaining()) {
+      final BufferOverflowException boe = new BufferOverflowException();
+      boe.initCause(new IllegalArgumentException(
+          "Failed to put since that.remaining() = " + thatRemaining
+              + " > this.remaining() = " + remaining()));
+      throw boe;
+    }
+  }
+
   /** Similar to {@link ByteBuffer#position()}. */
   int position();
 
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBuffer.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBuffer.java
index d37b095621c..5bc9300e905 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBuffer.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBuffer.java
@@ -118,6 +118,18 @@ public ChunkBuffer put(ByteBuffer b) {
     return this;
   }
 
+  @Override
+  public ChunkBuffer put(byte[] b, int offset, int length) {
+    checkArgument(b, offset, length);
+    buffer.put(b, offset, length);
+    return this;
+  }
+
+  @Override
+  public ChunkBuffer put(byte[] b) {
+    return put(b, 0, b.length);
+  }
+
   @Override
   public ChunkBuffer put(byte b) {
     buffer.put(b);
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBufferList.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBufferList.java
index e67726e98ff..afbb4253ef7 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBufferList.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBufferImplWithByteBufferList.java
@@ -20,7 +20,6 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
-import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.channels.GatheringByteChannel;
 import java.util.ArrayList;
@@ -135,15 +134,8 @@ public ChunkBuffer clear() {
 
   @Override
   public ChunkBuffer put(ByteBuffer that) {
-    final int thisRemaining = remaining();
+    checkArgument(that);
     int thatRemaining = that.remaining();
-    if (thatRemaining > thisRemaining) {
-      final BufferOverflowException boe = new BufferOverflowException();
-      boe.initCause(new IllegalArgumentException(
-          "Failed to put since that.remaining() = " + thatRemaining
-              + " > this.remaining() = " + thisRemaining));
-      throw boe;
-    }
 
     while (thatRemaining > 0) {
       final ByteBuffer b = current();
@@ -157,6 +149,27 @@ public ChunkBuffer put(ByteBuffer that) {
     return this;
   }
 
+  @Override
+  public ChunkBuffer put(byte[] b, int offset, int length) {
+    checkArgument(b, offset, length);
+
+    final int end = offset + length;
+    int off = offset;
+    while (off < end) {
+      final ByteBuffer cur = current();
+      final int bytes = Math.min(cur.remaining(), end - off);
+      cur.put(b, off, bytes);
+      off += bytes;
+      advanceCurrent();
+    }
+    return this;
+  }
+
+  @Override
+  public ChunkBuffer put(byte[] b) {
+    return put(b, 0, b.length);
+  }
+
   @Override
   public ChunkBuffer duplicate(int newPosition, int newLimit) {
     Preconditions.checkArgument(newPosition >= 0);
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/IncrementalChunkBuffer.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/IncrementalChunkBuffer.java
index 1d90fbc3972..8d20b42f1ec 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/IncrementalChunkBuffer.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/IncrementalChunkBuffer.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Preconditions;
 import java.io.IOException;
-import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.channels.GatheringByteChannel;
 import java.util.ArrayList;
@@ -212,13 +211,7 @@ public ChunkBuffer clear() {
 
   @Override
   public ChunkBuffer put(ByteBuffer that) {
-    if (that.remaining() > this.remaining()) {
-      final BufferOverflowException boe = new BufferOverflowException();
-      boe.initCause(new IllegalArgumentException(
-          "Failed to put since that.remaining() = " + that.remaining()
-              + " > this.remaining() = " + this.remaining()));
-      throw boe;
-    }
+    checkArgument(that);
 
     final int thatLimit = that.limit();
     for (int p = position(); that.position() < thatLimit;) {
@@ -231,6 +224,26 @@ public ChunkBuffer put(ByteBuffer that) {
     return this;
   }
 
+  @Override
+  public ChunkBuffer put(byte[] b, int offset, int length) {
+    checkArgument(b, offset, length);
+
+    final int end = offset + length;
+    for (int p = position(); offset < end;) {
+      final ByteBuffer buf = getAndAllocateAtPosition(p);
+      final int min = Math.min(buf.remaining(), end - offset);
+      buf.put(b, offset, min);
+      offset += min;
+      p += min;
+    }
+    return this;
+  }
+
+  @Override
+  public ChunkBuffer put(byte[] b) {
+    return put(b, 0, b.length);
+  }
+
   @Override
   public ChunkBuffer duplicate(int newPosition, int newLimit) {
     Preconditions.checkArgument(newPosition >= 0);
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/ChunkBufferPutBenchmark.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/ChunkBufferPutBenchmark.java
new file mode 100644
index 00000000000..3518691a216
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/ChunkBufferPutBenchmark.java
@@ -0,0 +1,407 @@
+/*
+ * 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.hadoop.ozone.common;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
+import java.util.concurrent.ThreadLocalRandom;
+import org.apache.hadoop.ozone.common.JfrByteBufferAllocations.AllocationStats;
+
+/**
+ * Microbenchmark for ChunkBuffer.put(byte[]) direct copy vs ByteBuffer.wrap 
path.
+ *
+ * <p>Focused on the scenarios where HDDS-15485 shows the clearest benefit:
+ * <ul>
+ *   <li>Throughput: 4KB stream fill with incremental buffer (64KB 
increment)</li>
+ *   <li>Allocations: same 4KB / 64KB-increment incremental buffer path (JFR + 
wrap calls)</li>
+ * </ul>
+ *
+ * <p>Run from the repo root:
+ * <pre>
+ *   mvn -pl hadoop-hdds/common -q test-compile exec:java \
+ *     -Dexec.mainClass=org.apache.hadoop.ozone.common.ChunkBufferPutBenchmark 
\
+ *     -Dexec.classpathScope=test \
+ *     -Dexec.args="--add-opens jdk.jfr/jdk.jfr=ALL-UNNAMED --add-opens 
jdk.jfr/jdk.jfr.consumer=ALL-UNNAMED"
+ * </pre>
+ * JFR ByteBuffer counts are sampled; put-op count reports exact wrap calls.
+ */
+public final class ChunkBufferPutBenchmark {
+
+  private static final int WARMUP_SECONDS = 10;
+  private static final int BENCHMARK_SECONDS = 20;
+  private static final int ALLOCATION_BENCHMARK_SECONDS = 5;
+  private static final int THROUGHPUT_ROUNDS = 3;
+  private static final DecimalFormat MBPS = new DecimalFormat("#,##0.0");
+  private static final DecimalFormat NS = new DecimalFormat("#,##0");
+  private static final DecimalFormat PCT = new DecimalFormat("#,##0.0");
+  private static final DecimalFormat RATIO = new DecimalFormat("0.00");
+  private static final DecimalFormat COUNT = new DecimalFormat("#,##0");
+
+  /** ozone.client.stream.buffer.size default. */
+  private static final int DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024;
+
+  /** Showcase: ozone.client.stream.buffer.increment for incremental buffer. */
+  private static final int INCREMENTAL_BUFFER_INCREMENT = 64 * 1024;
+
+  /** Hadoop io.file.buffer.size / FSDataOutputStream default. */
+  private static final int HADOOP_FS_BUFFER_SIZE = 4 * 1024;
+
+  private static final ThreadLocal<byte[]> SOURCE = ThreadLocal.withInitial(() 
-> {
+    byte[] bytes = new byte[DEFAULT_CHUNK_SIZE];
+    ThreadLocalRandom.current().nextBytes(bytes);
+    return bytes;
+  });
+
+  private ChunkBufferPutBenchmark() {
+  }
+
+  public static void main(String[] args) throws IOException {
+    System.out.println("ChunkBuffer.put(byte[]) microbenchmark (pre-allocated 
buffer, put-only)");
+    System.out.println("JVM: " + System.getProperty("java.version")
+        + " on " + System.getProperty("os.arch"));
+    System.out.println();
+
+    final Scenario showcase = new Scenario(
+        "Incremental buffer showcase",
+        DEFAULT_CHUNK_SIZE,
+        INCREMENTAL_BUFFER_INCREMENT,
+        HADOOP_FS_BUFFER_SIZE);
+
+    // Correctness self-check (verify=true) before timing; benchmark loops run
+    // with verify=false so the measurement is not polluted by read-back.
+    verifyCorrectness(showcase);
+
+    System.out.println("=== Throughput showcase ===");
+    runThroughputComparison(showcase);
+    System.out.println();
+
+    System.out.println("=== Allocation showcase ===");
+    runAllocationComparison(showcase);
+  }
+
+  private static void runThroughputComparison(Scenario scenario) {
+    printScenarioHeader(scenario);
+    warmupBothPaths(scenario);
+    final double[] improvements = new double[THROUGHPUT_ROUNDS];
+    for (int round = 0; round < THROUGHPUT_ROUNDS; round++) {
+      final boolean directFirst = round % 2 == 0;
+      final Result direct;
+      final Result wrap;
+      if (directFirst) {
+        direct = benchmarkThroughput(scenario, 
ChunkBufferPutBenchmark::loopStreamFillDirect);
+        wrap = benchmarkThroughput(scenario, 
ChunkBufferPutBenchmark::loopStreamFillWrap);
+      } else {
+        wrap = benchmarkThroughput(scenario, 
ChunkBufferPutBenchmark::loopStreamFillWrap);
+        direct = benchmarkThroughput(scenario, 
ChunkBufferPutBenchmark::loopStreamFillDirect);
+      }
+      improvements[round] = wrap.nsPerOp / direct.nsPerOp - 1.0;
+      System.out.printf("  round %d:%n", round + 1);
+      printThroughputComparison(scenario.writeSize, direct, wrap, "    ");
+    }
+    final double medianImprovement = median(improvements) * 100.0;
+    System.out.printf("  median improvement over %d rounds: %s%%%n",
+        THROUGHPUT_ROUNDS, PCT.format(medianImprovement));
+  }
+
+  private static void warmupBothPaths(Scenario scenario) {
+    final byte[] source = SOURCE.get();
+    final int writesPerChunk = scenario.chunkSize / scenario.writeSize;
+    try (ChunkBuffer buffer = ChunkBuffer.allocate(scenario.chunkSize,
+        scenario.bufferIncrement)) {
+      for (int i = 0; i < 2; i++) {
+        loopStreamFillDirect(buffer, source, scenario.writeSize, 
writesPerChunk,
+            WARMUP_SECONDS, false);
+        loopStreamFillWrap(buffer, source, scenario.writeSize, writesPerChunk,
+            WARMUP_SECONDS, false);
+      }
+    }
+  }
+
+  private static double median(double[] values) {
+    final double[] sorted = values.clone();
+    java.util.Arrays.sort(sorted);
+    return sorted[sorted.length / 2];
+  }
+
+  private static void runAllocationComparison(Scenario scenario) throws 
IOException {
+    printScenarioHeader(scenario);
+    if (!JfrByteBufferAllocations.isAvailable()) {
+      System.out.println("  JFR unavailable; reporting wrap-call count from 
put ops only.");
+    }
+    final AllocationResult direct = benchmarkAllocations(scenario,
+        ChunkBufferPutBenchmark::loopStreamFillDirect);
+    final AllocationResult wrap = benchmarkAllocations(scenario,
+        ChunkBufferPutBenchmark::loopStreamFillWrap);
+    printAllocationComparison(direct, wrap);
+  }
+
+  private static void printScenarioHeader(Scenario scenario) {
+    System.out.println("--- " + scenario.name + " ---");
+    System.out.println("Config: " + scenario.config());
+    System.out.println("Pattern: " + scenario.pattern());
+    System.out.printf("Chunk=%dKB increment=%dKB write=%dKB%n",
+        scenario.chunkSize / 1024,
+        scenario.bufferIncrement / 1024,
+        scenario.writeSize / 1024);
+  }
+
+  private static void printThroughputComparison(int writeSize, Result direct, 
Result wrap,
+      String prefix) {
+    final double speedup = wrap.nsPerOp / direct.nsPerOp;
+    final double pctFaster = (speedup - 1.0) * 100.0;
+    final double throughputSpeedup = direct.mbPerSec / wrap.mbPerSec;
+    final double throughputPct = (throughputSpeedup - 1.0) * 100.0;
+    System.out.printf("%sdirect put(byte[]):   %s MB/s | %s ns/op | %.2fs | %s 
ops%n",
+        prefix, MBPS.format(direct.mbPerSec), NS.format(direct.nsPerOp),
+        direct.elapsedSeconds, COUNT.format(direct.ops));
+    System.out.printf("%swrap put(ByteBuffer): %s MB/s | %s ns/op | %.2fs | %s 
ops%n",
+        prefix, MBPS.format(wrap.mbPerSec), NS.format(wrap.nsPerOp),
+        wrap.elapsedSeconds, COUNT.format(wrap.ops));
+    System.out.printf("%simprovement: %s%% faster (%sx) per %dKB write; "
+            + "throughput %s%% (%sx)%n",
+        prefix, PCT.format(pctFaster), RATIO.format(speedup), writeSize / 1024,
+        PCT.format(throughputPct), RATIO.format(throughputSpeedup));
+  }
+
+  private static void printAllocationComparison(AllocationResult direct,
+      AllocationResult wrap) {
+    System.out.printf("  direct put(byte[]):   %s put ops | %s ByteBuffer TLAB 
allocs | %s alloc bytes%n",
+        COUNT.format(direct.putOps), COUNT.format(direct.byteBufferAllocCount),
+        COUNT.format(direct.byteBufferAllocBytes));
+    System.out.printf("  wrap put(ByteBuffer): %s put ops | %s ByteBuffer TLAB 
allocs | %s alloc bytes%n",
+        COUNT.format(wrap.putOps), COUNT.format(wrap.byteBufferAllocCount),
+        COUNT.format(wrap.byteBufferAllocBytes));
+    System.out.printf("  ByteBuffer.wrap calls on wrap path (1 per put): %s%n",
+        COUNT.format(wrap.putOps));
+    System.out.printf("  direct path avoids %s ByteBuffer.wrap calls per 
run%n",
+        COUNT.format(wrap.putOps));
+    if (wrap.byteBufferAllocCount > 0 && direct.byteBufferAllocCount == 0) {
+      System.out.printf("  JFR confirms zero ByteBuffer TLAB allocations on 
direct path%n");
+    }
+    if (wrap.byteBufferAllocCount > direct.byteBufferAllocCount) {
+      final long saved = wrap.byteBufferAllocCount - 
direct.byteBufferAllocCount;
+      System.out.printf("  JFR sampled ByteBuffer TLAB allocations avoided on 
direct path: %s%n",
+          COUNT.format(saved));
+      System.out.println("  (JFR samples TLAB events; put-op count is the 
exact wrap-call metric)");
+    }
+  }
+
+  private static Result benchmarkThroughput(Scenario scenario, TimedLoop loop) 
{
+    final byte[] source = SOURCE.get();
+    final int writesPerChunk = scenario.chunkSize / scenario.writeSize;
+    try (ChunkBuffer buffer = ChunkBuffer.allocate(scenario.chunkSize,
+        scenario.bufferIncrement)) {
+      loop.run(buffer, source, scenario.writeSize, writesPerChunk, 
WARMUP_SECONDS, false);
+      final LoopResult benchmark = loop.run(buffer, source, scenario.writeSize,
+          writesPerChunk, BENCHMARK_SECONDS, false);
+      return toResult(benchmark, scenario.writeSize);
+    }
+  }
+
+  private static AllocationResult benchmarkAllocations(Scenario scenario, 
TimedLoop loop)
+      throws IOException {
+    final byte[] source = SOURCE.get();
+    final int writesPerChunk = scenario.chunkSize / scenario.writeSize;
+    try (ChunkBuffer buffer = ChunkBuffer.allocate(scenario.chunkSize,
+        scenario.bufferIncrement)) {
+      loop.run(buffer, source, scenario.writeSize, writesPerChunk, 
WARMUP_SECONDS, false);
+      final LoopResult[] benchmark = new LoopResult[1];
+      final AllocationStats stats = JfrByteBufferAllocations.measure(
+          () -> benchmark[0] = loop.run(buffer, source, scenario.writeSize,
+              writesPerChunk, ALLOCATION_BENCHMARK_SECONDS, false));
+      final long putOps = benchmark[0].totalBytes / scenario.writeSize;
+      return new AllocationResult(putOps, stats.getByteBufferAllocCount(),
+          stats.getByteBufferAllocBytes());
+    }
+  }
+
+  private static Result toResult(LoopResult benchmark, int writeSize) {
+    final long elapsedNanos = benchmark.elapsedNanos;
+    final long benchmarkBytes = benchmark.totalBytes;
+    final double seconds = elapsedNanos / 1_000_000_000.0;
+    final double mbPerSec = benchmarkBytes / seconds / (1024.0 * 1024.0);
+    final long ops = benchmarkBytes / writeSize;
+    final double nsPerOp = (double) elapsedNanos / ops;
+    return new Result(mbPerSec, nsPerOp, seconds, ops);
+  }
+
+  @FunctionalInterface
+  private interface TimedLoop {
+    LoopResult run(ChunkBuffer buffer, byte[] source, int writeSize,
+        int writesPerChunk, int seconds, boolean verify);
+  }
+
+  private static LoopResult loopStreamFillDirect(ChunkBuffer buffer, byte[] 
source,
+      int writeSize, int writesPerChunk, int seconds, boolean verify) {
+    long totalBytes = 0;
+    final long start = System.nanoTime();
+    final long deadline = start + seconds * 1_000_000_000L;
+    while (System.nanoTime() < deadline) {
+      buffer.clear();
+      int off = 0;
+      for (int i = 0; i < writesPerChunk; i++) {
+        buffer.put(source, off, writeSize);
+        off += writeSize;
+        totalBytes += writeSize;
+      }
+      if (verify) {
+        verifyBuffer(buffer, source, off);
+      }
+    }
+    return new LoopResult(totalBytes, System.nanoTime() - start);
+  }
+
+  private static LoopResult loopStreamFillWrap(ChunkBuffer buffer, byte[] 
source,
+      int writeSize, int writesPerChunk, int seconds, boolean verify) {
+    long totalBytes = 0;
+    final long start = System.nanoTime();
+    final long deadline = start + seconds * 1_000_000_000L;
+    while (System.nanoTime() < deadline) {
+      buffer.clear();
+      int off = 0;
+      for (int i = 0; i < writesPerChunk; i++) {
+        buffer.put(ByteBuffer.wrap(source, off, writeSize));
+        off += writeSize;
+        totalBytes += writeSize;
+      }
+      if (verify) {
+        verifyBuffer(buffer, source, off);
+      }
+    }
+    return new LoopResult(totalBytes, System.nanoTime() - start);
+  }
+
+  /**
+   * Reads back the first {@code bytesWritten} bytes and asserts they equal
+   * {@code source}. Used with verify=true to guarantee both put paths are
+   * byte-for-byte correct before (and independently of) timing them.
+   */
+  private static void verifyBuffer(ChunkBuffer buffer, byte[] source, int 
bytesWritten) {
+    final ChunkBuffer slice = buffer.duplicate(0, bytesWritten);
+    int pos = 0;
+    for (final ByteBuffer backing : slice.asByteBufferList()) {
+      final ByteBuffer b = backing.duplicate();
+      while (b.hasRemaining()) {
+        final byte actual = b.get();
+        if (actual != source[pos]) {
+          throw new IllegalStateException("Verification failed at byte " + pos
+              + ": expected " + source[pos] + " but got " + actual);
+        }
+        pos++;
+      }
+    }
+    if (pos != bytesWritten) {
+      throw new IllegalStateException("Verification read " + pos
+          + " bytes, expected " + bytesWritten);
+    }
+  }
+
+  private static void verifyCorrectness(Scenario scenario) {
+    final byte[] source = SOURCE.get();
+    final int writesPerChunk = scenario.chunkSize / scenario.writeSize;
+    try (ChunkBuffer buffer = ChunkBuffer.allocate(scenario.chunkSize,
+        scenario.bufferIncrement)) {
+      loopStreamFillDirect(buffer, source, scenario.writeSize, writesPerChunk, 
1, true);
+      loopStreamFillWrap(buffer, source, scenario.writeSize, writesPerChunk, 
1, true);
+    }
+    System.out.println("Correctness self-check passed (verify=true): "
+        + "direct put(byte[]) and wrap put(ByteBuffer) produce identical 
bytes.");
+    System.out.println();
+  }
+
+  private static final class LoopResult {
+    private final long totalBytes;
+    private final long elapsedNanos;
+
+    private LoopResult(long totalBytes, long elapsedNanos) {
+      this.totalBytes = totalBytes;
+      this.elapsedNanos = elapsedNanos;
+    }
+  }
+
+  private static final class AllocationResult {
+    private final long putOps;
+    private final long byteBufferAllocCount;
+    private final long byteBufferAllocBytes;
+
+    private AllocationResult(long putOps, long byteBufferAllocCount,
+        long byteBufferAllocBytes) {
+      this.putOps = putOps;
+      this.byteBufferAllocCount = byteBufferAllocCount;
+      this.byteBufferAllocBytes = byteBufferAllocBytes;
+    }
+  }
+
+  private static final class Scenario {
+    private final String name;
+    private final int chunkSize;
+    private final int bufferIncrement;
+    private final int writeSize;
+
+    private Scenario(String name, int chunkSize, int bufferIncrement, int 
writeSize) {
+      this.name = name;
+      this.chunkSize = chunkSize;
+      this.bufferIncrement = bufferIncrement;
+      this.writeSize = writeSize;
+    }
+
+    /** {@code ChunkBuffer.allocate} picks the impl from capacity vs 
increment. */
+    private String bufferKind() {
+      return bufferIncrement > 0 && bufferIncrement < chunkSize
+          ? "IncrementalChunkBuffer" : "ChunkBufferImplWithByteBuffer";
+    }
+
+    /** Config line derived from the numeric parameters (not hard coded). */
+    private String config() {
+      return "ozone.client.stream.buffer.size=" + human(chunkSize)
+          + ", ozone.client.stream.buffer.increment=" + human(bufferIncrement)
+          + ", io.file.buffer.size=" + human(writeSize);
+    }
+
+    /** Access pattern line derived from the numeric parameters (not hard 
coded). */
+    private String pattern() {
+      return human(writeSize) + " stream fill into " + bufferKind()
+          + " (" + human(bufferIncrement) + " steps)";
+    }
+  }
+
+  private static String human(int bytes) {
+    if (bytes != 0 && bytes % (1024 * 1024) == 0) {
+      return bytes / (1024 * 1024) + "MB";
+    }
+    if (bytes != 0 && bytes % 1024 == 0) {
+      return bytes / 1024 + "KB";
+    }
+    return bytes + "B";
+  }
+
+  private static final class Result {
+    private final double mbPerSec;
+    private final double nsPerOp;
+    private final double elapsedSeconds;
+    private final long ops;
+
+    private Result(double mbPerSec, double nsPerOp, double elapsedSeconds, 
long ops) {
+      this.mbPerSec = mbPerSec;
+      this.nsPerOp = nsPerOp;
+      this.elapsedSeconds = elapsedSeconds;
+      this.ops = ops;
+    }
+  }
+}
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/JfrByteBufferAllocations.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/JfrByteBufferAllocations.java
new file mode 100644
index 00000000000..9ba424f115e
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/JfrByteBufferAllocations.java
@@ -0,0 +1,133 @@
+/*
+ * 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.hadoop.ozone.common;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Reads jdk.ObjectAllocationInNewTLAB ByteBuffer allocations via JFR 
reflection
+ * so test compilation does not require jdk.jfr on the compile classpath.
+ */
+final class JfrByteBufferAllocations {
+
+  private static final boolean JFR_AVAILABLE = probeJfr();
+
+  private JfrByteBufferAllocations() {
+  }
+
+  static boolean isAvailable() {
+    return JFR_AVAILABLE;
+  }
+
+  static AllocationStats measure(Runnable workload) throws IOException {
+    if (!JFR_AVAILABLE) {
+      workload.run();
+      return new AllocationStats(0, 0);
+    }
+    try {
+      final Class<?> recordingClass = Class.forName("jdk.jfr.Recording");
+      final Object recording = recordingClass.getConstructor().newInstance();
+      final Object eventSettings = recordingClass.getMethod("enable", 
String.class)
+          .invoke(recording, "jdk.ObjectAllocationInNewTLAB");
+      
eventSettings.getClass().getMethod("withoutStackTrace").invoke(eventSettings);
+      recordingClass.getMethod("start").invoke(recording);
+      try {
+        workload.run();
+      } finally {
+        recordingClass.getMethod("stop").invoke(recording);
+      }
+      return readByteBufferAllocations(recording);
+    } catch (ReflectiveOperationException e) {
+      throw new IOException("Failed to run JFR allocation measurement", e);
+    }
+  }
+
+  private static AllocationStats readByteBufferAllocations(Object recording)
+      throws IOException, ReflectiveOperationException {
+    final Path dump = Files.createTempFile("chunkbuffer-put-bench", ".jfr");
+    try {
+      recording.getClass().getMethod("dump", Path.class).invoke(recording, 
dump);
+      final Class<?> recordingFileClass = 
Class.forName("jdk.jfr.consumer.RecordingFile");
+      final Object recordingFile = 
recordingFileClass.getConstructor(Path.class).newInstance(dump);
+      final java.lang.reflect.Method hasMoreEvents = 
recordingFileClass.getMethod("hasMoreEvents");
+      final java.lang.reflect.Method readEvent = 
recordingFileClass.getMethod("readEvent");
+      final java.lang.reflect.Method close = 
recordingFileClass.getMethod("close");
+
+      long byteBufferAllocCount = 0;
+      long byteBufferAllocBytes = 0;
+      try {
+        while ((Boolean) hasMoreEvents.invoke(recordingFile)) {
+          final Object event = readEvent.invoke(recordingFile);
+          final Class<?> eventClass = event.getClass();
+          final Object eventType = 
eventClass.getMethod("getEventType").invoke(event);
+          final String eventName = (String) 
eventType.getClass().getMethod("getName")
+              .invoke(eventType);
+          if (!"jdk.ObjectAllocationInNewTLAB".equals(eventName)) {
+            continue;
+          }
+          final Object objectClass = eventClass.getMethod("getValue", 
String.class)
+              .invoke(event, "objectClass");
+          if (objectClass == null) {
+            continue;
+          }
+          final String className = (String) 
objectClass.getClass().getMethod("getName")
+              .invoke(objectClass);
+          if (className.contains("ByteBuffer")) {
+            byteBufferAllocCount++;
+            byteBufferAllocBytes += (Long) eventClass.getMethod("getLong", 
String.class)
+                .invoke(event, "allocationSize");
+          }
+        }
+      } finally {
+        close.invoke(recordingFile);
+      }
+      return new AllocationStats(byteBufferAllocCount, byteBufferAllocBytes);
+    } finally {
+      Files.deleteIfExists(dump);
+    }
+  }
+
+  private static boolean probeJfr() {
+    try {
+      final Class<?> frClass = Class.forName("jdk.jfr.FlightRecorder");
+      return (Boolean) frClass.getMethod("isAvailable").invoke(null);
+    } catch (ReflectiveOperationException e) {
+      return false;
+    }
+  }
+
+  static final class AllocationStats {
+    private final long byteBufferAllocCount;
+    private final long byteBufferAllocBytes;
+
+    private AllocationStats(long byteBufferAllocCount, long 
byteBufferAllocBytes) {
+      this.byteBufferAllocCount = byteBufferAllocCount;
+      this.byteBufferAllocBytes = byteBufferAllocBytes;
+    }
+
+    long getByteBufferAllocCount() {
+      return byteBufferAllocCount;
+    }
+
+    long getByteBufferAllocBytes() {
+      return byteBufferAllocBytes;
+    }
+  }
+}
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChunkBuffer.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChunkBuffer.java
index d8520bebb8a..3c4ba482268 100644
--- 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChunkBuffer.java
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChunkBuffer.java
@@ -20,17 +20,23 @@
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import com.google.common.collect.ImmutableList;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.IntFunction;
+import java.util.stream.Stream;
 import org.apache.hadoop.hdds.utils.MockGatheringChannel;
 import org.apache.hadoop.hdds.utils.db.CodecBuffer;
 import org.apache.hadoop.hdds.utils.db.CodecTestUtil;
@@ -38,6 +44,9 @@
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * Test {@link ChunkBuffer} implementations.
@@ -57,6 +66,184 @@ public void after() throws Exception {
     CodecTestUtil.gc();
   }
 
+  static Stream<Arguments> chunkBufferTypes() {
+    return Stream.of(
+        Arguments.of("ByteBuffer", (IntFunction<ChunkBuffer>) 
ChunkBuffer::allocate),
+        Arguments.of("Incremental", (IntFunction<ChunkBuffer>) cap ->
+            ChunkBuffer.allocate(cap, Math.max(1, cap / 3))),
+        Arguments.of("ByteBufferList", (IntFunction<ChunkBuffer>) cap -> {
+          final int first = cap / 2;
+          return ChunkBuffer.wrap(ImmutableList.of(
+              ByteBuffer.allocate(first),
+              ByteBuffer.allocate(cap - first)));
+        }));
+  }
+
+  static Stream<Arguments> putTestCases() {
+    final List<Arguments> cases = new ArrayList<>();
+    chunkBufferTypes().forEach(impl -> {
+      final String name = (String) impl.get()[0];
+      @SuppressWarnings("unchecked")
+      final IntFunction<ChunkBuffer> factory =
+          (IntFunction<ChunkBuffer>) impl.get()[1];
+      for (PutMethod method : PutMethod.values()) {
+        cases.add(Arguments.of(name, factory, method));
+      }
+    });
+    return cases.stream();
+  }
+
+  @ParameterizedTest(name = "put rejects null [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putRejectsNull(String ignored, IntFunction<ChunkBuffer> factory,
+      PutMethod method) {
+    try (ChunkBuffer buffer = factory.apply(16)) {
+      method.assertRejectsNull(buffer);
+    }
+  }
+
+  @ParameterizedTest(name = "put(byte[]) rejects negative offset/length [{0}]")
+  @MethodSource("chunkBufferTypes")
+  void putByteArrayRejectsNegativeOffsetOrLength(String ignored,
+      IntFunction<ChunkBuffer> factory) {
+    final byte[] source = new byte[8];
+    try (ChunkBuffer buffer = factory.apply(16)) {
+      assertThrows(IllegalArgumentException.class, () -> buffer.put(source, 
-1, 4));
+      assertThrows(IllegalArgumentException.class, () -> buffer.put(source, 0, 
-1));
+    }
+  }
+
+  @ParameterizedTest(name = "put(byte[]) rejects out-of-range slice [{0}]")
+  @MethodSource("chunkBufferTypes")
+  void putByteArrayRejectsOutOfRangeSlice(String ignored,
+      IntFunction<ChunkBuffer> factory) {
+    final byte[] source = new byte[8];
+    try (ChunkBuffer buffer = factory.apply(16)) {
+      assertThrows(IllegalArgumentException.class, () -> buffer.put(source, 5, 
4));
+      assertThrows(IllegalArgumentException.class, () -> buffer.put(source, 0, 
9));
+    }
+  }
+
+  @ParameterizedTest(name = "put rejects buffer overflow [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putRejectsBufferOverflow(String type, IntFunction<ChunkBuffer> factory,
+      PutMethod method) {
+    final byte[] source = new byte[8];
+    try (ChunkBuffer buffer = factory.apply(4)) {
+      final BufferOverflowException overflow = assertThrows(
+          BufferOverflowException.class,
+          () -> method.putSlice(buffer, source, 0, 5));
+      if (method.expectsCheckArgumentOverflowCause(type)) {
+        assertInstanceOf(IllegalArgumentException.class, overflow.getCause());
+      }
+    }
+  }
+
+  @ParameterizedTest(name = "put rejects overflow after partial fill [{0} via 
{1}]")
+  @MethodSource("putTestCases")
+  void putRejectsOverflowAfterPartialFill(String type, 
IntFunction<ChunkBuffer> factory,
+      PutMethod method) throws IOException {
+    final byte[] first = {1, 2, 3, 4, 5};
+    final byte[] second = {6, 7, 8, 9};
+    try (ChunkBuffer buffer = factory.apply(8)) {
+      method.putSlice(buffer, first, 0, 5);
+      assertEquals(5, buffer.position());
+      final BufferOverflowException overflow = assertThrows(
+          BufferOverflowException.class, () -> method.putSlice(buffer, second, 
0, 4));
+      if (method.expectsCheckArgumentOverflowCause(type)) {
+        assertInstanceOf(IllegalArgumentException.class, overflow.getCause());
+      }
+      assertArrayEquals(first, readWrittenBytes(buffer));
+    }
+  }
+
+  @ParameterizedTest(name = "put writes array slice [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putWritesSlice(String ignored, IntFunction<ChunkBuffer> factory, 
PutMethod method)
+      throws IOException {
+    final byte[] source = {1, 2, 3, 4, 5, 6, 7, 8};
+    try (ChunkBuffer buffer = factory.apply(4)) {
+      method.putSlice(buffer, source, 2, 4);
+      assertEquals(4, buffer.position());
+      assertEquals(0, buffer.remaining());
+      assertArrayEquals(new byte[] {3, 4, 5, 6}, readWrittenBytes(buffer));
+    }
+  }
+
+  @ParameterizedTest(name = "put writes randomized slice [{0} via {2}]")
+  @MethodSource("putTestCases")
+  void putWritesRandomizedSlice(String ignored, IntFunction<ChunkBuffer> 
factory,
+      PutMethod method) throws IOException {
+    // Randomize buffer size, data size, offset and length (per review 
feedback:
+    // exercise both put paths the way testImplWithByteBuffer randomizes 
inputs).
+    for (int iter = 0; iter < 20; iter++) {
+      final int capacity = nextInt(1 << 10) + 1;
+      final int length = nextInt(capacity + 1);
+      final byte[] source = new byte[length + nextInt(64)];
+      ThreadLocalRandom.current().nextBytes(source);
+      final int offset = nextInt(source.length - length + 1);
+      try (ChunkBuffer buffer = factory.apply(capacity)) {
+        method.putSlice(buffer, source, offset, length);
+        assertEquals(length, buffer.position());
+        assertArrayEquals(Arrays.copyOfRange(source, offset, offset + length),
+            readWrittenBytes(buffer));
+      }
+    }
+  }
+
+  @ParameterizedTest(name = "put zero-length payload is no-op [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putZeroLengthPayloadIsNoOp(String ignored, IntFunction<ChunkBuffer> 
factory,
+      PutMethod method) throws IOException {
+    final byte[] source = {1, 2, 3, 4};
+    try (ChunkBuffer buffer = factory.apply(4)) {
+      method.putSlice(buffer, source, 0, 2);
+      method.putEmptySlice(buffer, source, 2, 2);
+      assertEquals(2, buffer.position());
+      assertArrayEquals(new byte[] {1, 2}, readWrittenBytes(buffer));
+    }
+  }
+
+  @ParameterizedTest(name = "put fills entire buffer [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putFillsEntireBuffer(String ignored, IntFunction<ChunkBuffer> factory,
+      PutMethod method) throws IOException {
+    final byte[] source = new byte[32];
+    ThreadLocalRandom.current().nextBytes(source);
+    try (ChunkBuffer buffer = factory.apply(source.length)) {
+      method.putAll(buffer, source);
+      assertEquals(source.length, buffer.position());
+      assertArrayEquals(source, readWrittenBytes(buffer));
+    }
+  }
+
+  @ParameterizedTest(name = "put spans incremental chunks [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putSpansIncrementalChunks(String ignored, IntFunction<ChunkBuffer> 
ignoredFactory,
+      PutMethod method) throws IOException {
+    final byte[] source = new byte[10];
+    ThreadLocalRandom.current().nextBytes(source);
+    try (ChunkBuffer buffer = ChunkBuffer.allocate(10, 3)) {
+      method.putSlice(buffer, source, 1, 7);
+      assertEquals(7, buffer.position());
+      assertArrayEquals(Arrays.copyOfRange(source, 1, 8), 
readWrittenBytes(buffer));
+    }
+  }
+
+  @ParameterizedTest(name = "put spans list chunks [{0} via {1}]")
+  @MethodSource("putTestCases")
+  void putSpansListChunks(String ignored, IntFunction<ChunkBuffer> 
ignoredFactory,
+      PutMethod method) throws IOException {
+    final byte[] source = new byte[10];
+    ThreadLocalRandom.current().nextBytes(source);
+    try (ChunkBuffer buffer = ChunkBuffer.wrap(ImmutableList.of(
+        ByteBuffer.allocate(4), ByteBuffer.allocate(6)))) {
+      method.putSlice(buffer, source, 1, 7);
+      assertEquals(7, buffer.position());
+      assertArrayEquals(Arrays.copyOfRange(source, 1, 8), 
readWrittenBytes(buffer));
+    }
+  }
+
   @Test
   void testImplWithByteBuffer() throws IOException {
     runTestImplWithByteBuffer(1);
@@ -226,4 +413,88 @@ private static void assertWrite(byte[] expected, 
ChunkBuffer impl) throws IOExce
     assertArrayEquals(expected, output.toByteArray());
     assertFalse(impl.hasRemaining());
   }
+
+  private static byte[] readWrittenBytes(ChunkBuffer buffer) throws 
IOException {
+    final int written = buffer.position();
+    final ChunkBuffer slice = buffer.duplicate(0, written);
+    final ByteArrayOutputStream output = new ByteArrayOutputStream(written);
+    slice.writeTo(new MockGatheringChannel(Channels.newChannel(output)));
+    return output.toByteArray();
+  }
+
+  private static ByteBuffer emptySlice(byte[] source, int offset, int length) {
+    final ByteBuffer slice = ByteBuffer.wrap(source, offset, length);
+    slice.position(slice.limit());
+    return slice;
+  }
+
+  /**
+   * Exercises {@link ChunkBuffer#put(byte[], int, int)} vs {@link 
ChunkBuffer#put(ByteBuffer)}
+   * with equivalent payloads.
+   */
+  private enum PutMethod {
+    BYTE_ARRAY {
+      @Override
+      void putSlice(ChunkBuffer buffer, byte[] source, int offset, int length) 
{
+        buffer.put(source, offset, length);
+      }
+
+      @Override
+      void putAll(ChunkBuffer buffer, byte[] source) {
+        buffer.put(source);
+      }
+
+      @Override
+      void putEmptySlice(ChunkBuffer buffer, byte[] source, int offset, int 
length) {
+        buffer.put(source, offset, 0);
+      }
+
+      @Override
+      void assertRejectsNull(ChunkBuffer buffer) {
+        assertThrows(NullPointerException.class, () -> buffer.put((byte[]) 
null));
+        assertThrows(NullPointerException.class, () -> buffer.put(null, 0, 0));
+      }
+
+      @Override
+      boolean expectsCheckArgumentOverflowCause(String implType) {
+        return true;
+      }
+    },
+    BYTE_BUFFER {
+      @Override
+      void putSlice(ChunkBuffer buffer, byte[] source, int offset, int length) 
{
+        buffer.put(ByteBuffer.wrap(source, offset, length));
+      }
+
+      @Override
+      void putAll(ChunkBuffer buffer, byte[] source) {
+        buffer.put(ByteBuffer.wrap(source));
+      }
+
+      @Override
+      void putEmptySlice(ChunkBuffer buffer, byte[] source, int offset, int 
length) {
+        buffer.put(emptySlice(source, offset, length));
+      }
+
+      @Override
+      void assertRejectsNull(ChunkBuffer buffer) {
+        assertThrows(NullPointerException.class, () -> buffer.put((ByteBuffer) 
null));
+      }
+
+      @Override
+      boolean expectsCheckArgumentOverflowCause(String implType) {
+        return !"ByteBuffer".equals(implType);
+      }
+    };
+
+    abstract void putSlice(ChunkBuffer buffer, byte[] source, int offset, int 
length);
+
+    abstract void putAll(ChunkBuffer buffer, byte[] source);
+
+    abstract void putEmptySlice(ChunkBuffer buffer, byte[] source, int offset, 
int length);
+
+    abstract void assertRejectsNull(ChunkBuffer buffer);
+
+    abstract boolean expectsCheckArgumentOverflowCause(String implType);
+  }
 }


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

Reply via email to