/* * Copyright 2025 The Netty Project * * The Netty Project 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: * * https://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 io.github.neoionet.netty.microbenchmark; import io.github.neoionet.netty.microbenchmark.data.WebSocketProxyPattern; import io.github.neoionet.netty.microbenchmark.data.ApiGatewayPattern; import io.netty.buffer.AdaptiveByteBufAllocator; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.PooledByteBufAllocator; import io.netty.util.ResourceLeakDetector; import io.netty.util.internal.MathUtil; import io.github.neoionet.netty.mimalloc.MiByteBufAllocator; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.CompilerControl; 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.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.TearDown; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.BenchmarkParams; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Set; import java.util.SplittableRandom; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; /** * This is a modified portion of `io.netty.microbench.buffer.ByteBufAllocatorAllocPatternBenchmark` * from the netty project. */ @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @Fork(value = 1, jvmArgs = { "-server", "-dsa", "-da", "-XX:InitialRAMPercentage=40.0", "-XX:MaxRAMPercentage=40.0", "-Dio.netty.leakDetection.level=disabled", "-Djmh.executor=CUSTOM", "-Djmh.executor.class=io.github.neoionet.netty.microbenchmark.executor.FastThreadLocalThreadHarnessExecutor" }) public class ByteBufAllocatorAllocPatternBenchmark { static { ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); } public enum SizePattern { API_GATEWAY(() -> ApiGatewayPattern.FLATTENED_SIZE_ARRAY), SOCKET_PROXY(() -> WebSocketProxyPattern.FLATTENED_SIZE_ARRAY); private final Supplier factory; SizePattern(Supplier factory) { this.factory = factory; } private int[] create() { return factory.get(); } } @Param({ "SOCKET_PROXY", "API_GATEWAY", }) public SizePattern sizePattern; private int[] sizesArray; public enum AllocatorType { ADAPTIVE(AdaptiveByteBufAllocator::new), MIMALLOC(MiByteBufAllocator::new), POOLED(() -> PooledByteBufAllocator.DEFAULT); private final Supplier factory; AllocatorType(Supplier factory) { this.factory = factory; } private ByteBufAllocator create() { return factory.get(); } } @Param({ "POOLED", "ADAPTIVE", "MIMALLOC", }) public AllocatorType allocatorType; private ByteBufAllocator allocator; @Param({ "false", "true" }) public boolean enableReadWrite; // Must be power of 2. @Param({ "128", // 128 buffers per thread "1024", // 1K buffers per thread "4096", // 4K buffers per thread "8192", // 8K buffers per thread "16384", // 16K buffers per thread "32768", // 32K buffers per thread "65536", // 64K buffers per thread }) public int MAX_LIVE_BUFFERS; public static Set benchParamsSet = ConcurrentHashMap.newKeySet(); @State(Scope.Thread) public static class AllocationPatternState { private int[] releaseIndexes; private int[] sizes; private int nextReleaseIndex; private int nextSizeIndex; private ByteBuf[] buffers; private ByteBufAllocator allocator; private boolean enableReadWrite; @Setup public void setup(ByteBufAllocatorAllocPatternBenchmark benchmark) { this.allocator = benchmark.allocator; this.enableReadWrite = benchmark.enableReadWrite; releaseIndexes = new int[benchmark.MAX_LIVE_BUFFERS]; sizes = new int[MathUtil.findNextPositivePowerOfTwo(benchmark.sizesArray.length)]; SplittableRandom rand = new SplittableRandom(42L); // Pre-generate the to be released index. for (int i = 0; i < releaseIndexes.length; i++) { releaseIndexes[i] = i; } for (int i = releaseIndexes.length - 1; i > 0; i--) { int index = rand.nextInt(i + 1); int temp = releaseIndexes[index]; releaseIndexes[index] = releaseIndexes[i]; releaseIndexes[i] = temp; } // Shuffle the `flattenedSizeArray` to `sizes`. for (int i = 0; i < sizes.length; i++) { int sizeIndex = rand.nextInt(benchmark.sizesArray.length); sizes[i] = benchmark.sizesArray[sizeIndex]; } nextReleaseIndex = 0; nextSizeIndex = 0; buffers = new ByteBuf[benchmark.MAX_LIVE_BUFFERS]; } private int getNextReleaseIndex() { int index = nextReleaseIndex; nextReleaseIndex = (nextReleaseIndex + 1) & (releaseIndexes.length - 1); return releaseIndexes[index]; } private int getNextSizeIndex() { int index = nextSizeIndex; nextSizeIndex = (nextSizeIndex + 1) & (sizes.length - 1); return index; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private static ByteBuf allocateHeap(ByteBufAllocator allocator, int size) { return allocator.heapBuffer(size); } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private static ByteBuf allocateDirect(ByteBufAllocator allocator, int size) { return allocator.directBuffer(size); } @CompilerControl(CompilerControl.Mode.DONT_INLINE) public ByteBuf performDirectAllocation() { int releaseIndex = getNextReleaseIndex(); this.readAndRelease(releaseIndex); int size = sizes[getNextSizeIndex()]; ByteBuf newBuf = allocateDirect(allocator, size); if (enableReadWrite) { newBuf.writeByte(size); } buffers[releaseIndex] = newBuf; return newBuf; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) public ByteBuf performHeapAllocation() { int releaseIndex = getNextReleaseIndex(); this.readAndRelease(releaseIndex); int size = sizes[getNextSizeIndex()]; ByteBuf newBuf = allocateHeap(allocator, size); if (enableReadWrite) { newBuf.writeByte(size); } buffers[releaseIndex] = newBuf; return newBuf; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private ByteBuf readAndRelease(int releaseIndex) { ByteBuf oldBuf = this.buffers[releaseIndex]; if (oldBuf != null) { if (enableReadWrite) { oldBuf.readByte(); } oldBuf.release(); } return oldBuf; } private static void releaseBufferArray(ByteBuf[] buffers) { if (buffers == null) { return; } for (int i = 0; i < buffers.length; i++) { if (buffers[i] != null && buffers[i].refCnt() > 0) { buffers[i].release(); buffers[i] = null; } } } private static final String netDataJavaRSSUrl = "http://localhost:19999/api/v1/data?chart=app.java_mem_usage&points=1&after=-1&format=json"; private static void printNetDataRSS() throws Exception { URL url = new URL(netDataJavaRSSUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { System.out.print(line); } } conn.disconnect(); } private static void printProcRSS() throws Exception { System.out.println("cRSS-pRSS:[" + ProcessMemoryUtil.getSelfRssKb() / 1024 + ", " + ProcessMemoryUtil.getSelfPeakRssKb() / 1024 + "]"); } private void printUsedMemory(BenchmarkParams benchmarkParams) throws Exception { long usedMem = 0; if (allocator instanceof PooledByteBufAllocator) { if (benchmarkParams.getBenchmark().contains("directAllocation")) { usedMem = ((PooledByteBufAllocator) allocator).metric().usedDirectMemory(); } else { usedMem = ((PooledByteBufAllocator) allocator).metric().usedHeapMemory(); } } if (allocator instanceof AdaptiveByteBufAllocator) { if (benchmarkParams.getBenchmark().contains("directAllocation")) { usedMem = ((AdaptiveByteBufAllocator) allocator).usedDirectMemory(); } else { usedMem = ((AdaptiveByteBufAllocator) allocator).usedHeapMemory(); } } if (allocator instanceof MiByteBufAllocator) { if (benchmarkParams.getBenchmark().contains("directAllocation")) { usedMem = ((MiByteBufAllocator) allocator).usedDirectMemory(); } else { usedMem = ((MiByteBufAllocator) allocator).usedHeapMemory(); } } System.out.println(";used-memory:[" + usedMem / 1024 / 1024 + "]"); } @TearDown public void tearDown(BenchmarkParams benchmarkParams) throws Exception { if (benchParamsSet.add(benchmarkParams)) { // printNetDataRSS(); printProcRSS(); printUsedMemory(benchmarkParams); } releaseBufferArray(buffers); } } @Setup public void setupAllocator() { allocator = allocatorType.create(); sizesArray = sizePattern.create(); benchParamsSet.clear(); } @Benchmark public void directAllocation(AllocationPatternState state) { state.performDirectAllocation(); } @Benchmark public void heapAllocation(AllocationPatternState state) { state.performHeapAllocation(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(ByteBufAllocatorAllocPatternBenchmark.class.getSimpleName()) .build(); new Runner(opt).run(); } private static final class ProcessMemoryUtil { private ProcessMemoryUtil() {} static long getSelfRssKb() { return readField("VmRSS:"); } static long getSelfPeakRssKb() { return readField("VmHWM:"); } private static long readField(String prefix) { Path statusPath = Paths.get("/proc/self/status"); try (Stream lines = Files.lines(statusPath)) { return lines .filter(line -> line.startsWith(prefix)) .findFirst() .map(ProcessMemoryUtil::parseKbLine) .orElseThrow(() -> new IllegalStateException(prefix + " not found in /proc/self/status")); } catch (IOException e) { throw new RuntimeException("Failed to read /proc/self/status", e); } } private static long parseKbLine(String line) { String[] parts = line.trim().split("\\s+"); return Long.parseLong(parts[1]); } } }