From e928cc5a0ce7c510c4e1e297a741f31a063f5ef4 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Thu, 18 Jun 2026 14:47:45 +0800 Subject: [PATCH 1/9] Add JMH benchmarks comparing read I/O strategies under memory pressure --- .../jmh/AbstractReadIOBenchmark.java | 289 ++++++++++++++ .../benchmark/jmh/RandomReadIOBenchmark.java | 346 ++++++++++++++++ .../jmh/SequentialReadIOBenchmark.java | 376 ++++++++++++++++++ 3 files changed, 1011 insertions(+) create mode 100644 lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java create mode 100644 lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java create mode 100644 lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java new file mode 100644 index 000000000000..491239ce403d --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -0,0 +1,289 @@ +/* + * 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.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +/** + * Abstract base for read I/O benchmarks. Provides shared FFI handles (pread, open, close, + * posix_madvise), thread-local buffers, file/mmap setup, and configuration parsing. + * + *

Subclasses define their own offset bounds and benchmark methods. + */ +@State(Scope.Benchmark) +@SuppressWarnings("restricted") +public abstract class AbstractReadIOBenchmark { + + protected static final int READ_SIZE = 16 * 1024; // 16 KiB + protected static final int READS_PER_OP = 16; + protected static final long ALIGNMENT = 4096; + + protected static final long FILE_SIZE = + Long.parseLong(envOrProp("BENCH_FILE_SIZE_MIB", "bench.fileSizeMiB", "1024")) * 1024L * 1024L; + + protected static final String BENCH_FILE = + envOrProp("BENCH_FILE", "bench.file", "/tmp/pread-bench.dat"); + + protected static final boolean DROP_CACHES = + Boolean.parseBoolean(envOrProp("BENCH_DROP_CACHES", "bench.dropCaches", "false")); + + protected static final int MADV_NORMAL = 0; + protected static final int MADV_RANDOM = 1; + protected static final int MADV_WILLNEED = 3; + + // FFI handles + protected static final MethodHandle PREAD; + protected static final MethodHandle OPEN; + protected static final MethodHandle CLOSE; + protected static final MethodHandle POSIX_MADVISE; + + static { + Linker linker = Linker.nativeLinker(); + SymbolLookup lookup = linker.defaultLookup(); + + PREAD = + linker.downcallHandle( + lookup.find("pread").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG)); + + OPEN = + linker.downcallHandle( + lookup.find("open").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + + CLOSE = + linker.downcallHandle( + lookup.find("close").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + + POSIX_MADVISE = + linker.downcallHandle( + lookup.find("posix_madvise").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT)); + } + + /** Per-thread pre-allocated buffers to avoid allocation noise in the measured path. */ + @State(Scope.Thread) + public static class ThreadBuffers { + public ByteBuffer directBuf; + public ByteBuffer heapBuf; + public Arena ffiArena; + public MemorySegment ffiBuf; + public MemorySegment ffiDirectIoBuf; + + @Setup(Level.Trial) + public void setup() { + directBuf = ByteBuffer.allocateDirect(READ_SIZE); + heapBuf = ByteBuffer.allocate(READ_SIZE); + ffiArena = Arena.ofConfined(); + ffiBuf = ffiArena.allocate(READ_SIZE); + ffiDirectIoBuf = ffiArena.allocate(READ_SIZE, 4096); + } + + @TearDown(Level.Trial) + public void tearDown() { + ffiArena.close(); + } + } + + protected Path tempFile; + protected FileChannel fileChannel; + protected MemorySegment mmapSegmentNormal; + protected MemorySegment mmapSegmentMadvRandom; + protected int nativeFd; + protected int directIoFd; + protected Arena arena; + + /** Subclasses return a name for logging (e.g. "RandomReadIOBenchmark"). */ + protected abstract String benchmarkName(); + + /** Subclasses may print extra config lines. */ + protected void printExtraConfig() {} + + @Setup(Level.Trial) + public void setup() throws Exception { + System.out.println("[bench] ===== " + benchmarkName() + " Configuration ====="); + System.out.println("[bench] file: " + BENCH_FILE); + System.out.println("[bench] fileSizeMiB: " + (FILE_SIZE / (1024 * 1024))); + System.out.println("[bench] dropCaches: " + DROP_CACHES); + System.out.println("[bench] readSize: " + READ_SIZE + " bytes"); + System.out.println("[bench] readsPerOp: " + READS_PER_OP); + printExtraConfig(); + System.out.println("[bench] ==============================================="); + + tempFile = Path.of(BENCH_FILE); + if (!Files.exists(tempFile)) { + throw new IOException( + "Benchmark file not found: " + + tempFile + + "\nCreate it with: dd if=/dev/urandom of=" + + BENCH_FILE + + " bs=1M count=" + + (FILE_SIZE / (1024 * 1024))); + } + long size = Files.size(tempFile); + if (size < FILE_SIZE) { + throw new IOException( + "Benchmark file too small: " + + size + + " bytes, expected at least " + + FILE_SIZE + + "\nRecreate with: dd if=/dev/urandom of=" + + BENCH_FILE + + " bs=1M count=" + + (FILE_SIZE / (1024 * 1024))); + } + + fileChannel = FileChannel.open(tempFile, StandardOpenOption.READ); + + arena = Arena.ofShared(); + + mmapSegmentNormal = fileChannel.map(MapMode.READ_ONLY, 0, FILE_SIZE, arena); + + mmapSegmentMadvRandom = fileChannel.map(MapMode.READ_ONLY, 0, FILE_SIZE, arena); + try { + int rc = (int) POSIX_MADVISE.invokeExact(mmapSegmentMadvRandom, FILE_SIZE, MADV_RANDOM); + if (rc != 0) { + System.err.println("WARNING: posix_madvise(MADV_RANDOM) returned " + rc); + } + } catch (Throwable t) { + throw new RuntimeException("posix_madvise(MADV_RANDOM) failed", t); + } + + MemorySegment pathStr = arena.allocateFrom(tempFile.toString()); + int O_RDONLY = 0; + try { + nativeFd = (int) OPEN.invokeExact(pathStr, O_RDONLY); + } catch (Throwable t) { + throw new RuntimeException("Failed to open file via FFI", t); + } + if (nativeFd < 0) { + throw new IOException("FFI open() returned " + nativeFd); + } + + // Open native fd with O_DIRECT for Direct I/O (Linux only, bypasses page cache) + int O_DIRECT = 0x4000; + try { + directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY | O_DIRECT); + } catch (Throwable t) { + throw new RuntimeException("Failed to open file with O_DIRECT via FFI", t); + } + if (directIoFd < 0) { + System.err.println( + "WARNING: O_DIRECT open failed (fd=" + + directIoFd + + "). " + + "Direct I/O benchmarks will fail. Use a filesystem that supports O_DIRECT."); + directIoFd = -1; + } + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + fileChannel.close(); + try { + int rc = (int) CLOSE.invokeExact(nativeFd); + if (directIoFd >= 0) { + rc = (int) CLOSE.invokeExact(directIoFd); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + arena.close(); + } + + /** + * Drops page caches before each iteration (warmup and measurement). This ensures each iteration + * starts with a cold page cache. JIT still warms up across iterations since the JVM persists + * across the fork. + */ + @Setup(Level.Iteration) + public void setupIteration() throws IOException { + if (DROP_CACHES) { + dropPageCaches(); + } + } + + /** + * Drops the kernel page cache to simulate cold-cache / memory-constrained scenarios. Requires + * running as root or with passwordless sudo. Uses: sync && echo 3 > /proc/sys/vm/drop_caches + */ + private static void dropPageCaches() throws IOException { + Process sync = new ProcessBuilder("sync").inheritIO().start(); + try { + if (sync.waitFor() != 0) { + throw new IOException("sync failed with exit code " + sync.exitValue()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during sync", e); + } + + Process drop = + new ProcessBuilder("sudo", "bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") + .inheritIO() + .start(); + try { + if (drop.waitFor() != 0) { + throw new IOException( + "Failed to drop page caches (exit code " + + drop.exitValue() + + "). Run as root or with: sudo sysctl vm.drop_caches=3"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during drop_caches", e); + } + System.out.println("[bench] Page caches dropped."); + } + + /** Reads a config value from env var first, then system property, then default. */ + protected static String envOrProp(String envKey, String propKey, String defaultValue) { + String env = System.getenv(envKey); + if (env != null && !env.isEmpty()) { + return env; + } + return System.getProperty(propKey, defaultValue); + } +} diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java new file mode 100644 index 000000000000..9133c2a4f01b --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -0,0 +1,346 @@ +/* + * 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.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmark comparing random read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + + * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory + * pressure. Each operation picks a random offset reading 16KB data for 16 times (256KB total per + * op). + * + *

Run with: + * + *

+ *   dd if=/dev/urandom of=/path/to/bench-16G.dat bs=1M count=16384
+ *   BENCH_FILE=/path/to/bench-16G.dat
+ *   BENCH_FILE_SIZE_MIB=16384
+ *   BENCH_DROP_CACHES=false
+ *   java -jar lucene/benchmark-jmh/build/benchmarks/lucene-benchmark-jmh.jar RandomReadIOBenchmark
+ * 
+ */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 3) +@Measurement(iterations = 5, time = 5) +@Fork( + value = 3, + jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) +public class RandomReadIOBenchmark extends AbstractReadIOBenchmark { + + private static final long MAX_OFFSET = FILE_SIZE - READ_SIZE; + private static final long MAX_ALIGNED_OFFSET = (MAX_OFFSET / ALIGNMENT) * ALIGNMENT; + + @Override + protected String benchmarkName() { + return "RandomReadIOBenchmark"; + } + + // ======== mmap — no madvise ======== + + @Benchmark + @Threads(1) + public void mmap_T01(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmap_T04(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmap_T08(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmap_T16(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + // ======== mmap + MADV_RANDOM ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandom_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandom_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandom_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandom_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + // ======== mmap + MADV_RANDOM + MADV_WILLNEED ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandomWillneed_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandomWillneed_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandomWillneed_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandomWillneed_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + // ======== FileChannel + DirectByteBuffer ======== + + @Benchmark + @Threads(1) + public void fileChannelDirect_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void fileChannelDirect_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void fileChannelDirect_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void fileChannelDirect_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + // ======== FileChannel + HeapByteBuffer ======== + + @Benchmark + @Threads(1) + public void fileChannelHeap_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void fileChannelHeap_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void fileChannelHeap_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void fileChannelHeap_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + // ======== FFI pread ======== + + @Benchmark + @Threads(1) + public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + // ======== FFI pread + O_DIRECT ======== + + @Benchmark + @Threads(1) + public void ffiPreadDirectIO_T01(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void ffiPreadDirectIO_T04(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void ffiPreadDirectIO_T08(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void ffiPreadDirectIO_T16(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // ---- Implementation: each read picks an independent random offset ---- + + private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + bh.consume(dst[0]); + } + } + + private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + bh.consume(dst[0]); + } + } + + private void doMmapMadvRandomWillneedReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + try { + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + MemorySegment slice = mmapSegmentMadvRandom.asSlice(offset, READ_SIZE); + int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) READ_SIZE, MADV_WILLNEED); + MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + bh.consume(dst[0]); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ByteBuffer buf = tb.directBuf; + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + buf.clear(); + int n = fileChannel.read(buf, offset); + bh.consume(n); + } + } + + private void doFileChannelHeapReads(ThreadBuffers tb, Blackhole bh) throws IOException { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ByteBuffer buf = tb.heapBuf; + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + buf.clear(); + int n = fileChannel.read(buf, offset); + bh.consume(n); + } + } + + private void doFfiReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiBuf; + try { + for (int i = 0; i < READS_PER_OP; i++) { + long offset = rng.nextLong(MAX_OFFSET); + long n = (long) PREAD.invokeExact(nativeFd, buf, (long) READ_SIZE, offset); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private void doFfiDirectIoReads(ThreadBuffers tb, Blackhole bh) { + if (directIoFd < 0) { + // O_DIRECT not available on this filesystem — skip silently + bh.consume(0); + return; + } + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiDirectIoBuf; + try { + for (int i = 0; i < READS_PER_OP; i++) { + // O_DIRECT requires aligned offset; generate random aligned position + long offset = (rng.nextLong(MAX_ALIGNED_OFFSET / ALIGNMENT)) * ALIGNMENT; + long n = (long) PREAD.invokeExact(directIoFd, buf, (long) READ_SIZE, offset); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java new file mode 100644 index 000000000000..9b88876f9bbd --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java @@ -0,0 +1,376 @@ +/* + * 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.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmark comparing sequential read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + + * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory + * pressure. Each operation picks a random starting offset, then reads 16 consecutive 16KB blocks + * sequentially forward (256KB total per op). + * + *

Run with: + * + *

+ *   dd if=/dev/urandom of=/path/to/bench-16G.dat bs=1M count=16384
+ *   BENCH_FILE=/path/to/bench-16G.dat
+ *   BENCH_FILE_SIZE_MIB=16384
+ *   BENCH_DROP_CACHES=false
+ *   java -jar lucene/benchmark-jmh/build/benchmarks/lucene-benchmark-jmh.jar SequentialReadIOBenchmark
+ * 
+ */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 3) +@Measurement(iterations = 5, time = 5) +@Fork( + value = 3, + jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) +public class SequentialReadIOBenchmark extends AbstractReadIOBenchmark { + + /** Must leave room for READS_PER_OP sequential reads from the starting offset. */ + private static final long MAX_START_OFFSET = FILE_SIZE - ((long) READ_SIZE * READS_PER_OP); + + private static final long MAX_ALIGNED_START = (MAX_START_OFFSET / ALIGNMENT) * ALIGNMENT; + private static final long TOTAL_READ_PER_OP = (long) READ_SIZE * READS_PER_OP; + + @Override + protected String benchmarkName() { + return "SequentialReadIOBenchmark"; + } + + @Override + protected void printExtraConfig() { + System.out.println("[bench] bytesPerOp: " + TOTAL_READ_PER_OP + " bytes (sequential)"); + } + + // ======== mmap — no madvise ======== + + @Benchmark + @Threads(1) + public void mmap_T01(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmap_T04(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmap_T08(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmap_T16(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + // ======== mmap + MADV_RANDOM ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandom_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandom_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandom_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandom_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + // ======== mmap + MADV_RANDOM + MADV_WILLNEED ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandomWillneed_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandomWillneed_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandomWillneed_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandomWillneed_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomWillneedReads(tb, bh); + } + + // ======== FileChannel + DirectByteBuffer ======== + + @Benchmark + @Threads(1) + public void fileChannelDirect_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void fileChannelDirect_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void fileChannelDirect_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void fileChannelDirect_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + // ======== FileChannel + HeapByteBuffer ======== + + @Benchmark + @Threads(1) + public void fileChannelHeap_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void fileChannelHeap_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void fileChannelHeap_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void fileChannelHeap_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelHeapReads(tb, bh); + } + + // ======== FFI pread ======== + + @Benchmark + @Threads(1) + public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + // ======== FFI pread + O_DIRECT ======== + + @Benchmark + @Threads(1) + public void ffiPreadDirectIO_T01(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void ffiPreadDirectIO_T04(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void ffiPreadDirectIO_T08(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void ffiPreadDirectIO_T16(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // ---- Implementation: random start, then sequential forward reads ---- + + private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { + long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < READS_PER_OP; i++) { + MemorySegment.copy( + mmapSegmentNormal, + ValueLayout.JAVA_BYTE, + startOffset + (long) i * READ_SIZE, + dst, + 0, + READ_SIZE); + bh.consume(dst[0]); + } + } + + private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { + long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < READS_PER_OP; i++) { + MemorySegment.copy( + mmapSegmentMadvRandom, + ValueLayout.JAVA_BYTE, + startOffset + (long) i * READ_SIZE, + dst, + 0, + READ_SIZE); + bh.consume(dst[0]); + } + } + + private void doMmapMadvRandomWillneedReads(ThreadBuffers tb, Blackhole bh) { + long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); + byte[] dst = tb.heapBuf.array(); + try { + // Prefetch the entire sequential range up front + MemorySegment slice = mmapSegmentMadvRandom.asSlice(startOffset, TOTAL_READ_PER_OP); + int rc = (int) POSIX_MADVISE.invokeExact(slice, TOTAL_READ_PER_OP, MADV_WILLNEED); + } catch (Throwable t) { + throw new RuntimeException(t); + } + for (int i = 0; i < READS_PER_OP; i++) { + MemorySegment.copy( + mmapSegmentMadvRandom, + ValueLayout.JAVA_BYTE, + startOffset + (long) i * READ_SIZE, + dst, + 0, + READ_SIZE); + bh.consume(dst[0]); + } + } + + private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ByteBuffer buf = tb.directBuf; + long startOffset = rng.nextLong(MAX_START_OFFSET); + for (int i = 0; i < READS_PER_OP; i++) { + buf.clear(); + int n = fileChannel.read(buf, startOffset + (long) i * READ_SIZE); + bh.consume(n); + } + } + + private void doFileChannelHeapReads(ThreadBuffers tb, Blackhole bh) throws IOException { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ByteBuffer buf = tb.heapBuf; + long startOffset = rng.nextLong(MAX_START_OFFSET); + for (int i = 0; i < READS_PER_OP; i++) { + buf.clear(); + int n = fileChannel.read(buf, startOffset + (long) i * READ_SIZE); + bh.consume(n); + } + } + + private void doFfiReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiBuf; + long startOffset = rng.nextLong(MAX_START_OFFSET); + try { + for (int i = 0; i < READS_PER_OP; i++) { + long n = + (long) + PREAD.invokeExact( + nativeFd, buf, (long) READ_SIZE, startOffset + (long) i * READ_SIZE); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private void doFfiDirectIoReads(ThreadBuffers tb, Blackhole bh) { + if (directIoFd < 0) { + // O_DIRECT not available on this filesystem — skip silently + bh.consume(0); + return; + } + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiDirectIoBuf; + // Align start offset for O_DIRECT + long startOffset = (rng.nextLong(MAX_ALIGNED_START / ALIGNMENT)) * ALIGNMENT; + try { + for (int i = 0; i < READS_PER_OP; i++) { + long n = + (long) + PREAD.invokeExact( + directIoFd, buf, (long) READ_SIZE, startOffset + (long) i * READ_SIZE); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} From d8bc829e4fbffac6bc840c2c896f2bcbb7762cb1 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Fri, 19 Jun 2026 08:53:19 +0800 Subject: [PATCH 2/9] Change read size from 16k to 4k --- .../apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java | 2 +- .../apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java | 2 +- .../lucene/benchmark/jmh/SequentialReadIOBenchmark.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index 491239ce403d..5edab6e79a67 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -46,7 +46,7 @@ @SuppressWarnings("restricted") public abstract class AbstractReadIOBenchmark { - protected static final int READ_SIZE = 16 * 1024; // 16 KiB + protected static final int READ_SIZE = 4 * 1024; // 4 KiB protected static final int READS_PER_OP = 16; protected static final long ALIGNMENT = 4096; diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java index 9133c2a4f01b..ec1aba01d49f 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -37,7 +37,7 @@ /** * Benchmark comparing random read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory - * pressure. Each operation picks a random offset reading 16KB data for 16 times (256KB total per + * pressure. Each operation picks a random offset reading 4KB data for 16 times (64KB total per * op). * *

Run with: diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java index 9b88876f9bbd..440a1ca40129 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java @@ -37,8 +37,8 @@ /** * Benchmark comparing sequential read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory - * pressure. Each operation picks a random starting offset, then reads 16 consecutive 16KB blocks - * sequentially forward (256KB total per op). + * pressure. Each operation picks a random starting offset, then reads 16 consecutive 4KB blocks + * sequentially forward (64KB total per op). * *

Run with: * From 5676506e5da6cb7800eddb34d9d41c591a0f311d Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Sat, 20 Jun 2026 23:55:51 +0800 Subject: [PATCH 3/9] Update CHANGES.txt --- lucene/CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 42a6e8073576..16437477c1a7 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -299,7 +299,7 @@ Bug Fixes Other --------------------- -(No changes) +* GITHUB#16279: Add JMH benchmarks comparing read I/O strategies under memory pressure ======================= Lucene 10.5.0 ======================= From 153a73326a65b5af3bee8adf9dcc2f095449513c Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Sun, 21 Jun 2026 00:30:33 +0800 Subject: [PATCH 4/9] Fix tidy --- .../org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java index ec1aba01d49f..d07f6bf8150a 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -37,8 +37,7 @@ /** * Benchmark comparing random read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory - * pressure. Each operation picks a random offset reading 4KB data for 16 times (64KB total per - * op). + * pressure. Each operation picks a random offset reading 4KB data for 16 times (64KB total per op). * *

Run with: * From 7e773f35d64e75ec404f640849d56cb30f1e0a61 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Sun, 21 Jun 2026 00:35:41 +0800 Subject: [PATCH 5/9] Address github-advanced-security CR: Command with a relative path 'xxx' is executed. --- .../apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index 5edab6e79a67..834479857cb7 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -250,7 +250,7 @@ public void setupIteration() throws IOException { * running as root or with passwordless sudo. Uses: sync && echo 3 > /proc/sys/vm/drop_caches */ private static void dropPageCaches() throws IOException { - Process sync = new ProcessBuilder("sync").inheritIO().start(); + Process sync = new ProcessBuilder("/usr/bin/sync").inheritIO().start(); try { if (sync.waitFor() != 0) { throw new IOException("sync failed with exit code " + sync.exitValue()); @@ -261,7 +261,7 @@ private static void dropPageCaches() throws IOException { } Process drop = - new ProcessBuilder("sudo", "bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") + new ProcessBuilder("/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") .inheritIO() .start(); try { From ec305004367547cbf45eebff57224c55db79e179 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Sun, 21 Jun 2026 15:13:59 +0800 Subject: [PATCH 6/9] Address github-advanced-security CR: Command with a relative path 'xxx' is executed. --- lucene/CHANGES.txt | 2 +- .../apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 16437477c1a7..1fd616315c8e 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -299,7 +299,7 @@ Bug Fixes Other --------------------- -* GITHUB#16279: Add JMH benchmarks comparing read I/O strategies under memory pressure +* GITHUB#16279: Add JMH benchmarks comparing read I/O strategies under memory pressure (neoremind) ======================= Lucene 10.5.0 ======================= diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index 834479857cb7..5586ff1b0e71 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -261,7 +261,8 @@ private static void dropPageCaches() throws IOException { } Process drop = - new ProcessBuilder("/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") + new ProcessBuilder( + "/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") .inheritIO() .start(); try { From 862598ea81c22289bf2cc3e2c77903d1f9c6f6f8 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Tue, 23 Jun 2026 14:45:23 +0800 Subject: [PATCH 7/9] Refactor benchmarks. Use batched prefetch WILLNEED hint. Refactor benchmarks --- .../jmh/AbstractReadIOBenchmark.java | 147 ++++--- .../benchmark/jmh/RandomReadIOBenchmark.java | 205 +++++----- .../jmh/SequentialReadIOBenchmark.java | 366 ++++-------------- 3 files changed, 272 insertions(+), 446 deletions(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index 5586ff1b0e71..f1a5e264e3a5 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -38,20 +38,19 @@ /** * Abstract base for read I/O benchmarks. Provides shared FFI handles (pread, open, close, - * posix_madvise), thread-local buffers, file/mmap setup, and configuration parsing. - * - *

Subclasses define their own offset bounds and benchmark methods. + * posix_madvise, fcntl), thread-local buffers, file/mmap setup, and configuration parsing. */ @State(Scope.Benchmark) @SuppressWarnings("restricted") public abstract class AbstractReadIOBenchmark { - protected static final int READ_SIZE = 4 * 1024; // 4 KiB - protected static final int READS_PER_OP = 16; protected static final long ALIGNMENT = 4096; + /** Max read size for buffer pre-allocation. Actual read size is a @Param on subclasses. */ + protected static final int MAX_READ_SIZE = 1024 * 1024; // 1MB max + protected static final long FILE_SIZE = - Long.parseLong(envOrProp("BENCH_FILE_SIZE_MIB", "bench.fileSizeMiB", "1024")) * 1024L * 1024L; + Long.parseLong(envOrProp("BENCH_FILE_SIZE_MB", "bench.fileSizeMB", "1024")) * 1024L * 1024L; protected static final String BENCH_FILE = envOrProp("BENCH_FILE", "bench.file", "/tmp/pread-bench.dat"); @@ -63,11 +62,16 @@ public abstract class AbstractReadIOBenchmark { protected static final int MADV_RANDOM = 1; protected static final int MADV_WILLNEED = 3; + private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac"); + private static final boolean IS_LINUX = + System.getProperty("os.name").toLowerCase().contains("linux"); + // FFI handles protected static final MethodHandle PREAD; protected static final MethodHandle OPEN; protected static final MethodHandle CLOSE; protected static final MethodHandle POSIX_MADVISE; + protected static final MethodHandle FCNTL; static { Linker linker = Linker.nativeLinker(); @@ -101,9 +105,18 @@ public abstract class AbstractReadIOBenchmark { ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT)); + + FCNTL = + linker.downcallHandle( + lookup.find("fcntl").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT)); } - /** Per-thread pre-allocated buffers to avoid allocation noise in the measured path. */ + /** Per-thread pre-allocated buffers sized to MAX_READ_SIZE. */ @State(Scope.Thread) public static class ThreadBuffers { public ByteBuffer directBuf; @@ -114,11 +127,11 @@ public static class ThreadBuffers { @Setup(Level.Trial) public void setup() { - directBuf = ByteBuffer.allocateDirect(READ_SIZE); - heapBuf = ByteBuffer.allocate(READ_SIZE); + directBuf = ByteBuffer.allocateDirect(MAX_READ_SIZE); + heapBuf = ByteBuffer.allocate(MAX_READ_SIZE); ffiArena = Arena.ofConfined(); - ffiBuf = ffiArena.allocate(READ_SIZE); - ffiDirectIoBuf = ffiArena.allocate(READ_SIZE, 4096); + ffiBuf = ffiArena.allocate(MAX_READ_SIZE); + ffiDirectIoBuf = ffiArena.allocate(MAX_READ_SIZE, 4096); } @TearDown(Level.Trial) @@ -135,21 +148,23 @@ public void tearDown() { protected int directIoFd; protected Arena arena; - /** Subclasses return a name for logging (e.g. "RandomReadIOBenchmark"). */ - protected abstract String benchmarkName(); + protected String benchmarkName() { + return getClass().getSimpleName(); + } - /** Subclasses may print extra config lines. */ - protected void printExtraConfig() {} + protected void validateReadSize(int readSize) { + if (readSize > MAX_READ_SIZE) { + throw new IllegalArgumentException( + "readSize (" + readSize + ") exceeds MAX_READ_SIZE (" + MAX_READ_SIZE + ")."); + } + } @Setup(Level.Trial) public void setup() throws Exception { System.out.println("[bench] ===== " + benchmarkName() + " Configuration ====="); - System.out.println("[bench] file: " + BENCH_FILE); - System.out.println("[bench] fileSizeMiB: " + (FILE_SIZE / (1024 * 1024))); + System.out.println("[bench] file: " + BENCH_FILE); + System.out.println("[bench] fileSizeMB: " + (FILE_SIZE / (1024 * 1024))); System.out.println("[bench] dropCaches: " + DROP_CACHES); - System.out.println("[bench] readSize: " + READ_SIZE + " bytes"); - System.out.println("[bench] readsPerOp: " + READS_PER_OP); - printExtraConfig(); System.out.println("[bench] ==============================================="); tempFile = Path.of(BENCH_FILE); @@ -202,19 +217,30 @@ public void setup() throws Exception { throw new IOException("FFI open() returned " + nativeFd); } - // Open native fd with O_DIRECT for Direct I/O (Linux only, bypasses page cache) - int O_DIRECT = 0x4000; + // Direct I/O: Linux uses O_DIRECT, macOS uses fcntl(F_NOCACHE) try { - directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY | O_DIRECT); + if (IS_LINUX) { + int O_DIRECT = 0x4000; + directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY | O_DIRECT); + } else if (IS_MAC) { + directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY); + if (directIoFd >= 0) { + int F_NOCACHE = 48; + int rc = (int) FCNTL.invokeExact(directIoFd, F_NOCACHE, 1); + if (rc != 0) { + System.err.println("WARNING: fcntl(F_NOCACHE) failed"); + CLOSE.invokeExact(directIoFd); + directIoFd = -1; + } + } + } else { + directIoFd = -1; + } } catch (Throwable t) { - throw new RuntimeException("Failed to open file with O_DIRECT via FFI", t); + throw new RuntimeException("Failed to open file for direct I/O", t); } if (directIoFd < 0) { - System.err.println( - "WARNING: O_DIRECT open failed (fd=" - + directIoFd - + "). " - + "Direct I/O benchmarks will fail. Use a filesystem that supports O_DIRECT."); + System.err.println("WARNING: Direct I/O unavailable. Those benchmarks will skip."); directIoFd = -1; } } @@ -233,11 +259,6 @@ public void tearDown() throws Exception { arena.close(); } - /** - * Drops page caches before each iteration (warmup and measurement). This ensures each iteration - * starts with a cold page cache. JIT still warms up across iterations since the JVM persists - * across the fork. - */ @Setup(Level.Iteration) public void setupIteration() throws IOException { if (DROP_CACHES) { @@ -245,36 +266,40 @@ public void setupIteration() throws IOException { } } - /** - * Drops the kernel page cache to simulate cold-cache / memory-constrained scenarios. Requires - * running as root or with passwordless sudo. Uses: sync && echo 3 > /proc/sys/vm/drop_caches - */ private static void dropPageCaches() throws IOException { - Process sync = new ProcessBuilder("/usr/bin/sync").inheritIO().start(); - try { - if (sync.waitFor() != 0) { - throw new IOException("sync failed with exit code " + sync.exitValue()); + if (IS_MAC) { + Process purge = new ProcessBuilder("/usr/bin/sudo", "purge").inheritIO().start(); + try { + if (purge.waitFor() != 0) { + throw new IOException("purge failed with exit code " + purge.exitValue()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during purge", e); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted during sync", e); - } - - Process drop = - new ProcessBuilder( - "/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") - .inheritIO() - .start(); - try { - if (drop.waitFor() != 0) { - throw new IOException( - "Failed to drop page caches (exit code " - + drop.exitValue() - + "). Run as root or with: sudo sysctl vm.drop_caches=3"); + } else { + Process sync = new ProcessBuilder("/usr/bin/sync").inheritIO().start(); + try { + if (sync.waitFor() != 0) { + throw new IOException("sync failed with exit code " + sync.exitValue()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during sync", e); + } + Process drop = + new ProcessBuilder( + "/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") + .inheritIO() + .start(); + try { + if (drop.waitFor() != 0) { + throw new IOException("Failed to drop page caches (exit code " + drop.exitValue() + ")."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during drop_caches", e); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted during drop_caches", e); } System.out.println("[bench] Page caches dropped."); } diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java index d07f6bf8150a..d14b07e940f3 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -25,49 +25,46 @@ 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.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.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -/** - * Benchmark comparing random read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + - * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory - * pressure. Each operation picks a random offset reading 4KB data for 16 times (64KB total per op). - * - *

Run with: - * - *

- *   dd if=/dev/urandom of=/path/to/bench-16G.dat bs=1M count=16384
- *   BENCH_FILE=/path/to/bench-16G.dat
- *   BENCH_FILE_SIZE_MIB=16384
- *   BENCH_DROP_CACHES=false
- *   java -jar lucene/benchmark-jmh/build/benchmarks/lucene-benchmark-jmh.jar RandomReadIOBenchmark
- * 
- */ +/** Benchmark comparing random read I/O strategies under varying concurrency and memory pressure. */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) -@Warmup(iterations = 3, time = 3) -@Measurement(iterations = 5, time = 5) +@Warmup(iterations = 2, time = 3) +@Measurement(iterations = 3, time = 5) @Fork( - value = 3, + value = 2, jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) public class RandomReadIOBenchmark extends AbstractReadIOBenchmark { - private static final long MAX_OFFSET = FILE_SIZE - READ_SIZE; - private static final long MAX_ALIGNED_OFFSET = (MAX_OFFSET / ALIGNMENT) * ALIGNMENT; + @Param({"4096"}) + public int readSize; + + @Param({"16"}) + public int readsPerOp; - @Override - protected String benchmarkName() { - return "RandomReadIOBenchmark"; + private long maxOffset; + private long maxAlignedOffset; + + @Setup(Level.Trial) + public void validateParams() { + validateReadSize(readSize); + maxOffset = FILE_SIZE - readSize; + maxAlignedOffset = (maxOffset / ALIGNMENT) * ALIGNMENT; } - // ======== mmap — no madvise ======== + // ======== mmap NORMAL ======== @Benchmark @Threads(1) @@ -119,144 +116,144 @@ public void mmapMadvRandom_T16(ThreadBuffers tb, Blackhole bh) { doMmapMadvRandomReads(tb, bh); } - // ======== mmap + MADV_RANDOM + MADV_WILLNEED ======== + // ======== mmap NORMAL + batched MADV_WILLNEED ======== @Benchmark @Threads(1) - public void mmapMadvRandomWillneed_T01(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); + public void mmapBatchedPrefetch_T01(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); } @Benchmark @Threads(4) - public void mmapMadvRandomWillneed_T04(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); + public void mmapBatchedPrefetch_T04(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); } @Benchmark @Threads(8) - public void mmapMadvRandomWillneed_T08(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); + public void mmapBatchedPrefetch_T08(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); } @Benchmark @Threads(16) - public void mmapMadvRandomWillneed_T16(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); + public void mmapBatchedPrefetch_T16(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); } - // ======== FileChannel + DirectByteBuffer ======== + // ======== mmap RANDOM + batched MADV_WILLNEED ======== @Benchmark @Threads(1) - public void fileChannelDirect_T01(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); + public void mmapMadvRandomBatchedPrefetch_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); } @Benchmark @Threads(4) - public void fileChannelDirect_T04(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); + public void mmapMadvRandomBatchedPrefetch_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); } @Benchmark @Threads(8) - public void fileChannelDirect_T08(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); + public void mmapMadvRandomBatchedPrefetch_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); } @Benchmark @Threads(16) - public void fileChannelDirect_T16(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); + public void mmapMadvRandomBatchedPrefetch_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); } - // ======== FileChannel + HeapByteBuffer ======== + // ======== FFI pread ======== @Benchmark @Threads(1) - public void fileChannelHeap_T01(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); + public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); } @Benchmark @Threads(4) - public void fileChannelHeap_T04(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); + public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); } @Benchmark @Threads(8) - public void fileChannelHeap_T08(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); + public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); } @Benchmark @Threads(16) - public void fileChannelHeap_T16(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); + public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); } - // ======== FFI pread ======== + // ======== FileChannel + DirectByteBuffer ======== @Benchmark @Threads(1) - public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); + public void fileChannelDirectBuffer_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); } @Benchmark @Threads(4) - public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); + public void fileChannelDirectBuffer_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); } @Benchmark @Threads(8) - public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); + public void fileChannelDirectBuffer_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); } @Benchmark @Threads(16) - public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); + public void fileChannelDirectBuffer_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); } // ======== FFI pread + O_DIRECT ======== - @Benchmark + // @Benchmark @Threads(1) public void ffiPreadDirectIO_T01(ThreadBuffers tb, Blackhole bh) { doFfiDirectIoReads(tb, bh); } - @Benchmark + // @Benchmark @Threads(4) public void ffiPreadDirectIO_T04(ThreadBuffers tb, Blackhole bh) { doFfiDirectIoReads(tb, bh); } - @Benchmark + // @Benchmark @Threads(8) public void ffiPreadDirectIO_T08(ThreadBuffers tb, Blackhole bh) { doFfiDirectIoReads(tb, bh); } - @Benchmark + // @Benchmark @Threads(16) public void ffiPreadDirectIO_T16(ThreadBuffers tb, Blackhole bh) { doFfiDirectIoReads(tb, bh); } - // ---- Implementation: each read picks an independent random offset ---- + // ======== Implementation ======== private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); byte[] dst = tb.heapBuf.array(); - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); bh.consume(dst[0]); } } @@ -264,22 +261,27 @@ private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); byte[] dst = tb.heapBuf.array(); - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); bh.consume(dst[0]); } } - private void doMmapMadvRandomWillneedReads(ThreadBuffers tb, Blackhole bh) { + private void doMmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); byte[] dst = tb.heapBuf.array(); + long[] offsets = new long[readsPerOp]; try { - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - MemorySegment slice = mmapSegmentMadvRandom.asSlice(offset, READ_SIZE); - int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) READ_SIZE, MADV_WILLNEED); - MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + offsets[i] = rng.nextLong(maxOffset); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment slice = mmapSegmentNormal.asSlice(offsets[i], readSize); + int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offsets[i], dst, 0, readSize); bh.consume(dst[0]); } } catch (Throwable t) { @@ -287,23 +289,34 @@ private void doMmapMadvRandomWillneedReads(ThreadBuffers tb, Blackhole bh) { } } - private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { + private void doMmapMadvRandomBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); - ByteBuffer buf = tb.directBuf; - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - buf.clear(); - int n = fileChannel.read(buf, offset); - bh.consume(n); + byte[] dst = tb.heapBuf.array(); + long[] offsets = new long[readsPerOp]; + try { + for (int i = 0; i < readsPerOp; i++) { + offsets[i] = rng.nextLong(maxOffset); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment slice = mmapSegmentMadvRandom.asSlice(offsets[i], readSize); + int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy( + mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offsets[i], dst, 0, readSize); + bh.consume(dst[0]); + } + } catch (Throwable t) { + throw new RuntimeException(t); } } - private void doFileChannelHeapReads(ThreadBuffers tb, Blackhole bh) throws IOException { + private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { ThreadLocalRandom rng = ThreadLocalRandom.current(); - ByteBuffer buf = tb.heapBuf; - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - buf.clear(); + ByteBuffer buf = tb.directBuf; + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + buf.clear().limit(readSize); int n = fileChannel.read(buf, offset); bh.consume(n); } @@ -313,9 +326,9 @@ private void doFfiReads(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); MemorySegment buf = tb.ffiBuf; try { - for (int i = 0; i < READS_PER_OP; i++) { - long offset = rng.nextLong(MAX_OFFSET); - long n = (long) PREAD.invokeExact(nativeFd, buf, (long) READ_SIZE, offset); + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + long n = (long) PREAD.invokeExact(nativeFd, buf, (long) readSize, offset); bh.consume(n); } } catch (Throwable t) { @@ -325,17 +338,15 @@ private void doFfiReads(ThreadBuffers tb, Blackhole bh) { private void doFfiDirectIoReads(ThreadBuffers tb, Blackhole bh) { if (directIoFd < 0) { - // O_DIRECT not available on this filesystem — skip silently bh.consume(0); return; } ThreadLocalRandom rng = ThreadLocalRandom.current(); MemorySegment buf = tb.ffiDirectIoBuf; try { - for (int i = 0; i < READS_PER_OP; i++) { - // O_DIRECT requires aligned offset; generate random aligned position - long offset = (rng.nextLong(MAX_ALIGNED_OFFSET / ALIGNMENT)) * ALIGNMENT; - long n = (long) PREAD.invokeExact(directIoFd, buf, (long) READ_SIZE, offset); + for (int i = 0; i < readsPerOp; i++) { + long offset = (rng.nextLong(maxAlignedOffset / ALIGNMENT)) * ALIGNMENT; + long n = (long) PREAD.invokeExact(directIoFd, buf, (long) readSize, offset); bh.consume(n); } } catch (Throwable t) { diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java index 440a1ca40129..fd1cfb8a44a4 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java @@ -20,7 +20,6 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -28,6 +27,7 @@ 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.State; import org.openjdk.jmh.annotations.Threads; @@ -35,342 +35,132 @@ import org.openjdk.jmh.infra.Blackhole; /** - * Benchmark comparing sequential read I/O strategies: mmap (normal, MADV_RANDOM, MADV_RANDOM + - * MADV_WILLNEED), FileChannel, FFI pread, and O_DIRECT — under varying concurrency and memory - * pressure. Each operation picks a random starting offset, then reads 16 consecutive 4KB blocks - * sequentially forward (64KB total per op). - * - *

Run with: - * - *

- *   dd if=/dev/urandom of=/path/to/bench-16G.dat bs=1M count=16384
- *   BENCH_FILE=/path/to/bench-16G.dat
- *   BENCH_FILE_SIZE_MIB=16384
- *   BENCH_DROP_CACHES=false
- *   java -jar lucene/benchmark-jmh/build/benchmarks/lucene-benchmark-jmh.jar SequentialReadIOBenchmark
- * 
+ * Benchmark comparing sequential (whole-file scan) I/O strategies. Single-threaded. Each op reads + * {@code readsPerOp} pages sequentially forward from the current file position, wrapping at EOF. */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) -@Warmup(iterations = 3, time = 3) -@Measurement(iterations = 5, time = 5) +@Warmup(iterations = 1, time = 2) +@Measurement(iterations = 1, time = 3) @Fork( - value = 3, + value = 1, jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) +@Threads(1) public class SequentialReadIOBenchmark extends AbstractReadIOBenchmark { - /** Must leave room for READS_PER_OP sequential reads from the starting offset. */ - private static final long MAX_START_OFFSET = FILE_SIZE - ((long) READ_SIZE * READS_PER_OP); - - private static final long MAX_ALIGNED_START = (MAX_START_OFFSET / ALIGNMENT) * ALIGNMENT; - private static final long TOTAL_READ_PER_OP = (long) READ_SIZE * READS_PER_OP; - - @Override - protected String benchmarkName() { - return "SequentialReadIOBenchmark"; - } - - @Override - protected void printExtraConfig() { - System.out.println("[bench] bytesPerOp: " + TOTAL_READ_PER_OP + " bytes (sequential)"); - } - - // ======== mmap — no madvise ======== - - @Benchmark - @Threads(1) - public void mmap_T01(ThreadBuffers tb, Blackhole bh) { - doMmapNormalReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void mmap_T04(ThreadBuffers tb, Blackhole bh) { - doMmapNormalReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void mmap_T08(ThreadBuffers tb, Blackhole bh) { - doMmapNormalReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void mmap_T16(ThreadBuffers tb, Blackhole bh) { - doMmapNormalReads(tb, bh); - } - - // ======== mmap + MADV_RANDOM ======== - - @Benchmark - @Threads(1) - public void mmapMadvRandom_T01(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void mmapMadvRandom_T04(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void mmapMadvRandom_T08(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void mmapMadvRandom_T16(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomReads(tb, bh); - } - - // ======== mmap + MADV_RANDOM + MADV_WILLNEED ======== - - @Benchmark - @Threads(1) - public void mmapMadvRandomWillneed_T01(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void mmapMadvRandomWillneed_T04(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void mmapMadvRandomWillneed_T08(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void mmapMadvRandomWillneed_T16(ThreadBuffers tb, Blackhole bh) { - doMmapMadvRandomWillneedReads(tb, bh); - } - - // ======== FileChannel + DirectByteBuffer ======== - - @Benchmark - @Threads(1) - public void fileChannelDirect_T01(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void fileChannelDirect_T04(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void fileChannelDirect_T08(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void fileChannelDirect_T16(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelDirectReads(tb, bh); - } - - // ======== FileChannel + HeapByteBuffer ======== - - @Benchmark - @Threads(1) - public void fileChannelHeap_T01(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void fileChannelHeap_T04(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void fileChannelHeap_T08(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void fileChannelHeap_T16(ThreadBuffers tb, Blackhole bh) throws IOException { - doFileChannelHeapReads(tb, bh); - } - - // ======== FFI pread ======== - - @Benchmark - @Threads(1) - public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); - } - - @Benchmark - @Threads(4) - public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); - } - - @Benchmark - @Threads(8) - public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); - } - - @Benchmark - @Threads(16) - public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { - doFfiReads(tb, bh); - } + @Param({"4096"}) + public int readSize; - // ======== FFI pread + O_DIRECT ======== + @Param({"64"}) + public int readsPerOp; - @Benchmark - @Threads(1) - public void ffiPreadDirectIO_T01(ThreadBuffers tb, Blackhole bh) { - doFfiDirectIoReads(tb, bh); - } + /** Sliding prefetch window size. */ + private static final long PREFETCH_WINDOW = 2 * 1024 * 1024; - @Benchmark - @Threads(4) - public void ffiPreadDirectIO_T04(ThreadBuffers tb, Blackhole bh) { - doFfiDirectIoReads(tb, bh); - } + /** Current sequential scan position — advances across JMH invocations, wraps at EOF. */ + private long seqPosition = 0; - @Benchmark - @Threads(8) - public void ffiPreadDirectIO_T08(ThreadBuffers tb, Blackhole bh) { - doFfiDirectIoReads(tb, bh); - } + // ======== mmap NORMAL ======== @Benchmark - @Threads(16) - public void ffiPreadDirectIO_T16(ThreadBuffers tb, Blackhole bh) { - doFfiDirectIoReads(tb, bh); - } - - // ---- Implementation: random start, then sequential forward reads ---- - - private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { - long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); + public void mmap(ThreadBuffers tb, Blackhole bh) { byte[] dst = tb.heapBuf.array(); - for (int i = 0; i < READS_PER_OP; i++) { - MemorySegment.copy( - mmapSegmentNormal, - ValueLayout.JAVA_BYTE, - startOffset + (long) i * READ_SIZE, - dst, - 0, - READ_SIZE); + long offset = seqPosition; + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); bh.consume(dst[0]); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } } + seqPosition = offset; } - private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { - long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); - byte[] dst = tb.heapBuf.array(); - for (int i = 0; i < READS_PER_OP; i++) { - MemorySegment.copy( - mmapSegmentMadvRandom, - ValueLayout.JAVA_BYTE, - startOffset + (long) i * READ_SIZE, - dst, - 0, - READ_SIZE); - bh.consume(dst[0]); - } - } + // ======== mmap NORMAL + sliding 2MB WILLNEED prefetch window ======== - private void doMmapMadvRandomWillneedReads(ThreadBuffers tb, Blackhole bh) { - long startOffset = ThreadLocalRandom.current().nextLong(MAX_START_OFFSET); + @Benchmark + public void mmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { byte[] dst = tb.heapBuf.array(); + long offset = seqPosition; try { - // Prefetch the entire sequential range up front - MemorySegment slice = mmapSegmentMadvRandom.asSlice(startOffset, TOTAL_READ_PER_OP); - int rc = (int) POSIX_MADVISE.invokeExact(slice, TOTAL_READ_PER_OP, MADV_WILLNEED); + long prefetchOff = offset + (long) readsPerOp * readSize; + if (prefetchOff + PREFETCH_WINDOW <= FILE_SIZE) { + MemorySegment slice = mmapSegmentNormal.asSlice(prefetchOff, PREFETCH_WINDOW); + int rc = (int) POSIX_MADVISE.invokeExact(slice, PREFETCH_WINDOW, MADV_WILLNEED); + } } catch (Throwable t) { throw new RuntimeException(t); } - for (int i = 0; i < READS_PER_OP; i++) { - MemorySegment.copy( - mmapSegmentMadvRandom, - ValueLayout.JAVA_BYTE, - startOffset + (long) i * READ_SIZE, - dst, - 0, - READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); bh.consume(dst[0]); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } } + seqPosition = offset; } - private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { - ThreadLocalRandom rng = ThreadLocalRandom.current(); - ByteBuffer buf = tb.directBuf; - long startOffset = rng.nextLong(MAX_START_OFFSET); - for (int i = 0; i < READS_PER_OP; i++) { - buf.clear(); - int n = fileChannel.read(buf, startOffset + (long) i * READ_SIZE); - bh.consume(n); - } - } - - private void doFileChannelHeapReads(ThreadBuffers tb, Blackhole bh) throws IOException { - ThreadLocalRandom rng = ThreadLocalRandom.current(); - ByteBuffer buf = tb.heapBuf; - long startOffset = rng.nextLong(MAX_START_OFFSET); - for (int i = 0; i < READS_PER_OP; i++) { - buf.clear(); - int n = fileChannel.read(buf, startOffset + (long) i * READ_SIZE); - bh.consume(n); - } - } + // ======== FFI pread ======== - private void doFfiReads(ThreadBuffers tb, Blackhole bh) { - ThreadLocalRandom rng = ThreadLocalRandom.current(); + @Benchmark + public void ffiPread(ThreadBuffers tb, Blackhole bh) { MemorySegment buf = tb.ffiBuf; - long startOffset = rng.nextLong(MAX_START_OFFSET); + long offset = seqPosition; try { - for (int i = 0; i < READS_PER_OP; i++) { - long n = - (long) - PREAD.invokeExact( - nativeFd, buf, (long) READ_SIZE, startOffset + (long) i * READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + long n = (long) PREAD.invokeExact(nativeFd, buf, (long) readSize, offset); bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } } } catch (Throwable t) { throw new RuntimeException(t); } + seqPosition = offset; } - private void doFfiDirectIoReads(ThreadBuffers tb, Blackhole bh) { + // ======== FileChannel + DirectByteBuffer ======== + + @Benchmark + public void fileChannelDirectBuffer(ThreadBuffers tb, Blackhole bh) throws IOException { + ByteBuffer buf = tb.directBuf; + long offset = seqPosition; + for (int i = 0; i < readsPerOp; i++) { + buf.clear().limit(readSize); + int n = fileChannel.read(buf, offset); + bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) offset = 0; + } + seqPosition = offset; + } + + // ======== FFI pread + O_DIRECT ======== + + @Benchmark + public void ffiPreadDirectIO(ThreadBuffers tb, Blackhole bh) { if (directIoFd < 0) { - // O_DIRECT not available on this filesystem — skip silently bh.consume(0); return; } - ThreadLocalRandom rng = ThreadLocalRandom.current(); MemorySegment buf = tb.ffiDirectIoBuf; - // Align start offset for O_DIRECT - long startOffset = (rng.nextLong(MAX_ALIGNED_START / ALIGNMENT)) * ALIGNMENT; + long offset = (seqPosition / ALIGNMENT) * ALIGNMENT; try { - for (int i = 0; i < READS_PER_OP; i++) { - long n = - (long) - PREAD.invokeExact( - directIoFd, buf, (long) READ_SIZE, startOffset + (long) i * READ_SIZE); + for (int i = 0; i < readsPerOp; i++) { + long n = (long) PREAD.invokeExact(directIoFd, buf, (long) readSize, offset); bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) offset = 0; } } catch (Throwable t) { throw new RuntimeException(t); } + seqPosition = offset; } } From 7a3d08060e5abd800d5bcf22b57565153a6af5a0 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Fri, 26 Jun 2026 14:57:04 +0800 Subject: [PATCH 8/9] @SuppressWarnings("unused") for retcode of native calls --- .../apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java | 2 +- .../org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java | 2 ++ .../apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index f1a5e264e3a5..4ca74b327dcf 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -41,7 +41,7 @@ * posix_madvise, fcntl), thread-local buffers, file/mmap setup, and configuration parsing. */ @State(Scope.Benchmark) -@SuppressWarnings("restricted") +@SuppressWarnings({"restricted", "unused"}) public abstract class AbstractReadIOBenchmark { protected static final long ALIGNMENT = 4096; diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java index d14b07e940f3..a7dafed80481 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -268,6 +268,7 @@ private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { } } + @SuppressWarnings("unused") private void doMmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); byte[] dst = tb.heapBuf.array(); @@ -289,6 +290,7 @@ private void doMmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { } } + @SuppressWarnings("unused") private void doMmapMadvRandomBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { ThreadLocalRandom rng = ThreadLocalRandom.current(); byte[] dst = tb.heapBuf.array(); diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java index fd1cfb8a44a4..536e2dddb5cc 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java @@ -81,6 +81,7 @@ public void mmap(ThreadBuffers tb, Blackhole bh) { // ======== mmap NORMAL + sliding 2MB WILLNEED prefetch window ======== @Benchmark + @SuppressWarnings("unused") public void mmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { byte[] dst = tb.heapBuf.array(); long offset = seqPosition; From e58c37de583f43a743c50343c89aa5777a6376a4 Mon Sep 17 00:00:00 2001 From: neoReMinD Date: Fri, 26 Jun 2026 23:14:41 +0800 Subject: [PATCH 9/9] Fix Forbidden field access --- .../jmh/AbstractReadIOBenchmark.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java index 4ca74b327dcf..b44f16672d19 100644 --- a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -30,6 +30,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Locale; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; @@ -41,7 +42,6 @@ * posix_madvise, fcntl), thread-local buffers, file/mmap setup, and configuration parsing. */ @State(Scope.Benchmark) -@SuppressWarnings({"restricted", "unused"}) public abstract class AbstractReadIOBenchmark { protected static final long ALIGNMENT = 4096; @@ -62,9 +62,10 @@ public abstract class AbstractReadIOBenchmark { protected static final int MADV_RANDOM = 1; protected static final int MADV_WILLNEED = 3; - private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac"); + private static final boolean IS_MAC = + System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); private static final boolean IS_LINUX = - System.getProperty("os.name").toLowerCase().contains("linux"); + System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("linux"); // FFI handles protected static final MethodHandle PREAD; @@ -161,12 +162,6 @@ protected void validateReadSize(int readSize) { @Setup(Level.Trial) public void setup() throws Exception { - System.out.println("[bench] ===== " + benchmarkName() + " Configuration ====="); - System.out.println("[bench] file: " + BENCH_FILE); - System.out.println("[bench] fileSizeMB: " + (FILE_SIZE / (1024 * 1024))); - System.out.println("[bench] dropCaches: " + DROP_CACHES); - System.out.println("[bench] ==============================================="); - tempFile = Path.of(BENCH_FILE); if (!Files.exists(tempFile)) { throw new IOException( @@ -200,7 +195,7 @@ public void setup() throws Exception { try { int rc = (int) POSIX_MADVISE.invokeExact(mmapSegmentMadvRandom, FILE_SIZE, MADV_RANDOM); if (rc != 0) { - System.err.println("WARNING: posix_madvise(MADV_RANDOM) returned " + rc); + throw new IllegalStateException("WARNING: posix_madvise(MADV_RANDOM) returned " + rc); } } catch (Throwable t) { throw new RuntimeException("posix_madvise(MADV_RANDOM) failed", t); @@ -228,9 +223,8 @@ public void setup() throws Exception { int F_NOCACHE = 48; int rc = (int) FCNTL.invokeExact(directIoFd, F_NOCACHE, 1); if (rc != 0) { - System.err.println("WARNING: fcntl(F_NOCACHE) failed"); CLOSE.invokeExact(directIoFd); - directIoFd = -1; + throw new IllegalStateException("WARNING: fcntl(F_NOCACHE) failed"); } } } else { @@ -240,12 +234,12 @@ public void setup() throws Exception { throw new RuntimeException("Failed to open file for direct I/O", t); } if (directIoFd < 0) { - System.err.println("WARNING: Direct I/O unavailable. Those benchmarks will skip."); directIoFd = -1; } } @TearDown(Level.Trial) + @SuppressWarnings({"restricted", "unused"}) public void tearDown() throws Exception { fileChannel.close(); try { @@ -301,7 +295,6 @@ private static void dropPageCaches() throws IOException { throw new IOException("Interrupted during drop_caches", e); } } - System.out.println("[bench] Page caches dropped."); } /** Reads a config value from env var first, then system property, then default. */