diff --git a/src/java.base/share/classes/java/lang/foreign/SegmentAllocator.java b/src/java.base/share/classes/java/lang/foreign/SegmentAllocator.java index 1297406dcf194..56d38834f2b78 100644 --- a/src/java.base/share/classes/java/lang/foreign/SegmentAllocator.java +++ b/src/java.base/share/classes/java/lang/foreign/SegmentAllocator.java @@ -27,6 +27,7 @@ import jdk.internal.foreign.AbstractMemorySegmentImpl; import jdk.internal.foreign.ArenaImpl; +import jdk.internal.foreign.NoInitSegmentAllocator; import jdk.internal.foreign.SlicingAllocator; import jdk.internal.foreign.StringSupport; import jdk.internal.vm.annotation.ForceInline; @@ -720,22 +721,22 @@ private static void assertWritable(MemorySegment segment) { @ForceInline private MemorySegment allocateNoInit(long byteSize) { - return this instanceof ArenaImpl arenaImpl ? - arenaImpl.allocateNoInit(byteSize, 1) : + return this instanceof NoInitSegmentAllocator noInit ? + noInit.allocateNoInit(byteSize, 1) : allocate(byteSize); } @ForceInline private MemorySegment allocateNoInit(MemoryLayout layout) { - return this instanceof ArenaImpl arenaImpl ? - arenaImpl.allocateNoInit(layout.byteSize(), layout.byteAlignment()) : + return this instanceof NoInitSegmentAllocator noInit ? + noInit.allocateNoInit(layout.byteSize(), layout.byteAlignment()) : allocate(layout); } @ForceInline private MemorySegment allocateNoInit(MemoryLayout layout, long size) { - return this instanceof ArenaImpl arenaImpl ? - arenaImpl.allocateNoInit(layout.byteSize() * size, layout.byteAlignment()) : + return this instanceof NoInitSegmentAllocator noInit ? + noInit.allocateNoInit(layout.byteSize() * size, layout.byteAlignment()) : allocate(layout, size); } } diff --git a/src/java.base/share/classes/jdk/internal/foreign/ArenaImpl.java b/src/java.base/share/classes/jdk/internal/foreign/ArenaImpl.java index da8a6c2666637..a9df67c6c4ff2 100644 --- a/src/java.base/share/classes/jdk/internal/foreign/ArenaImpl.java +++ b/src/java.base/share/classes/jdk/internal/foreign/ArenaImpl.java @@ -25,10 +25,12 @@ package jdk.internal.foreign; +import jdk.internal.vm.annotation.ForceInline; + import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment.Scope; -public final class ArenaImpl implements Arena { +public final class ArenaImpl implements Arena, NoInitSegmentAllocator { private final MemorySessionImpl session; private final boolean shouldReserveMemory; @@ -47,15 +49,16 @@ public void close() { session.close(); } + @ForceInline + @Override public NativeMemorySegmentImpl allocateNoInit(long byteSize, long byteAlignment) { Utils.checkAllocationSizeAndAlign(byteSize, byteAlignment); return SegmentFactories.allocateSegment(byteSize, byteAlignment, session, shouldReserveMemory); } + @ForceInline @Override public NativeMemorySegmentImpl allocate(long byteSize, long byteAlignment) { - NativeMemorySegmentImpl segment = allocateNoInit(byteSize, byteAlignment); - segment.fill((byte)0); - return segment; + return NoInitSegmentAllocator.super.allocate(byteSize, byteAlignment); } } diff --git a/src/java.base/share/classes/jdk/internal/foreign/CaptureStateUtil.java b/src/java.base/share/classes/jdk/internal/foreign/CaptureStateUtil.java new file mode 100644 index 0000000000000..ddeb57111ceab --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/foreign/CaptureStateUtil.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.foreign; + +import jdk.internal.invoke.MhUtil; +import jdk.internal.vm.annotation.ForceInline; + +import java.lang.foreign.Arena; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * An internal utility class that can be used to adapt system-call-styled method handles + * for efficient and easy use. + */ +public final class CaptureStateUtil { + + private static final StructLayout CAPTURE_LAYOUT = Linker.Option.captureStateLayout(); + private static final CarrierLocalArenaPools POOL = CarrierLocalArenaPools.create(CAPTURE_LAYOUT); + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + // The method handles below are bound to static methods residing in this class + + private static final MethodHandle NON_NEGATIVE_INT_MH = + MhUtil.findStatic(LOOKUP, "nonNegative", + MethodType.methodType(boolean.class, int.class)); + + private static final MethodHandle SUCCESS_INT_MH = + MhUtil.findStatic(LOOKUP, "success", + MethodType.methodType(int.class, int.class, MemorySegment.class)); + + private static final MethodHandle ERROR_INT_MH = + MhUtil.findStatic(LOOKUP, "error", + MethodType.methodType(int.class, MethodHandle.class, int.class, MemorySegment.class)); + + private static final MethodHandle NON_NEGATIVE_LONG_MH = + MhUtil.findStatic(LOOKUP, "nonNegative", + MethodType.methodType(boolean.class, long.class)); + + private static final MethodHandle SUCCESS_LONG_MH = + MhUtil.findStatic(LOOKUP, "success", + MethodType.methodType(long.class, long.class, MemorySegment.class)); + + private static final MethodHandle ERROR_LONG_MH = + MhUtil.findStatic(LOOKUP, "error", + MethodType.methodType(long.class, MethodHandle.class, long.class, MemorySegment.class)); + + private static final MethodHandle ACQUIRE_ARENA_MH = + MhUtil.findStatic(LOOKUP, "acquireArena", + MethodType.methodType(Arena.class)); + + private static final MethodHandle ALLOCATE_MH = + MhUtil.findStatic(LOOKUP, "allocate", + MethodType.methodType(MemorySegment.class, Arena.class)); + + private static final MethodHandle ARENA_CLOSE_MH = + MhUtil.findVirtual(LOOKUP, Arena.class, "close", + MethodType.methodType(void.class)); + + // The `BASIC_HANDLE_CACHE` contains the common "basic handles" that can be reused for + // all adapted method handles. Keeping as much as possible reusable reduces the number + // of combinators needed to form an adapted method handle. + // The map is lazily computed. + // + private static final Map BASIC_HANDLE_CACHE = + new ConcurrentHashMap<>(); + + // A key that holds both the `returnType` and the `stateName` needed to look up a + // specific "basic handle" in the `BASIC_HANDLE_CACHE`. + // returnType E {int.class | long.class} + // stateName can be anything non-null but should E {"GetLastError" | "WSAGetLastError"} | "errno")} + record BasicKey(Class returnType, String stateName) { + + BasicKey(MethodHandle target, String stateName) { + this(returnType(target), Objects.requireNonNull(stateName)); + } + + static Class returnType(MethodHandle target) { + // Implicit null check + final Class returnType = target.type().returnType(); + + if (!(returnType.equals(int.class) || returnType.equals(long.class))) { + throw illegalArgDoesNot(target, "return an int or a long"); + } + if (target.type().parameterCount() == 0 || target.type().parameterType(0) != MemorySegment.class) { + throw illegalArgDoesNot(target, "have a MemorySegment as the first parameter"); + } + return returnType; + } + + private static IllegalArgumentException illegalArgDoesNot(MethodHandle target, String info) { + return new IllegalArgumentException("The provided target " + target + + " does not " + info); + } + + } + + private CaptureStateUtil() {} + + /** + * {@return a new MethodHandle that adapts the provided {@code target} so that it + * directly returns the same value as the {@code target} if it is non-negative, + * otherwise returns the negated captured state defined by the provided + * {@code stateName}} + *

+ * This method is suitable for adapting system-call method handles(e.g. + * {@code open()}, {@code read()}, and {@code close()}). Clients can check the return + * value as shown in this example: + * {@snippet lang = java: + * // (MemorySegment capture, MemorySegment pathname, int flags)int + * static final MethodHandle CAPTURING_OPEN = ... + * + * // (MemorySegment pathname, int flags)int + * static final MethodHandle OPEN = CaptureStateUtil + * .adaptSystemCall(CAPTURING_OPEN, "errno"); + * + * try { + * int fh = (int)OPEN.invokeExact(pathName, flags); + * if (fh < 0) { + * throw new IOException("Error opening file: errno = " + (-fh)); + * } + * processFile(fh); + * } catch (Throwable t) { + * throw new RuntimeException(t); + * } + * + *} + * + * For a {@code target} method handle that takes a {@code MemorySegment} and two + * {@code int} parameters and returns an {@code int} value, the method returns a new + * method handle that is doing the equivalent of: + *

+ * {@snippet lang = java: + * private static final MemoryLayout CAPTURE_LAYOUT = + * Linker.Option.captureStateLayout(); + * private static final CarrierLocalArenaPools POOL = + * CarrierLocalArenaPools.create(CAPTURE_LAYOUT); + * + * public int invoke(MethodHandle target, + * String stateName, + * int a, int b) { + * try (var arena = POOL.take()) { + * final MemorySegment segment = arena.allocate(CAPTURE_LAYOUT); + * final int result = (int) handle.invoke(segment, a, b); + * if (result >= 0) { + * return result; + * } + * return -(int) CAPTURE_LAYOUT + * .varHandle(MemoryLayout.PathElement.groupElement(stateName)) + * .get(segment, 0); + * } + * } + *} + * except it is more performant. In the above {@code stateName} is the name of the + * captured state (e.g. {@code errno}). The static {@code CAPTURE_LAYOUT} is shared + * across all target method handles adapted by this method. + * + * @param target method handle that returns an {@code int} or a {@code long} and + * has a capturing state MemorySegment as its first parameter + * @param stateName the name of the capturing state member layout (i.e. "errno", + * "GetLastError", or "WSAGetLastError") + * @throws IllegalArgumentException if the provided {@code target}'s return type is + * not {@code int} or {@code long} + * @throws IllegalArgumentException if the provided {@code target}'s first parameter + * type is not {@linkplain MemorySegment} + * @throws IllegalArgumentException if the provided {@code stateName} is unknown on + * the current platform + */ + public static MethodHandle adaptSystemCall(MethodHandle target, + String stateName) { + // Invariants checked in the BasicKey record + final BasicKey basicKey = new BasicKey(target, stateName); + + // ((int | long), MemorySegment)(int | long) + final MethodHandle basicHandle = BASIC_HANDLE_CACHE + // Do not use a lambda in order to allow early use in the init sequence + // This is equivalent to: + // computeIfAbsent(basicKey, CaptureStateUtil::basicHandleFor); + .computeIfAbsent(basicKey, new Function<>() { + @Override + public MethodHandle apply(BasicKey basicKey) { + return basicHandleFor(basicKey); + } + }); + + // Make `target` specific adaptations of the basic handle + + // Pre-pend all the parameters from the `target` MH. + // (C0=MemorySegment, C1-Cn, MemorySegment)(int|long) + MethodHandle innerAdapted = MethodHandles.collectArguments(basicHandle, 0, target); + + final int[] perm = new int[target.type().parameterCount() + 1]; + for (int i = 0; i < target.type().parameterCount(); i++) { + perm[i] = i; + } + // Last takes first + perm[target.type().parameterCount()] = 0; + // Deduplicate the first and last coordinate and only use the first one. + // (C0=MemorySegment, C1-Cn)(int|long) + innerAdapted = MethodHandles.permuteArguments(innerAdapted, target.type(), perm); + + // Use an `Arena` for the first argument instead and extract a segment from it. + // (C0=Arena, C1-Cn)(int|long) + innerAdapted = MethodHandles.collectArguments(innerAdapted, 0, ALLOCATE_MH); + + // Add an identity function for the result of the cleanup action. + // ((int|long))(int|long) + MethodHandle cleanup = MethodHandles.identity(basicKey.returnType()); + // Add a dummy `Throwable` argument for the cleanup action. + // This means, anything thrown will just be propagated. + // (Throwable, (int|long))(int|long) + cleanup = MethodHandles.dropArguments(cleanup, 0, Throwable.class); + // Add the first `Arena` parameter of the `innerAdapted` method handle to the + // cleanup action and invoke `Arena::close` when it is run. The `cleanup` handle + // does not have to have all parameters. It can have zero or more. + // (Throwable, (int|long), Arena)(int|long) + cleanup = MethodHandles.collectArguments(cleanup, 2, ARENA_CLOSE_MH); + + // Combine the `innerAdapted` and `cleanup` action into a try/finally block. + // (Arena, C1-Cn)(int|long) + final MethodHandle tryFinally = MethodHandles.tryFinally(innerAdapted, cleanup); + + // Acquire the arena from the global pool. + // With this, we finally arrive at the intended method handle: + // (C1-Cn)(int|long) + return MethodHandles.collectArguments(tryFinally, 0, ACQUIRE_ARENA_MH); + } + + private static MethodHandle basicHandleFor(BasicKey basicKey) { + final VarHandle vh = CAPTURE_LAYOUT.varHandle( + MemoryLayout.PathElement.groupElement(basicKey.stateName())); + // This MH is used to extract the named captured state + // from the capturing `MemorySegment`. + // (MemorySegment, long)int + MethodHandle intExtractor = vh.toMethodHandle(VarHandle.AccessMode.GET); + // As the MH is already adapted to use the appropriate + // offset, we just insert `0L` for the offset. + // (MemorySegment)int + intExtractor = MethodHandles.insertArguments(intExtractor, 1, 0L); + + // If X is the `returnType` (either `int` or `long`) then + // the code below is equivalent to: + // + // X handle(X returnValue, MemorySegment segment) + // if (returnValue >= 0) { + // // Ignore the segment + // return returnValue; + // } else { + // // ignore the returnValue + // return -(X)intExtractor.invokeExact(segment); + // } + // } + if (basicKey.returnType().equals(int.class)) { + // (int, MemorySegment)int + return MethodHandles.guardWithTest( + NON_NEGATIVE_INT_MH, + SUCCESS_INT_MH, + ERROR_INT_MH.bindTo(intExtractor)); + } else { + // (long, MemorySegment)long + return MethodHandles.guardWithTest( + NON_NEGATIVE_LONG_MH, + SUCCESS_LONG_MH, + ERROR_LONG_MH.bindTo(intExtractor)); + } + } + + // The methods below are reflective used via static MethodHandles + + @ForceInline + private static Arena acquireArena() { + return POOL.take(); + } + + @ForceInline + private static MemorySegment allocate(Arena arena) { + // We do not need to zero out the segment. + return ((NoInitSegmentAllocator) arena) + .allocateNoInit(CAPTURE_LAYOUT.byteSize(), CAPTURE_LAYOUT.byteAlignment()); + } + + @ForceInline + private static boolean nonNegative(int value) { + return value >= 0; + } + + @ForceInline + private static int success(int value, + MemorySegment segment) { + return value; + } + + @ForceInline + private static int error(MethodHandle errorHandle, + int value, + MemorySegment segment) throws Throwable { + return -(int) errorHandle.invokeExact(segment); + } + + @ForceInline + private static boolean nonNegative(long value) { + return value >= 0L; + } + + @ForceInline + private static long success(long value, + MemorySegment segment) { + return value; + } + + @ForceInline + private static long error(MethodHandle errorHandle, + long value, + MemorySegment segment) throws Throwable { + return -(int) errorHandle.invokeExact(segment); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/foreign/CarrierLocalArenaPools.java b/src/java.base/share/classes/jdk/internal/foreign/CarrierLocalArenaPools.java new file mode 100644 index 0000000000000..9919aaa030824 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/foreign/CarrierLocalArenaPools.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.foreign; + +import jdk.internal.access.JavaLangAccess; +import jdk.internal.access.SharedSecrets; +import jdk.internal.misc.CarrierThread; +import jdk.internal.misc.TerminatingThreadLocal; +import jdk.internal.misc.Unsafe; +import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.ref.Reference; +import java.util.Objects; + +public final class CarrierLocalArenaPools { + + @Stable + private final TerminatingThreadLocal tl; + + private CarrierLocalArenaPools(long byteSize, long byteAlignment) { + this.tl = new TerminatingThreadLocal<>() { + + private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess(); + + // This method can be invoked by either a virtual thread, a platform thread + // , or a carrier thread (e.g. ForkJoinPool-1-worker-1). + @Override + protected LocalArenaPoolImpl initialValue() { + if (JLA.currentCarrierThread() instanceof CarrierThread) { + // Only a carrier thread that is an instance of `CarrierThread` can + // ever carry virtual threads. (Notably, a `CarrierThread` can also + // carry a platform thread.) This means a `CarrierThread` can carry + // any number of virtual threads, and they can be mounted/unmounted + // from the carrier thread at almost any time. Therefore, we must use + // stronger-than-plain semantics when dealing with mutual exclusion + // of thread local resources. + return new LocalArenaPoolImpl.OfCarrier(byteSize, byteAlignment); + } else { + // A carrier thread that is not an instance of `CarrierThread` can + // never carry a virtual thread. Because of this, only one thread will + // be mounted on such a carrier thread. Therefore, we can use plain + // memory semantics when dealing with mutual exclusion of thread local + // resources. + return new LocalArenaPoolImpl.OfPlatform(byteSize, byteAlignment); + } + } + + // This method is never invoked by a virtual thread but can be invoked by + // a platform thread or a carrier thread (e.g. ForkJoinPool-1-worker-1). + // Note: the fork join pool can expand/contract dynamically + // We do not use the method here as we are using an automatic arena. + @Override + protected void threadTerminated(LocalArenaPoolImpl pool) {} + }; + } + + @ForceInline + public Arena take() { + return tl.get() + .take(); + } + + private static sealed abstract class LocalArenaPoolImpl { + + static final int AVAILABLE = 0; + static final int TAKEN = 1; + + // Hold a reference so that the arena is not GC:ed before the thread dies. + @Stable + private final Arena originalArena; + @Stable + private final MemorySegment recyclableSegment; + + // Used both directly and reflectively + int segmentAvailability; + + private LocalArenaPoolImpl(long byteSize, + long byteAlignment) { + this.originalArena = Arena.ofAuto(); + this.recyclableSegment = originalArena.allocate(byteSize, byteAlignment); + } + + @ForceInline + public final Arena take() { + final Arena arena = Arena.ofConfined(); + return tryAcquireSegment() + ? new SlicingArena(originalArena, (ArenaImpl) arena, recyclableSegment) + : arena; + } + + /** + * {@return {@code true } if the segment was acquired for exclusive use, {@code + * false} otherwise} + */ + abstract boolean tryAcquireSegment(); + + /** + * Unconditionally releases the acquired segment if it was previously acquired, + * otherwise this is a no-op. + */ + abstract void releaseSegment(); + + /** + * Thread safe implementation. + */ + public static final class OfCarrier + extends LocalArenaPoolImpl { + + // Unsafe allows earlier use in the init sequence and + // better start and warmup properties. + static final Unsafe UNSAFE = Unsafe.getUnsafe(); + static final long SEG_AVAIL_OFFSET = + UNSAFE.objectFieldOffset(LocalArenaPoolImpl.class, "segmentAvailability"); + + public OfCarrier(long byteSize, + long byteAlignment) { + super(byteSize, byteAlignment); + } + + @ForceInline + boolean tryAcquireSegment() { + return UNSAFE.compareAndSetInt(this, SEG_AVAIL_OFFSET, AVAILABLE, TAKEN); + } + + @ForceInline + void releaseSegment() { + UNSAFE.putIntVolatile(this, SEG_AVAIL_OFFSET, AVAILABLE); + } + } + + /** + * No need for thread-safe implementation here as a platform thread is exclusively + * mounted on a particular carrier thread. + */ + public static final class OfPlatform + extends LocalArenaPoolImpl { + + public OfPlatform(long byteSize, + long byteAlignment) { + super(byteSize, byteAlignment); + } + + @ForceInline + boolean tryAcquireSegment() { + if (segmentAvailability == TAKEN) { + return false; + } else { + segmentAvailability = TAKEN; + return true; + } + } + + @ForceInline + void releaseSegment() { + segmentAvailability = AVAILABLE; + } + } + + /** + * A SlicingArena is similar to a {@linkplain SlicingAllocator} but if the backing + * segment cannot be used for allocation, a fall-back arena is used instead. This + * means allocation never fails due to the size and alignment of the backing + * segment. + */ + private final class SlicingArena implements Arena, NoInitSegmentAllocator { + + // In order to prevent use-after-free issues, we make sure the original arena + // is reachable until the dying moments of a carrier thread AND remains + // reachable whenever a carved out segment can be reached. The reason for + // this is reinterpreted segments carved out from the original arena can be + // used independently of the original arena but are freed when the + // original arena is collected. + // + // To solve this, we also hold a reference to the original arena from which we + // carved out the `segment`. This covers the case when a VT was remounted on + // another CarrierThread and the original CarrierThread + // died and therefore the original arena was not referenced anymore. + @Stable + private final Arena originalArena; + @Stable + private final ArenaImpl delegate; + @Stable + private final MemorySegment segment; + @Stable + private final Thread owner; + + private long sp = 0L; + + @ForceInline + private SlicingArena(Arena originalArena, + ArenaImpl delegate, + MemorySegment segment) { + this.originalArena = originalArena; + this.delegate = delegate; + this.segment = segment; + this.owner = Thread.currentThread(); + } + + @ForceInline + @Override + public MemorySegment.Scope scope() { + return delegate.scope(); + } + + @ForceInline + @Override + public NativeMemorySegmentImpl allocate(long byteSize, long byteAlignment) { + return NoInitSegmentAllocator.super.allocate(byteSize, byteAlignment); + } + + @SuppressWarnings("restricted") + @ForceInline + public NativeMemorySegmentImpl allocateNoInit(long byteSize, long byteAlignment) { + final long min = segment.address(); + final long start = Utils.alignUp(min + sp, byteAlignment) - min; + if (start + byteSize <= segment.byteSize()) { + Utils.checkAllocationSizeAndAlign(byteSize, byteAlignment); + final MemorySegment slice = segment.asSlice(start, byteSize, byteAlignment); + sp = start + byteSize; + return fastReinterpret(delegate, (NativeMemorySegmentImpl) slice, byteSize); + } else { + return delegate.allocateNoInit(byteSize, byteAlignment); + } + } + + @ForceInline + @Override + public void close() { + assertOwnerThread(); + delegate.close(); + // This is probably not strictly needed but shows intent + Reference.reachabilityFence(originalArena); + // Intentionally do not releaseSegment() in a finally clause as + // the segment still is in play if close() initially fails (e.g. is closed + // from a non-owner thread). Later on the close() method might be + // successfully re-invoked (e.g. from its owner thread). + LocalArenaPoolImpl.this.releaseSegment(); + } + + @ForceInline + void assertOwnerThread() { + if (owner != Thread.currentThread()) { + throw new WrongThreadException(); + } + } + + } + } + + // Equivalent to but faster than: + // return (NativeMemorySegmentImpl) slice + // .reinterpret(byteSize, delegate, null); */ + @ForceInline + static NativeMemorySegmentImpl fastReinterpret(ArenaImpl arena, + NativeMemorySegmentImpl segment, + long byteSize) { + // We already know the segment: + // * is native + // * we have native access + // * there is no cleanup action + // * the segment is read/write + return SegmentFactories.makeNativeSegmentUnchecked(segment.address(), byteSize, + MemorySessionImpl.toMemorySession(arena), false, null); + } + + public static CarrierLocalArenaPools create(long byteSize) { + if (byteSize < 0) { + throw new IllegalArgumentException(); + } + return new CarrierLocalArenaPools(byteSize, 1L); + } + + public static CarrierLocalArenaPools create(long byteSize, + long byteAlignment) { + Utils.checkAllocationSizeAndAlign(byteSize, byteAlignment); + return new CarrierLocalArenaPools(byteSize, byteAlignment); + } + + public static CarrierLocalArenaPools create(MemoryLayout layout) { + Objects.requireNonNull(layout); + return new CarrierLocalArenaPools(layout.byteSize(), layout.byteAlignment()); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/foreign/NoInitSegmentAllocator.java b/src/java.base/share/classes/jdk/internal/foreign/NoInitSegmentAllocator.java new file mode 100644 index 0000000000000..8bb9fb37add8c --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/foreign/NoInitSegmentAllocator.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.foreign; + +import jdk.internal.vm.annotation.ForceInline; + +import java.lang.foreign.SegmentAllocator; + +public interface NoInitSegmentAllocator extends SegmentAllocator { + + NativeMemorySegmentImpl allocateNoInit(long byteSize, long byteAlignment); + + @ForceInline + @Override + default NativeMemorySegmentImpl allocate(long byteSize, long byteAlignment) { + NativeMemorySegmentImpl segment = allocateNoInit(byteSize, byteAlignment); + segment.fill((byte)0); + return segment; + } +} \ No newline at end of file diff --git a/src/java.base/share/classes/jdk/internal/invoke/MhUtil.java b/src/java.base/share/classes/jdk/internal/invoke/MhUtil.java index 16a4e10221c66..444adccc8186a 100644 --- a/src/java.base/share/classes/jdk/internal/invoke/MhUtil.java +++ b/src/java.base/share/classes/jdk/internal/invoke/MhUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -75,4 +75,14 @@ public static MethodHandle findVirtual(MethodHandles.Lookup lookup, } } + public static MethodHandle findStatic(MethodHandles.Lookup lookup, + String name, + MethodType type) { + try { + return lookup.findStatic(lookup.lookupClass(), name, type); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + } diff --git a/test/jdk/java/foreign/TestCaptureStateUtil.java b/test/jdk/java/foreign/TestCaptureStateUtil.java new file mode 100644 index 0000000000000..c0a6f750c6188 --- /dev/null +++ b/test/jdk/java/foreign/TestCaptureStateUtil.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Test CaptureStateUtil + * @modules java.base/jdk.internal.foreign + * @run junit TestCaptureStateUtil + */ + +import jdk.internal.foreign.CaptureStateUtil; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; + +import static org.junit.jupiter.api.Assertions.*; + +final class TestCaptureStateUtil { + + private static final String ERRNO_NAME = "errno"; + + private static final VarHandle ERRNO_HANDLE = Linker.Option.captureStateLayout() + .varHandle(MemoryLayout.PathElement.groupElement(ERRNO_NAME)); + + private static final MethodHandle INT_DUMMY_HANDLE; + private static final MethodHandle LONG_DUMMY_HANDLE; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + INT_DUMMY_HANDLE = lookup + .findStatic(TestCaptureStateUtil.class, "dummy", + MethodType.methodType(int.class, MemorySegment.class, int.class, int.class)); + LONG_DUMMY_HANDLE = lookup + .findStatic(TestCaptureStateUtil.class, "dummy", + MethodType.methodType(long.class, MemorySegment.class, long.class, int.class)); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + private static final MethodHandle ADAPTED_INT = CaptureStateUtil + .adaptSystemCall(INT_DUMMY_HANDLE, ERRNO_NAME); + private static final MethodHandle ADAPTED_LONG = CaptureStateUtil + .adaptSystemCall(LONG_DUMMY_HANDLE, ERRNO_NAME); + + @Test + void successfulInt() throws Throwable { + int r = (int) ADAPTED_INT.invokeExact(1, 0); + assertEquals(1, r); + } + + private static final int EACCES = 13; /* Permission denied */ + + @Test + void errorInt() throws Throwable { + int r = (int) ADAPTED_INT.invokeExact(-1, EACCES); + assertEquals(-EACCES, r); + } + + @Test + void successfulLong() throws Throwable { + long r = (long) ADAPTED_LONG.invokeExact(1L, 0); + assertEquals(1, r); + } + + @Test + void errorLong() throws Throwable { + long r = (long) ADAPTED_LONG.invokeExact(-1L, EACCES); + assertEquals(-EACCES, r); + } + + @Test + void successfulIntPerHandle() throws Throwable { + MethodHandle handle = CaptureStateUtil + .adaptSystemCall(INT_DUMMY_HANDLE, ERRNO_NAME); + int r = (int) handle.invokeExact(1, 0); + assertEquals(1, r); + } + + @Test + void invariants() throws Throwable { + MethodHandle noSegment = MethodHandles.lookup() + .findStatic(TestCaptureStateUtil.class, "wrongType", + MethodType.methodType(long.class, long.class, int.class)); + + var noSegEx = assertThrows(IllegalArgumentException.class, () -> CaptureStateUtil.adaptSystemCall(noSegment, ERRNO_NAME)); + assertTrue(noSegEx.getMessage().contains("does not have a MemorySegment as the first parameter")); + + MethodHandle noArgMH = MethodHandles.empty(MethodType.methodType(int.class)); + var emptyEx = assertThrows(IllegalArgumentException.class, () -> CaptureStateUtil.adaptSystemCall(noArgMH, ERRNO_NAME)); + assertTrue(emptyEx.getMessage().contains("does not have a MemorySegment as the first parameter")); + + MethodHandle wrongReturnType = MethodHandles.lookup() + .findStatic(TestCaptureStateUtil.class, "wrongType", + MethodType.methodType(short.class, MemorySegment.class, long.class, int.class)); + + var wrongRetEx = assertThrows(IllegalArgumentException.class, () -> CaptureStateUtil.adaptSystemCall(wrongReturnType, ERRNO_NAME)); + assertTrue(wrongRetEx.getMessage().contains("does not return an int or a long")); + + var wrongCaptureName = assertThrows(IllegalArgumentException.class, () -> CaptureStateUtil.adaptSystemCall(LONG_DUMMY_HANDLE, "foo")); + assertTrue(wrongCaptureName.getMessage().startsWith("Bad layout path: cannot resolve 'foo' in layout ["), wrongCaptureName.getMessage()); + + assertThrows(NullPointerException.class, () -> CaptureStateUtil.adaptSystemCall(null, ERRNO_NAME)); + assertThrows(IllegalArgumentException.class, () -> CaptureStateUtil.adaptSystemCall(noSegment, null)); + } + + // Dummy method that is just returning the provided parameters + private static int dummy(MemorySegment segment, int result, int errno) { + ERRNO_HANDLE.set(segment, 0, errno); + return result; + } + + // Dummy method that is just returning the provided parameters + private static long dummy(MemorySegment segment, long result, int errno) { + ERRNO_HANDLE.set(segment, 0, errno); + return result; + } + + private static long wrongType(long result, int errno) { + return 0; + } + + private static short wrongType(MemorySegment segment, long result, int errno) { + return 0; + } + +} \ No newline at end of file diff --git a/test/jdk/java/foreign/TestCarrierLocalArenaPools.java b/test/jdk/java/foreign/TestCarrierLocalArenaPools.java new file mode 100644 index 0000000000000..250e797d36d6d --- /dev/null +++ b/test/jdk/java/foreign/TestCarrierLocalArenaPools.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +/* + * @test + * @summary Test TestCarrierLocalArenaPools + * @library /test/lib + * @modules java.base/jdk.internal.foreign + * @run junit TestCarrierLocalArenaPools + */ + +import java.lang.foreign.Arena; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.VarHandle; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import jdk.internal.foreign.CarrierLocalArenaPools; +import jdk.test.lib.thread.VThreadRunner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static java.lang.foreign.ValueLayout.JAVA_BYTE; +import static java.lang.foreign.ValueLayout.JAVA_LONG; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.*; + +final class TestCarrierLocalArenaPools { + + private static final long POOL_SIZE = 64; + private static final long SMALL_ALLOC_SIZE = 8; + private static final long VERY_LARGE_ALLOC_SIZE = 1L << 10; + + @Test + void invariants1LongArg() { + assertThrows(IllegalArgumentException.class, () -> CarrierLocalArenaPools.create(-1)); + CarrierLocalArenaPools pool = CarrierLocalArenaPools.create(0); + try (var arena = pool.take()) { + // This should come from the underlying arena and not from recyclable memory + assertDoesNotThrow(() -> arena.allocate(1)); + try (var arena2 = pool.take()) { + assertDoesNotThrow(() -> arena.allocate(1)); + } + } + } + + @Test + void invariants2LongArgs() { + assertThrows(IllegalArgumentException.class, () -> CarrierLocalArenaPools.create(-1, 2)); + assertThrows(IllegalArgumentException.class, () -> CarrierLocalArenaPools.create(1, -1)); + assertThrows(IllegalArgumentException.class, () -> CarrierLocalArenaPools.create(1, 3)); + CarrierLocalArenaPools pool = CarrierLocalArenaPools.create(0, 16); + try (var arena = pool.take()) { + // This should come from the underlying arena and not from recyclable memory + assertDoesNotThrow(() -> arena.allocate(1)); + try (var arena2 = pool.take()) { + assertDoesNotThrow(() -> arena.allocate(1)); + } + } + } + + @ParameterizedTest + @MethodSource("pools") + void negativeAlloc(CarrierLocalArenaPools pool) { + Consumer action = arena -> + assertThrows(IllegalArgumentException.class, () -> arena.allocate(-1)); + doInTwoStackedArenas(pool, action, action); + } + + @ParameterizedTest + @MethodSource("pools") + void negativeAllocVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> negativeAlloc(pool)); + } + + @ParameterizedTest + @MethodSource("pools") + void allocateConfinement(CarrierLocalArenaPools pool) { + Consumer allocateAction = arena -> + assertThrows(WrongThreadException.class, () -> { + CompletableFuture future = CompletableFuture.supplyAsync(pool::take); + var otherThreadArena = future.get(); + otherThreadArena.allocate(SMALL_ALLOC_SIZE); + // Intentionally do not close the otherThreadArena here. + }); + doInTwoStackedArenas(pool, allocateAction, allocateAction); + } + + @ParameterizedTest + @MethodSource("pools") + void allocateConfinementVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> allocateConfinement(pool)); + } + + @ParameterizedTest + @MethodSource("pools") + void closeConfinement(CarrierLocalArenaPools pool) { + Consumer closeAction = arena -> { + // Do not use CompletableFuture here as it might accidentally run on the + // same carrier thread as a virtual thread. + AtomicReference otherThreadArena = new AtomicReference<>(); + var thread = Thread.ofPlatform().start(() -> { + otherThreadArena.set(pool.take()); + }); + try { + thread.join(); + } catch (InterruptedException ie) { + fail(ie); + } + assertThrows(WrongThreadException.class, otherThreadArena.get()::close); + }; + doInTwoStackedArenas(pool, closeAction, closeAction); + } + + @ParameterizedTest + @MethodSource("pools") + void closeConfinementVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> closeConfinement(pool)); + } + + @ParameterizedTest + @MethodSource("pools") + void reuse(CarrierLocalArenaPools pool) { + MemorySegment firstSegment; + MemorySegment secondSegment; + try (var arena = pool.take()) { + firstSegment = arena.allocate(SMALL_ALLOC_SIZE); + } + try (var arena = pool.take()) { + secondSegment = arena.allocate(SMALL_ALLOC_SIZE); + } + assertNotSame(firstSegment, secondSegment); + assertNotSame(firstSegment.scope(), secondSegment.scope()); + assertEquals(firstSegment.address(), secondSegment.address()); + assertThrows(IllegalStateException.class, () -> firstSegment.get(JAVA_BYTE, 0)); + assertThrows(IllegalStateException.class, () -> secondSegment.get(JAVA_BYTE, 0)); + } + + @ParameterizedTest + @MethodSource("pools") + void reuseVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> reuse(pool)); + } + + @ParameterizedTest + @MethodSource("pools") + void largeAlloc(CarrierLocalArenaPools pool) { + try (var arena = pool.take()) { + var segment = arena.allocate(VERY_LARGE_ALLOC_SIZE); + assertEquals(VERY_LARGE_ALLOC_SIZE, segment.byteSize()); + } + } + + @ParameterizedTest + @MethodSource("pools") + void largeAllocSizeVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> largeAlloc(pool)); + } + + @Test + void allocationSameAsPoolSize() { + var pool = CarrierLocalArenaPools.create(4); + long firstAddress; + try (var arena = pool.take()) { + var segment = arena.allocate(4); + firstAddress = segment.address(); + } + try (var arena = pool.take()) { + var segment = arena.allocate(4 + 1); + assertNotEquals(firstAddress, segment.address()); + } + for (int i = 0; i < 10; i++) { + try (var arena = pool.take()) { + var segment = arena.allocate(4); + assertEquals(firstAddress, segment.address()); + var segmentTwo = arena.allocate(4); + assertNotEquals(firstAddress, segmentTwo.address()); + } + } + } + + @Test + void allocationCaptureStateLayout() { + var layout = Linker.Option.captureStateLayout(); + var pool = CarrierLocalArenaPools.create(layout); + long firstAddress; + try (var arena = pool.take()) { + var segment = arena.allocate(layout); + firstAddress = segment.address(); + } + for (int i = 0; i < 10; i++) { + try (var arena = pool.take()) { + var segment = arena.allocate(layout); + assertEquals(firstAddress, segment.address()); + var segmentTwo = arena.allocate(layout); + assertNotEquals(firstAddress, segmentTwo.address()); + } + } + } + + @ParameterizedTest + @MethodSource("pools") + void outOfOrderUse(CarrierLocalArenaPools pool) { + Arena firstArena = pool.take(); + Arena secondArena = pool.take(); + firstArena.close(); + Arena thirdArena = pool.take(); + secondArena.close(); + thirdArena.close(); + } + + @ParameterizedTest + @MethodSource("pools") + void zeroing(CarrierLocalArenaPools pool) { + try (var arena = pool.take()) { + var seg = arena.allocate(SMALL_ALLOC_SIZE); + seg.fill((byte) 1); + } + try (var arena = pool.take()) { + var seg = arena.allocate(SMALL_ALLOC_SIZE); + for (int i = 0; i < SMALL_ALLOC_SIZE; i++) { + assertEquals((byte) 0, seg.get(JAVA_BYTE, i)); + } + } + } + + @ParameterizedTest + @MethodSource("pools") + void zeroingVt(CarrierLocalArenaPools pool) { + VThreadRunner.run(() -> zeroing(pool)); + } + + @ParameterizedTest + @MethodSource("pools") + void useAfterFree(CarrierLocalArenaPools pool) { + MemorySegment segment = null; + try (var arena = pool.take()){ + segment = arena.allocate(SMALL_ALLOC_SIZE); + } + final var closedSegment = segment; + var e = assertThrows(IllegalStateException.class, () -> closedSegment.get(ValueLayout.JAVA_INT, 0)); + assertEquals("Already closed", e.getMessage()); + } + + @ParameterizedTest + @MethodSource("pools") + void toStringTest(CarrierLocalArenaPools pool) { + assertTrue(pool.toString().contains("ArenaPool")); + try (var arena = pool.take()) { + assertTrue(arena.toString().contains("SlicingArena")); + } + } + + private static final VarHandle LONG_HANDLE = JAVA_LONG.varHandle(); + + /** + * The objective of this test is to try to provoke a situation where threads are + * competing to use allocated pooled memory and then trying to make sure no thread + * can see the same shared memory another thread is using. + */ + @Test + void stress() throws InterruptedException { + + // Force the ForkJoin pool to expand/contract so that VT:s will be allocated + // on FJP threads that are later terminated. + long sum = LongStream.range(0, ForkJoinPool.getCommonPoolParallelism() * 2L) + .parallel() + .boxed() + // Using a CompletableFuture expands the FJP + .map(i -> CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + return i; + })) + .map(CompletableFuture::join) + .mapToLong(Long::longValue) + .sum(); + + // Just use one pool variant as testing here is fairly expensive. + final CarrierLocalArenaPools pool = CarrierLocalArenaPools.create(POOL_SIZE); + // Make sure it works for both virtual and platform threads (as they are handled differently) + for (var threadBuilder : List.of(Thread.ofVirtual(), Thread.ofPlatform())) { + final Thread[] threads = IntStream.range(0, 1024).mapToObj(_ -> + threadBuilder.start(() -> { + final long threadId = Thread.currentThread().threadId(); + while (!Thread.interrupted()) { + for (int i = 0; i < 1_000_000; i++) { + try (Arena arena = pool.take()) { + // Try to assert no two threads get allocated the same memory region. + final MemorySegment segment = arena.allocate(JAVA_LONG); + LONG_HANDLE.setVolatile(segment, 0L, threadId); + assertEquals(threadId, (long) LONG_HANDLE.getVolatile(segment, 0L)); + } + } + Thread.yield(); // make sure the driver thread gets a chance. + } + })).toArray(Thread[]::new); + Thread.sleep(Duration.of(5, SECONDS)); + Arrays.stream(threads).forEach( + thread -> { + assertTrue(thread.isAlive()); + thread.interrupt(); + }); + } + } + + // Factories and helper methods + + static Stream pools() { + return Stream.of( + CarrierLocalArenaPools.create(POOL_SIZE), + CarrierLocalArenaPools.create(POOL_SIZE, 16), + CarrierLocalArenaPools.create(MemoryLayout.sequenceLayout(POOL_SIZE, JAVA_BYTE)) + ); + } + + static void doInTwoStackedArenas(CarrierLocalArenaPools pool, + Consumer firstAction, + Consumer secondAction) { + try (var firstArena = pool.take()) { + firstAction.accept(firstArena); + try (var secondArena = pool.take()) { + secondAction.accept(secondArena); + } + } + } + +} \ No newline at end of file diff --git a/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolBench.java b/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolBench.java new file mode 100644 index 0000000000000..e4463e54fbdcb --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolBench.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +package org.openjdk.bench.java.lang.foreign; + +import jdk.internal.foreign.CarrierLocalArenaPools; +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.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.foreign.Arena; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgs = { + "--add-exports=java.base/jdk.internal.foreign=ALL-UNNAMED", + "--enable-native-access=ALL-UNNAMED"}) +public class ArenaPoolBench { + + private static final CarrierLocalArenaPools POOLS = CarrierLocalArenaPools.create(64, 8); + + @Param({"4", "16", "64", "512"}) + public int ELEM_SIZE; + + @Benchmark + public long confined() { + try (var arena = Arena.ofConfined()) { + return arena.allocate(ELEM_SIZE).address(); + } + } + + @Benchmark + public long pooled() { + try (var arena = POOLS.take()) { + return arena.allocate(ELEM_SIZE).address(); + } + } + + @Fork(value = 3, jvmArgsAppend = "-Djmh.executor=VIRTUAL") + public static class OfVirtual extends ArenaPoolBench { + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolFromBench.java b/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolFromBench.java new file mode 100644 index 0000000000000..68bef1593f55f --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/foreign/ArenaPoolFromBench.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +package org.openjdk.bench.java.lang.foreign; + +import jdk.internal.foreign.CarrierLocalArenaPools; +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.Warmup; + +import java.lang.foreign.Arena; +import java.lang.foreign.ValueLayout; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgs = { + "--add-exports=java.base/jdk.internal.foreign=ALL-UNNAMED", + "--enable-native-access=ALL-UNNAMED"}) +public class ArenaPoolFromBench { + + private static final CarrierLocalArenaPools POOLS = CarrierLocalArenaPools.create(32); + + private static final String TEXT = "The quick brown fox"; + + @Benchmark + public long confinedInt() { + try (var arena = Arena.ofConfined()) { + return arena.allocateFrom(ValueLayout.JAVA_INT, 42).address(); + } + } + + @Benchmark + public long confinedSting() { + try (var arena = Arena.ofConfined()) { + return arena.allocateFrom(TEXT).address(); + } + } + + @Benchmark + public long pooledInt() { + try (var arena = POOLS.take()) { + return arena.allocateFrom(ValueLayout.JAVA_INT, 42).address(); + } + } + + @Benchmark + public long pooledString() { + try (var arena = POOLS.take()) { + return arena.allocateFrom(TEXT).address(); + } + } + + @Fork(value = 3, jvmArgsAppend = "-Djmh.executor=VIRTUAL") + public static class OfVirtual extends ArenaPoolFromBench { + } + +} \ No newline at end of file diff --git a/test/micro/org/openjdk/bench/java/lang/foreign/CaptureStateUtilBench.java b/test/micro/org/openjdk/bench/java/lang/foreign/CaptureStateUtilBench.java new file mode 100644 index 0000000000000..8d7a3af3759c0 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/foreign/CaptureStateUtilBench.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.openjdk.bench.java.lang.foreign; + +import jdk.internal.foreign.CaptureStateUtil; +import jdk.internal.foreign.CarrierLocalArenaPools; +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.Warmup; + +import java.lang.foreign.Arena; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgs = {"--add-exports=java.base/jdk.internal.foreign=ALL-UNNAMED", + "--enable-native-access=ALL-UNNAMED"}) +public class CaptureStateUtilBench { + + private static final CarrierLocalArenaPools POOLS = CarrierLocalArenaPools.create(16); + + private static final String ERRNO_NAME = "errno"; + + private static final VarHandle ERRNO_HANDLE = Linker.Option.captureStateLayout() + .varHandle(MemoryLayout.PathElement.groupElement(ERRNO_NAME)); + + private static final long SIZE = Linker.Option.captureStateLayout().byteSize(); + + private static final MethodHandle DUMMY_EXPLICIT_ALLOC = dummyExplicitAlloc(); + private static final MethodHandle DUMMY_TL_ALLOC = dummyTlAlloc(); + + @Benchmark + public int explicitAllocationSuccess() throws Throwable { + try (var arena = Arena.ofConfined()) { + return (int) DUMMY_EXPLICIT_ALLOC.invokeExact(arena.allocate(SIZE), 0, 0); + } + } + + @Benchmark + public int explicitAllocationFail() throws Throwable { + try (var arena = Arena.ofConfined()) { + return (int) DUMMY_EXPLICIT_ALLOC.invokeExact(arena.allocate(SIZE), -1, 1); + } + } + + @Benchmark + public int tlAllocationSuccess() throws Throwable { + try (var arena = POOLS.take()) { + return (int) DUMMY_EXPLICIT_ALLOC.invokeExact(arena.allocate(SIZE), 0, 0); + } + } + + @Benchmark + public int tlAllocationFail() throws Throwable { + try (var arena = POOLS.take()) { + return (int) DUMMY_EXPLICIT_ALLOC.invokeExact(arena.allocate(SIZE), -1, 1); + } + } + + @Benchmark + public int adaptedSysCallSuccess() throws Throwable { + return (int) DUMMY_TL_ALLOC.invokeExact(0, 0); + } + + @Benchmark + public int adaptedSysCallFail() throws Throwable { + return (int) DUMMY_TL_ALLOC.invokeExact( -1, 1); + } + + private static MethodHandle dummyExplicitAlloc() { + try { + return MethodHandles.lookup().findStatic(CaptureStateUtilBench.class, + "dummy", MethodType.methodType(int.class, MemorySegment.class, int.class, int.class)); + } catch (ReflectiveOperationException roe) { + throw new RuntimeException(roe); + } + } + + private static MethodHandle dummyTlAlloc() { + final MethodHandle handle = dummyExplicitAlloc(); + return CaptureStateUtil.adaptSystemCall(handle, ERRNO_NAME); + } + + // Dummy method that is just returning the provided parameters + private static int dummy(MemorySegment segment, int result, int errno) { + if (errno != 0) { + // Assuming the capture state is only modified upon detecting an error. + ERRNO_HANDLE.set(segment, 0, errno); + } + return result; + } + + @Fork(value = 3, jvmArgsAppend = "-Djmh.executor=VIRTUAL") + public static class OfVirtual extends CaptureStateUtilBench {} + +} \ No newline at end of file