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 adea16888a2 HDDS-16742. Read cmdType from the request descriptor
instead of rendering it to text (#11517)
adea16888a2 is described below
commit adea16888a280a41da5a36fdd9c8aa9fee98741d
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Wed Oct 14 17:07:43 2026 -0400
HDDS-16742. Read cmdType from the request descriptor instead of rendering
it to text (#11517)
Generated-by: Claude Code (Claude Opus 4.8)
---
.../hadoop/ozone/grpc/metrics/GrpcMetrics.java | 3 +-
.../GrpcMetricsServerRequestInterceptor.java | 18 +-
.../TestGrpcMetricsServerRequestInterceptor.java | 101 +++++
.../om/TestGrpcMetricsInterceptorBenchmark.java | 424 +++++++++++++++++++++
4 files changed, 536 insertions(+), 10 deletions(-)
diff --git
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetrics.java
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetrics.java
index bc39d9bf6ca..b7e9cacd06b 100644
---
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetrics.java
+++
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetrics.java
@@ -61,7 +61,8 @@ public class GrpcMetrics implements MetricsSource {
private final MetricsRegistry registry;
private final boolean grpcQuantileEnable;
- private String requestType;
+ // written by the gRPC handler threads and read by the metrics snapshot
+ private volatile String requestType;
@Metric("Number of sent bytes")
private MutableCounterLong sentBytes;
diff --git
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetricsServerRequestInterceptor.java
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetricsServerRequestInterceptor.java
index 45041fec504..55c7f93741e 100644
---
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetricsServerRequestInterceptor.java
+++
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/grpc/metrics/GrpcMetricsServerRequestInterceptor.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.grpc.metrics;
import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.Descriptors;
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
@@ -29,6 +30,8 @@
*/
public class GrpcMetricsServerRequestInterceptor implements ServerInterceptor {
+ private static final String CMD_TYPE_FIELD = "cmdType";
+
private final GrpcMetrics grpcMetrics;
private long receivedTime;
private long startTime;
@@ -60,21 +63,18 @@ public void onMessage(ReqT message) {
if (message instanceof AbstractMessage) {
AbstractMessage parsedMessage = (AbstractMessage) message;
messageSize += parsedMessage.getSerializedSize();
+ // Read the field directly instead of rendering the whole request to
text
+ Descriptors.FieldDescriptor cmdType =
+
parsedMessage.getDescriptorForType().findFieldByName(CMD_TYPE_FIELD);
+ if (cmdType != null && parsedMessage.hasField(cmdType)) {
+
grpcMetrics.setRequestType(parsedMessage.getField(cmdType).toString());
+ }
} else {
grpcMetrics.incrUnknownMessagesReceived();
}
grpcMetrics.incrReceivedBytes(messageSize);
- String[] messageFields = message.toString()
- .split(System.lineSeparator());
- // messageFields[0] should be in the format
- // cmdType: "type"
- String[] cmdTypeLine = messageFields[0].split(":");
-
- // Get only the type and remove any leading spaces
- grpcMetrics.setRequestType(cmdTypeLine[1].trim());
-
super.onMessage(message);
}
diff --git
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/ozone/grpc/metrics/TestGrpcMetricsServerRequestInterceptor.java
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/ozone/grpc/metrics/TestGrpcMetricsServerRequestInterceptor.java
new file mode 100644
index 00000000000..ac2a4045205
--- /dev/null
+++
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/ozone/grpc/metrics/TestGrpcMetricsServerRequestInterceptor.java
@@ -0,0 +1,101 @@
+/*
+ * 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.grpc.metrics;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import
org.apache.hadoop.hdds.protocol.proto.ScmBlockLocationProtocolProtos.SCMBlockLocationRequest;
+import
org.apache.hadoop.hdds.protocol.proto.ScmBlockLocationProtocolProtos.Type;
+import
org.apache.hadoop.hdds.protocol.proto.ScmBlockLocationProtocolProtos.UserInfo;
+import org.apache.hadoop.metrics2.lib.MetricsAnnotations;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link GrpcMetricsServerRequestInterceptor}.
+ *
+ * <p>{@code SCMBlockLocationRequest} stands in for {@code OMRequest}, which
this module cannot depend on: both have
+ * {@code cmdType} as field 1 and a free-text {@code traceID} as field 2.
+ */
+public class TestGrpcMetricsServerRequestInterceptor {
+
+ private GrpcMetrics metrics;
+ private ServerCall.Listener<Object> delegate;
+ private ServerCall.Listener<Object> listener;
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ metrics = new GrpcMetrics(new OzoneConfiguration());
+ // populates the @Metric counters, as DefaultMetricsSystem.register() does
+ MetricsAnnotations.makeSource(metrics);
+
+ delegate = mock(ServerCall.Listener.class);
+ ServerCallHandler<Object, Object> handler = mock(ServerCallHandler.class);
+ when(handler.startCall(any(), any())).thenReturn(delegate);
+ listener = new GrpcMetricsServerRequestInterceptor(metrics)
+ .interceptCall(mock(ServerCall.class), new Metadata(), handler);
+ }
+
+ @Test
+ public void testRequestTypeIsReadFromCmdType() {
+ SCMBlockLocationRequest request = SCMBlockLocationRequest.newBuilder()
+ .setCmdType(Type.GetScmInfo)
+ .setTraceID("trace:with:colons" + System.lineSeparator() + "next:
line")
+ .build();
+
+ listener.onMessage(request);
+
+ assertEquals("GetScmInfo", metrics.getRequestType());
+ assertEquals(request.getSerializedSize(), metrics.getReceivedBytes());
+ assertEquals(0, metrics.getUnknownMessagesReceived());
+ verify(delegate).onMessage(request);
+ }
+
+ @Test
+ public void testMessageWithoutCmdTypeKeepsRequestType() {
+ UserInfo message = UserInfo.newBuilder().setUserName("user: name").build();
+
+ listener.onMessage(message);
+
+ assertEquals("NoRequest", metrics.getRequestType());
+ assertEquals(message.getSerializedSize(), metrics.getReceivedBytes());
+ verify(delegate).onMessage(message);
+ }
+
+ @Test
+ public void testNonProtobufMessageIsCountedAsUnknown() {
+ // the previous text parsing threw ArrayIndexOutOfBoundsException for a
message without ':' in its first line
+ String message = "not a protobuf message";
+
+ listener.onMessage(message);
+
+ assertEquals("NoRequest", metrics.getRequestType());
+ assertEquals(1, metrics.getUnknownMessagesReceived());
+ assertEquals(0, metrics.getReceivedBytes());
+ verify(delegate).onMessage(message);
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcMetricsInterceptorBenchmark.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcMetricsInterceptorBenchmark.java
new file mode 100644
index 00000000000..ca53639824e
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcMetricsInterceptorBenchmark.java
@@ -0,0 +1,424 @@
+/*
+ * 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.om;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ByteString;
+import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.metrics2.lib.MetricsAnnotations;
+import org.apache.hadoop.ozone.grpc.metrics.GrpcMetrics;
+import
org.apache.hadoop.ozone.grpc.metrics.GrpcMetricsServerRequestInterceptor;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateKeyRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyArgs;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeysRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LookupKeyRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OzoneAclInfo;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Benchmark for HDDS-16742: the cost of setting the {@code LatestRequestType}
tag in
+ * {@link GrpcMetricsServerRequestInterceptor#interceptCall}'s {@code
onMessage}.
+ *
+ * <p>Two arms run in the same JVM, interleaved, on the same {@code OMRequest}
objects:
+ * <ul>
+ * <li>{@code text}: the removed implementation, copied below in {@link
TextFormatRequestInterceptor}. It renders
+ * the whole request with {@code toString()} and parses the first
line.</li>
+ * <li>{@code descriptor}: the current {@link
GrpcMetricsServerRequestInterceptor}, which reads {@code cmdType}
+ * through the message descriptor.</li>
+ * </ul>
+ * Everything else the listener does (serialized size, received-bytes counter,
forwarding to a no-op delegate) is
+ * identical in both arms, so the difference is the request-type extraction.
+ *
+ * <p>Reported per request size: single-thread and {@link #THREADS}-thread
throughput (median of
+ * {@link #ROUNDS} rounds), per-call latency percentiles (single thread, over
all rounds; each sample
+ * includes the cost of the {@code System.nanoTime} calls around it), bytes
allocated per call (from the per-thread
+ * allocation counter of the JVM), and young/old GC count and time during the
multi-thread measurement.
+ *
+ * <p>Run with:
+ * <pre>
+ * mvn -pl :ozone-manager test -Dtest=TestGrpcMetricsInterceptorBenchmark
-Dexcluded-test-groups= \
+ * -DskipShade -DskipRecon
+ * </pre>
+ */
+@Tag("benchmark")
+public class TestGrpcMetricsInterceptorBenchmark {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TestGrpcMetricsInterceptorBenchmark.class);
+
+ private static final long WARMUP_MS = 2_000;
+ private static final long MEASURE_MS = 4_000;
+ private static final int ROUNDS = 3;
+ private static final int THREADS = 8;
+ private static final int LATENCY_SAMPLES = 50_000;
+ private static final int ALLOCATION_CALLS = 20_000;
+
+ private static final com.sun.management.ThreadMXBean THREAD_MX =
+ (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
+
+ @Test
+ // The 5 minute default timeout of the build applies unless a method-level
@Timeout overrides it.
+ @Timeout(value = 30, unit = TimeUnit.MINUTES)
+ void benchmarkRequestTypeExtraction() throws Exception {
+ assertTrue(THREAD_MX.isThreadAllocatedMemorySupported(), "needs per-thread
allocation counters");
+ THREAD_MX.setThreadAllocatedMemoryEnabled(true);
+
+ List<OMRequest> requests = Arrays.asList(lookupKey(), createKey(),
deleteKeys(1000));
+ StringBuilder report = new StringBuilder(String.format("%n"
+ + "BENCHRESULT gRPC metrics request interceptor, LatestRequestType
extraction (HDDS-16742)%n"
+ + " rounds=%d (warm-up %d ms, measure %d ms each), multi-thread=%d
threads; text = toString() parsing,"
+ + " descriptor = field read%n", ROUNDS, WARMUP_MS, MEASURE_MS,
THREADS));
+
+ for (OMRequest request : requests) {
+ // both arms must report the same tag value
+ assertEquals(request.getCmdType().name(), extractOnce(Arm.TEXT,
request));
+ assertEquals(request.getCmdType().name(), extractOnce(Arm.DESCRIPTOR,
request));
+
+ Result text = new Result();
+ Result descriptor = new Result();
+ for (int round = 0; round < ROUNDS; round++) {
+ // alternate which arm goes first in each round
+ Arm first = (round & 1) == 0 ? Arm.TEXT : Arm.DESCRIPTOR;
+ Arm second = first == Arm.TEXT ? Arm.DESCRIPTOR : Arm.TEXT;
+ measure(first, request, first == Arm.TEXT ? text : descriptor);
+ measure(second, request, second == Arm.TEXT ? text : descriptor);
+ }
+ report.append(format(request, text, descriptor));
+ }
+ LOG.info(report.toString());
+ }
+
+ private enum Arm { TEXT, DESCRIPTOR }
+
+ private void measure(Arm arm, OMRequest request, Result result) throws
Exception {
+ result.singleThreadOps.add(throughput(arm, request, 1));
+ result.allocatedBytesPerOp.add(allocatedBytesPerOp(arm, request));
+ result.latencies.add(latencies(arm, request));
+ GcSnapshot gcBefore = GcSnapshot.now();
+ result.multiThreadOps.add(throughput(arm, request, THREADS));
+ GcSnapshot gcAfter = GcSnapshot.now();
+ result.gcCount.add((double) (gcAfter.count - gcBefore.count));
+ result.gcMillis.add((double) (gcAfter.millis - gcBefore.millis));
+ }
+
+ /** Runs {@code threads} threads, each with its own listener, and returns
the total calls per second. */
+ private double throughput(Arm arm, OMRequest request, int threads) throws
Exception {
+ GrpcMetrics metrics = newMetrics();
+ long[] counts = new long[threads];
+ CountDownLatch done = new CountDownLatch(threads);
+ CountDownLatch start = new CountDownLatch(1);
+ for (int t = 0; t < threads; t++) {
+ final int index = t;
+ Thread worker = new Thread(() -> {
+ ServerCall.Listener<Object> listener = listener(arm, metrics);
+ try {
+ start.await();
+ long warmupEnd = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(WARMUP_MS);
+ while (System.nanoTime() < warmupEnd) {
+ listener.onMessage(request);
+ }
+ long measureEnd = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(MEASURE_MS);
+ long n = 0;
+ // check the clock every 256 calls to keep its cost out of the loop
+ while (true) {
+ for (int i = 0; i < 256; i++) {
+ listener.onMessage(request);
+ }
+ n += 256;
+ if (System.nanoTime() >= measureEnd) {
+ break;
+ }
+ }
+ counts[index] = n;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ done.countDown();
+ }
+ });
+ worker.start();
+ }
+ start.countDown();
+ done.await();
+ long total = 0;
+ for (long c : counts) {
+ total += c;
+ }
+ return total / (MEASURE_MS / 1000.0);
+ }
+
+ /** Bytes allocated by the calling thread per {@code onMessage} call, after
a warm-up. */
+ private double allocatedBytesPerOp(Arm arm, OMRequest request) {
+ ServerCall.Listener<Object> listener = listener(arm, newMetrics());
+ for (int i = 0; i < ALLOCATION_CALLS; i++) {
+ listener.onMessage(request);
+ }
+ int calls = ALLOCATION_CALLS;
+ long tid = Thread.currentThread().getId();
+ long before = THREAD_MX.getThreadAllocatedBytes(tid);
+ for (int i = 0; i < calls; i++) {
+ listener.onMessage(request);
+ }
+ long after = THREAD_MX.getThreadAllocatedBytes(tid);
+ return (after - before) / (double) calls;
+ }
+
+ /** Per-call latencies in nanoseconds, single thread, after a warm-up. */
+ private long[] latencies(Arm arm, OMRequest request) {
+ ServerCall.Listener<Object> listener = listener(arm, newMetrics());
+ for (int i = 0; i < LATENCY_SAMPLES; i++) {
+ listener.onMessage(request);
+ }
+ long[] samples = new long[LATENCY_SAMPLES];
+ for (int i = 0; i < LATENCY_SAMPLES; i++) {
+ long t0 = System.nanoTime();
+ listener.onMessage(request);
+ samples[i] = System.nanoTime() - t0;
+ }
+ return samples;
+ }
+
+ private static String extractOnce(Arm arm, OMRequest request) {
+ GrpcMetrics metrics = newMetrics();
+ listener(arm, metrics).onMessage(request);
+ return metrics.getRequestType();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ServerCall.Listener<Object> listener(Arm arm, GrpcMetrics
metrics) {
+ ServerInterceptor interceptor = arm == Arm.TEXT
+ ? new TextFormatRequestInterceptor(metrics)
+ : new GrpcMetricsServerRequestInterceptor(metrics);
+ ServerCallHandler<Object, Object> handler = (call, headers) -> new
ServerCall.Listener<Object>() { };
+ return interceptor.interceptCall(mock(ServerCall.class), new Metadata(),
handler);
+ }
+
+ private static GrpcMetrics newMetrics() {
+ GrpcMetrics metrics = new GrpcMetrics(new OzoneConfiguration());
+ // populates the @Metric counters, as DefaultMetricsSystem.register() does
+ MetricsAnnotations.makeSource(metrics);
+ return metrics;
+ }
+
+ /**
+ * The {@code onMessage} logic removed by HDDS-16742, copied verbatim, so
both arms can run in one JVM. The
+ * timing fields and {@code onComplete} are not used by this benchmark and
are omitted.
+ */
+ private static final class TextFormatRequestInterceptor implements
ServerInterceptor {
+ private final GrpcMetrics grpcMetrics;
+
+ TextFormatRequestInterceptor(GrpcMetrics grpcMetrics) {
+ this.grpcMetrics = grpcMetrics;
+ }
+
+ @Override
+ public <ReqT, RespT> ServerCall.Listener<ReqT>
interceptCall(ServerCall<ReqT, RespT> serverCall,
+ Metadata headers, ServerCallHandler<ReqT, RespT> serverCallHandler) {
+ return new
SimpleForwardingServerCallListener<ReqT>(serverCallHandler.startCall(serverCall,
headers)) {
+ @Override
+ public void onMessage(ReqT message) {
+ long messageSize = 0;
+ if (message instanceof AbstractMessage) {
+ AbstractMessage parsedMessage = (AbstractMessage) message;
+ messageSize += parsedMessage.getSerializedSize();
+ } else {
+ grpcMetrics.incrUnknownMessagesReceived();
+ }
+
+ grpcMetrics.incrReceivedBytes(messageSize);
+
+ String[] messageFields = message.toString()
+ .split(System.lineSeparator());
+ // messageFields[0] should be in the format
+ // cmdType: "type"
+ String[] cmdTypeLine = messageFields[0].split(":");
+
+ // Get only the type and remove any leading spaces
+ grpcMetrics.setRequestType(cmdTypeLine[1].trim());
+
+ super.onMessage(message);
+ }
+ };
+ }
+ }
+
+ private static OMRequest.Builder baseRequest(Type type) {
+ return OMRequest.newBuilder()
+ .setCmdType(type)
+ .setClientId("client-0f3a6c2e-5d5b-4b0e-9c62-7a3c1e0b8f41")
+ .setTraceID("4f1c2d3e4a5b6c7d:8e9f0a1b2c3d4e5f:0:1")
+
.setUserInfo(UserInfo.newBuilder().setUserName("hadoop").setRemoteAddress("10.0.0.17").build());
+ }
+
+ private static KeyArgs.Builder keyArgs(String keyName) {
+ return KeyArgs.newBuilder()
+ .setVolumeName("vol1")
+ .setBucketName("bucket1")
+ .setKeyName(keyName);
+ }
+
+ /** A small read request. */
+ private static OMRequest lookupKey() {
+ return baseRequest(Type.LookupKey)
+ .setLookupKeyRequest(LookupKeyRequest.newBuilder()
+
.setKeyArgs(keyArgs("dir1/dir2/part-00000.parquet").setSortDatanodes(true).setLatestVersionLocation(true)))
+ .build();
+ }
+
+ /** A typical write: key arguments with ACLs and user metadata. */
+ private static OMRequest createKey() {
+ KeyArgs.Builder args =
keyArgs("warehouse/sales/date=2026-10-13/part-00042-c000.snappy.parquet")
+ .setDataSize(134_217_728L)
+ .setType(HddsProtos.ReplicationType.RATIS)
+ .setFactor(HddsProtos.ReplicationFactor.THREE)
+ .setModificationTime(1_791_900_000_000L);
+ for (int i = 0; i < 4; i++) {
+ args.addAcls(OzoneAclInfo.newBuilder()
+ .setType(i == 0 ? OzoneAclInfo.OzoneAclType.USER :
OzoneAclInfo.OzoneAclType.GROUP)
+ .setName(i == 0 ? "hadoop" : "group" + i)
+ .setRights(ByteString.copyFrom(new byte[] {(byte) 0x7f, 0x01}))
+ .setAclScope(OzoneAclInfo.OzoneAclScope.ACCESS));
+ }
+ for (int i = 0; i < 4; i++) {
+
args.addMetadata(HddsProtos.KeyValue.newBuilder().setKey("x-amz-meta-attr" +
i).setValue("value-" + i));
+ }
+ return baseRequest(Type.CreateKey)
+
.setCreateKeyRequest(CreateKeyRequest.newBuilder().setKeyArgs(args).setClientID(123_456_789L))
+ .build();
+ }
+
+ /** A large request: one delete of {@code n} keys, the shape of an S3
multi-object delete. */
+ private static OMRequest deleteKeys(int n) {
+ DeleteKeyArgs.Builder args =
DeleteKeyArgs.newBuilder().setVolumeName("vol1").setBucketName("bucket1");
+ for (int i = 0; i < n; i++) {
+ args.addKeys(String.format("logs/2026/10/13/app-%05d.log.gz", i));
+ }
+ return baseRequest(Type.DeleteKeys)
+
.setDeleteKeysRequest(DeleteKeysRequest.newBuilder().setDeleteKeys(args))
+ .build();
+ }
+
+ private static String format(OMRequest request, Result text, Result
descriptor) {
+ long[] t = Result.merged(text.latencies);
+ long[] d = Result.merged(descriptor.latencies);
+ return String.format("%n %s (%,d bytes serialized)%n"
+ + " %-28s %14s %14s %9s%n"
+ + " %-28s %14.0f %14.0f %8.1fx%n"
+ + " %-28s %14.0f %14.0f %8.1fx%n"
+ + " %-28s %14.0f %14.0f %8.1fx%n"
+ + " %-28s %14d %14d%n"
+ + " %-28s %14d %14d%n"
+ + " %-28s %14d %14d%n"
+ + " %-28s %14.0f %14.0f%n"
+ + " %-28s %14.0f %14.0f%n",
+ request.getCmdType(), request.getSerializedSize(),
+ "", "text", "descriptor", "change",
+ "ops/s, 1 thread", median(text.singleThreadOps),
median(descriptor.singleThreadOps),
+ median(descriptor.singleThreadOps) / median(text.singleThreadOps),
+ "ops/s, " + THREADS + " threads", median(text.multiThreadOps),
median(descriptor.multiThreadOps),
+ median(descriptor.multiThreadOps) / median(text.multiThreadOps),
+ "bytes allocated / call", median(text.allocatedBytesPerOp),
median(descriptor.allocatedBytesPerOp),
+ median(text.allocatedBytesPerOp) / Math.max(1.0,
median(descriptor.allocatedBytesPerOp)),
+ "latency p50 ns", percentile(t, 0.50), percentile(d, 0.50),
+ "latency p99 ns", percentile(t, 0.99), percentile(d, 0.99),
+ "latency p99.9 ns", percentile(t, 0.999), percentile(d, 0.999),
+ "GCs, " + THREADS + "-thread runs", median(text.gcCount),
median(descriptor.gcCount),
+ "GC ms, " + THREADS + "-thread runs", median(text.gcMillis),
median(descriptor.gcMillis));
+ }
+
+ private static double median(List<Double> values) {
+ List<Double> sorted = new ArrayList<>(values);
+ sorted.sort(null);
+ return sorted.get(sorted.size() / 2);
+ }
+
+ private static long percentile(long[] sorted, double p) {
+ int index = (int) Math.ceil(p * sorted.length) - 1;
+ return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
+ }
+
+ /** Measurements of one arm for one request, collected over all rounds. */
+ private static final class Result {
+ private final List<Double> singleThreadOps = new ArrayList<>();
+ private final List<Double> multiThreadOps = new ArrayList<>();
+ private final List<Double> allocatedBytesPerOp = new ArrayList<>();
+ private final List<Double> gcCount = new ArrayList<>();
+ private final List<Double> gcMillis = new ArrayList<>();
+ private final List<long[]> latencies = new ArrayList<>();
+
+ static long[] merged(List<long[]> samples) {
+ int size = 0;
+ for (long[] s : samples) {
+ size += s.length;
+ }
+ long[] all = new long[size];
+ int offset = 0;
+ for (long[] s : samples) {
+ System.arraycopy(s, 0, all, offset, s.length);
+ offset += s.length;
+ }
+ Arrays.sort(all);
+ return all;
+ }
+ }
+
+ /** Total collection count and time over all garbage collectors. */
+ private static final class GcSnapshot {
+ private final long count;
+ private final long millis;
+
+ private GcSnapshot(long count, long millis) {
+ this.count = count;
+ this.millis = millis;
+ }
+
+ static GcSnapshot now() {
+ long count = 0;
+ long millis = 0;
+ for (GarbageCollectorMXBean gc :
ManagementFactory.getGarbageCollectorMXBeans()) {
+ count += Math.max(0, gc.getCollectionCount());
+ millis += Math.max(0, gc.getCollectionTime());
+ }
+ return new GcSnapshot(count, millis);
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]