8349146: [REDO] Implement a better allocator for downcalls

Reviewed-by: mcimadamore, jvernee, liach
This commit is contained in:
Per Minborg 2025-05-02 14:14:59 +00:00
parent 995d54161f
commit 9f9e73d5f9
11 changed files with 989 additions and 21 deletions

View File

@ -0,0 +1,214 @@
/*
* 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.misc.CarrierThreadLocal;
import jdk.internal.vm.annotation.ForceInline;
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentAllocator;
import java.lang.ref.Reference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
/**
* A buffer stack that allows efficient reuse of memory segments. This is useful in cases
* where temporary memory is needed.
* <p>
* Use the factories {@code BufferStack.of(...)} to create new instances of this class.
* <p>
* Note: The reused segments are neither zeroed out before nor after re-use.
*/
public final class BufferStack {
private final long byteSize;
private final long byteAlignment;
private final CarrierThreadLocal<PerThread> tl;
private BufferStack(long byteSize, long byteAlignment) {
this.byteSize = byteSize;
this.byteAlignment = byteAlignment;
this.tl = new CarrierThreadLocal<>() {
@Override
protected BufferStack.PerThread initialValue() {
return BufferStack.PerThread.of(byteSize, byteAlignment);
}
};
}
/**
* {@return a new Arena that tries to provide {@code byteSize} and {@code byteAlignment}
* allocations by recycling the BufferStack's internal memory}
*
* @param byteSize to be reserved from this BufferStack's internal memory
* @param byteAlignment to be used for reservation
*/
@ForceInline
public Arena pushFrame(long byteSize, long byteAlignment) {
return tl.get().pushFrame(byteSize, byteAlignment);
}
/**
* {@return a new Arena that tries to provide {@code byteSize}
* allocations by recycling the BufferStack's internal memory}
*
* @param byteSize to be reserved from this BufferStack's internal memory
*/
@ForceInline
public Arena pushFrame(long byteSize) {
return pushFrame(byteSize, 1);
}
/**
* {@return a new Arena that tries to provide {@code layout}
* allocations by recycling the BufferStack's internal memory}
*
* @param layout for which to reserve internal memory
*/
@ForceInline
public Arena pushFrame(MemoryLayout layout) {
return pushFrame(layout.byteSize(), layout.byteAlignment());
}
@Override
public String toString() {
return "BufferStack[byteSize=" + byteSize + ", byteAlignment=" + byteAlignment + "]";
}
private record PerThread(ReentrantLock lock,
Arena arena,
SlicingAllocator stack,
CleanupAction cleanupAction) {
@ForceInline
public Arena pushFrame(long size, long byteAlignment) {
boolean needsLock = Thread.currentThread().isVirtual() && !lock.isHeldByCurrentThread();
if (needsLock && !lock.tryLock()) {
// Rare: another virtual thread on the same carrier competed for acquisition.
return Arena.ofConfined();
}
if (!stack.canAllocate(size, byteAlignment)) {
if (needsLock) lock.unlock();
return Arena.ofConfined();
}
return new Frame(needsLock, size, byteAlignment);
}
static PerThread of(long byteSize, long byteAlignment) {
final Arena arena = Arena.ofAuto();
return new PerThread(new ReentrantLock(),
arena,
new SlicingAllocator(arena.allocate(byteSize, byteAlignment)),
new CleanupAction(arena));
}
private record CleanupAction(Arena arena) implements Consumer<MemorySegment> {
@Override
public void accept(MemorySegment memorySegment) {
Reference.reachabilityFence(arena);
}
}
private final class Frame implements Arena {
private final boolean locked;
private final long parentOffset;
private final long topOfStack;
private final Arena confinedArena;
private final SegmentAllocator frame;
@SuppressWarnings("restricted")
@ForceInline
public Frame(boolean locked, long byteSize, long byteAlignment) {
this.locked = locked;
this.parentOffset = stack.currentOffset();
final MemorySegment frameSegment = stack.allocate(byteSize, byteAlignment);
this.topOfStack = stack.currentOffset();
this.confinedArena = Arena.ofConfined();
// The cleanup action will keep the original automatic `arena` (from which
// the reusable segment is first allocated) alive even if this Frame
// becomes unreachable but there are reachable segments still alive.
this.frame = new SlicingAllocator(frameSegment.reinterpret(confinedArena, cleanupAction));
}
@ForceInline
private void assertOrder() {
if (topOfStack != stack.currentOffset())
throw new IllegalStateException("Out of order access: frame not top-of-stack");
}
@ForceInline
@Override
@SuppressWarnings("restricted")
public MemorySegment allocate(long byteSize, long byteAlignment) {
// Make sure we are on the right thread and not closed
MemorySessionImpl.toMemorySession(confinedArena).checkValidState();
return frame.allocate(byteSize, byteAlignment);
}
@ForceInline
@Override
public MemorySegment.Scope scope() {
return confinedArena.scope();
}
@ForceInline
@Override
public void close() {
assertOrder();
// the Arena::close method is called "early" as it checks thread
// confinement and crucially before any mutation of the internal
// state takes place.
confinedArena.close();
stack.resetTo(parentOffset);
if (locked) {
lock.unlock();
}
}
}
}
public static BufferStack of(long byteSize, long byteAlignment) {
if (byteSize < 0) {
throw new IllegalArgumentException("Negative byteSize: " + byteSize);
}
if (byteAlignment < 0) {
throw new IllegalArgumentException("Negative byteAlignment: " + byteAlignment);
}
return new BufferStack(byteSize, byteAlignment);
}
public static BufferStack of(long byteSize) {
return new BufferStack(byteSize, 1);
}
public static BufferStack of(MemoryLayout layout) {
// Implicit null check
return of(layout.byteSize(), layout.byteAlignment());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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
@ -38,6 +38,22 @@ public final class SlicingAllocator implements SegmentAllocator {
this.segment = segment;
}
public long currentOffset() {
return sp;
}
public void resetTo(long offset) {
if (offset < 0 || offset > sp)
throw new IllegalArgumentException(String.format("offset %d should be in [0, %d] ", offset, sp));
this.sp = offset;
}
public boolean canAllocate(long byteSize, long byteAlignment) {
long min = segment.address();
long start = Utils.alignUp(min + sp, byteAlignment) - min;
return start + byteSize <= segment.byteSize();
}
MemorySegment trySlice(long byteSize, long byteAlignment) {
long min = segment.address();
long start = Utils.alignUp(min + sp, byteAlignment) - min;

View File

@ -24,9 +24,9 @@
*/
package jdk.internal.foreign.abi;
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.JavaLangInvokeAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.foreign.BufferStack;
import jdk.internal.foreign.CABI;
import jdk.internal.foreign.abi.AbstractLinker.UpcallStubFactory;
import jdk.internal.foreign.abi.aarch64.linux.LinuxAArch64Linker;
@ -390,26 +390,12 @@ public final class SharedUtils {
: chunkOffset;
}
private static final int LINKER_STACK_SIZE = Integer.getInteger("jdk.internal.foreign.LINKER_STACK_SIZE", 256);
private static final BufferStack LINKER_STACK = BufferStack.of(LINKER_STACK_SIZE, 1);
@ForceInline
public static Arena newBoundedArena(long size) {
return new Arena() {
final Arena arena = Arena.ofConfined();
final SegmentAllocator slicingAllocator = SegmentAllocator.slicingAllocator(arena.allocate(size));
@Override
public Scope scope() {
return arena.scope();
}
@Override
public void close() {
arena.close();
}
@Override
public MemorySegment allocate(long byteSize, long byteAlignment) {
return slicingAllocator.allocate(byteSize, byteAlignment);
}
};
return LINKER_STACK.pushFrame(size, 8);
}
public static Arena newEmptyArena() {

View File

@ -784,6 +784,8 @@ jdk/jfr/jvm/TestWaste.java 8282427 generic-
# jdk_foreign
java/foreign/TestBufferStackStress.java 8350455 macosx-all
############################################################################
# Client manual tests

View File

@ -0,0 +1,286 @@
/*
* 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
* @library /test/lib
* @modules java.base/jdk.internal.foreign
* @build NativeTestHelper TestBufferStack
* @run junit/othervm --enable-native-access=ALL-UNNAMED TestBufferStack
*/
import jdk.internal.foreign.BufferStack;
import org.junit.jupiter.api.Test;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentAllocator;
import java.lang.invoke.MethodHandle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import jdk.test.lib.thread.VThreadRunner;
import static java.lang.foreign.MemoryLayout.structLayout;
import static java.lang.foreign.ValueLayout.*;
import static org.junit.jupiter.api.Assertions.*;
final class TestBufferStack extends NativeTestHelper {
private static final long POOL_SIZE = 64;
private static final long SMALL_ALLOC_SIZE = 8;
@Test
void invariants() {
var exBS = assertThrows(IllegalArgumentException.class, () -> BufferStack.of(-1, 1));
assertEquals("Negative byteSize: -1", exBS.getMessage());
var exBA = assertThrows(IllegalArgumentException.class, () -> BufferStack.of(1, -1));
assertEquals("Negative byteAlignment: -1", exBA.getMessage());
assertThrows(NullPointerException.class, () -> BufferStack.of(null));
BufferStack stack = newBufferStack();
assertThrows(IllegalArgumentException.class, () -> stack.pushFrame(-1, 8));
assertThrows(IllegalArgumentException.class, () -> stack.pushFrame(SMALL_ALLOC_SIZE, -1));
try (var arena = stack.pushFrame(SMALL_ALLOC_SIZE, 1)) {
assertThrows(IllegalArgumentException.class, () -> arena.allocate(-1));
assertThrows(IllegalArgumentException.class, () -> arena.allocate(4, -1));
}
}
@Test
void invariantsVt() {
VThreadRunner.run(this::invariants);
}
@Test
void testScopedAllocation() {
int stackSize = 128;
BufferStack stack = newBufferStack();
try (Arena frame1 = stack.pushFrame(3 * JAVA_INT.byteSize(), JAVA_INT.byteAlignment())) {
// Segments have expected sizes and are accessible and allocated consecutively in the same scope.
MemorySegment segment11 = frame1.allocate(JAVA_INT);
assertEquals(frame1.scope(), segment11.scope());
assertEquals(JAVA_INT.byteSize(), segment11.byteSize());
segment11.set(JAVA_INT, 0, 1);
MemorySegment segment12 = frame1.allocate(JAVA_INT);
assertEquals(segment11.address() + JAVA_INT.byteSize(), segment12.address());
assertEquals(JAVA_INT.byteSize(), segment12.byteSize());
assertEquals(frame1.scope(), segment12.scope());
segment12.set(JAVA_INT, 0, 1);
MemorySegment segment2;
try (Arena frame2 = stack.pushFrame(JAVA_LONG.byteSize(), JAVA_LONG.byteAlignment())) {
assertNotEquals(frame1.scope(), frame2.scope());
// same here, but a new scope.
segment2 = frame2.allocate(JAVA_LONG);
assertEquals( segment12.address() + /*segment12 size + frame 1 spare + alignment constraint*/ 3 * JAVA_INT.byteSize(), segment2.address());
assertEquals(JAVA_LONG.byteSize(), segment2.byteSize());
assertEquals(frame2.scope(), segment2.scope());
segment2.set(JAVA_LONG, 0, 1);
// Frames must be closed in stack order.
assertThrows(IllegalStateException.class, frame1::close);
}
// Scope is closed here, inner segments throw.
assertThrows(IllegalStateException.class, () -> segment2.get(JAVA_INT, 0));
// A new stack frame allocates at the same location (but different scope) as the previous did.
try (Arena frame3 = stack.pushFrame(2 * JAVA_INT.byteSize(), JAVA_INT.byteAlignment())) {
MemorySegment segment3 = frame3.allocate(JAVA_INT);
assertEquals(frame3.scope(), segment3.scope());
assertEquals(segment12.address() + 2 * JAVA_INT.byteSize(), segment3.address());
}
// Fallback arena behaves like regular stack frame.
MemorySegment outOfStack;
try (Arena hugeFrame = stack.pushFrame(1024, 4)) {
outOfStack = hugeFrame.allocate(4);
assertEquals(hugeFrame.scope(), outOfStack.scope());
assertTrue(outOfStack.asOverlappingSlice(segment11).isEmpty());
}
assertThrows(IllegalStateException.class, () -> outOfStack.get(JAVA_INT, 0));
// Outer segments are still accessible.
segment11.get(JAVA_INT, 0);
segment12.get(JAVA_INT, 0);
}
}
@Test
void testScopedAllocationVt() {
VThreadRunner.run(this::testScopedAllocation);
}
static {
System.loadLibrary("TestBufferStack");
}
private static final MemoryLayout HVAPoint3D = structLayout(NativeTestHelper.C_DOUBLE, C_DOUBLE, C_DOUBLE);
private static final MemorySegment UPCALL_MH = upcallStub(TestBufferStack.class, "recurse", FunctionDescriptor.of(HVAPoint3D, C_INT));
private static final MethodHandle DOWNCALL_MH = downcallHandle("recurse", FunctionDescriptor.of(HVAPoint3D, C_INT, ADDRESS));
public static MemorySegment recurse(int depth) {
try {
return (MemorySegment) DOWNCALL_MH.invokeExact((SegmentAllocator) Arena.ofAuto(), depth, UPCALL_MH);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Test
void testDeepStack() {
// Each downcall and upcall require 48 bytes of stack.
// After five allocations we start falling back.
MemorySegment point = recurse(10);
assertEquals( 12.0, point.getAtIndex(C_DOUBLE, 0));
assertEquals(11.0, point.getAtIndex(C_DOUBLE, 1));
assertEquals( 10.0, point.getAtIndex(C_DOUBLE, 2));
}
@Test
void testDeepStackVt() {
VThreadRunner.run(this::testDeepStack);
}
@Test
void equals() {
var first = newBufferStack();
var second = newBufferStack();
assertNotEquals(first, second);
assertEquals(first, first);
}
@Test
void allocationSameAsPoolSize() {
MemoryLayout twoInts = MemoryLayout.sequenceLayout(2, JAVA_INT);
var pool = newBufferStack();
long firstAddress;
try (var arena = pool.pushFrame(JAVA_INT)) {
var segment = arena.allocate(JAVA_INT);
firstAddress = segment.address();
}
for (int i = 0; i < 10; i++) {
try (var arena = pool.pushFrame(twoInts)) {
var segment = arena.allocate(JAVA_INT);
assertEquals(firstAddress, segment.address());
var segmentTwo = arena.allocate(JAVA_INT);
assertEquals(firstAddress + JAVA_INT.byteSize(), segmentTwo.address());
// Questionable exception type
assertThrows(IndexOutOfBoundsException.class, () -> arena.allocate(JAVA_INT));
}
}
}
@Test
void allocationSameAsPoolSizeVt() {
VThreadRunner.run(this::allocationSameAsPoolSize);
}
@Test
void allocateConfinement() {
var pool = newBufferStack();
Consumer<Arena> allocateAction = arena ->
assertThrows(WrongThreadException.class, () -> {
CompletableFuture<Arena> future = CompletableFuture.supplyAsync(() -> pool.pushFrame(SMALL_ALLOC_SIZE, 1));
var otherThreadArena = future.get();
otherThreadArena.allocate(SMALL_ALLOC_SIZE);
// Intentionally do not close the otherThreadArena here.
});
doInTwoStackedArenas(pool, allocateAction, allocateAction);
}
@Test
void allocateConfinementVt() {
VThreadRunner.run(this::allocateConfinement);
}
@Test
void closeConfinement() {
var pool = newBufferStack();
Consumer<Arena> closeAction = arena -> {
// Do not use CompletableFuture here as it might accidentally run on the
// same carrier thread as a virtual thread.
AtomicReference<Arena> otherThreadArena = new AtomicReference<>();
var thread = Thread.ofPlatform().start(() -> {
otherThreadArena.set(pool.pushFrame(SMALL_ALLOC_SIZE, 1));
});
try {
thread.join();
} catch (InterruptedException ie) {
fail(ie);
}
assertThrows(WrongThreadException.class, otherThreadArena.get()::close);
};
doInTwoStackedArenas(pool, closeAction, closeAction);
}
@Test
void closeConfinementVt() {
VThreadRunner.run(this::closeConfinement);
}
@Test
void toStringTest() {
BufferStack stack = newBufferStack();
assertEquals("BufferStack[byteSize=" + POOL_SIZE + ", byteAlignment=1]", stack.toString());
}
@Test
void allocBounds() {
BufferStack stack = newBufferStack();
try (var arena = stack.pushFrame(SMALL_ALLOC_SIZE, 1)) {
assertThrows(IllegalArgumentException.class, () -> arena.allocate(-1));
assertDoesNotThrow(() -> arena.allocate(SMALL_ALLOC_SIZE));
assertThrows(IndexOutOfBoundsException.class, () -> arena.allocate(SMALL_ALLOC_SIZE + 1));
}
}
@Test
void accessBounds() {
BufferStack stack = newBufferStack();
try (var arena = stack.pushFrame(SMALL_ALLOC_SIZE, 1)) {
var segment = arena.allocate(SMALL_ALLOC_SIZE);
assertThrows(IndexOutOfBoundsException.class, () -> segment.get(JAVA_BYTE, SMALL_ALLOC_SIZE));
}
}
static void doInTwoStackedArenas(BufferStack pool,
Consumer<Arena> firstAction,
Consumer<Arena> secondAction) {
try (var firstArena = pool.pushFrame(SMALL_ALLOC_SIZE, 1)) {
firstAction.accept(firstArena);
try (var secondArena = pool.pushFrame(SMALL_ALLOC_SIZE, 1)) {
secondAction.accept(secondArena);
}
}
}
private static BufferStack newBufferStack() {
return BufferStack.of(POOL_SIZE, 1);
}
}

View File

@ -0,0 +1,72 @@
/*
* 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
* @modules java.base/jdk.internal.foreign
* @build NativeTestHelper TestBufferStackStress
* @run junit TestBufferStackStress
*/
import jdk.internal.foreign.BufferStack;
import org.junit.jupiter.api.Test;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.time.Duration;
import java.util.Arrays;
import java.util.stream.IntStream;
import static java.lang.foreign.ValueLayout.*;
import static java.time.temporal.ChronoUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.*;
public class TestBufferStackStress {
@Test
public void stress() throws InterruptedException {
BufferStack stack = BufferStack.of(256, 1);
Thread[] vThreads = IntStream.range(0, 1024).mapToObj(_ ->
Thread.ofVirtual().start(() -> {
long threadId = Thread.currentThread().threadId();
while (!Thread.interrupted()) {
for (int i = 0; i < 1_000_000; i++) {
try (Arena arena = stack.pushFrame(JAVA_LONG.byteSize(), JAVA_LONG.byteAlignment())) {
// Try to assert no two vThreads get allocated the same stack space.
MemorySegment segment = arena.allocate(JAVA_LONG);
JAVA_LONG.varHandle().setVolatile(segment, 0L, threadId);
assertEquals(threadId, (long) JAVA_LONG.varHandle().getVolatile(segment, 0L));
}
}
Thread.yield(); // make sure the driver thread gets a chance.
}
})).toArray(Thread[]::new);
Thread.sleep(Duration.of(10, SECONDS));
Arrays.stream(vThreads).forEach(
thread -> {
assertTrue(thread.isAlive());
thread.interrupt();
});
}
}

View File

@ -0,0 +1,136 @@
/*
* 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
* @modules java.base/jdk.internal.foreign
* @build NativeTestHelper TestBufferStackStress2
* @run junit TestBufferStackStress2
*/
import jdk.internal.foreign.BufferStack;
import org.junit.jupiter.api.Test;
import java.io.FileDescriptor;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
final class TestBufferStackStress2 {
private static final long POOL_SIZE = 64;
private static final long SMALL_ALLOC_SIZE = 8;
/**
* The objective with this test is to test the case when a virtual thread VT0 is
* mounted on a carrier thread CT0; VT0 is then suspended; The pool of carrier threads
* are then contracted; VT0 is then remounted on another carrier thread C1. VT0 runs
* for a while when there is a lot of GC activity.
* In other words, we are trying to establish that there is no use-after-free and that
* the original arena, from which reusable segments are initially allocated from, is
* not closed underneath.
* <p>
* Unfortunately, this test takes about 30 seconds as that is the time it takes for
* the pool of carrier threads to be contracted.
*/
@Test
void movingVirtualThreadWithGc() throws InterruptedException {
final long begin = System.nanoTime();
var pool = BufferStack.of(POOL_SIZE, 1);
System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
var done = new AtomicBoolean();
var completed = new AtomicBoolean();
var quiescent = new Object();
var executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
while (!done.get()) {
FileDescriptor.out.sync();
}
return null;
});
executor.submit(() -> {
System.out.println(duration(begin) + "ALLOCATING = " + Thread.currentThread());
try (Arena arena = pool.pushFrame(SMALL_ALLOC_SIZE, 1)) {
MemorySegment segment = arena.allocate(SMALL_ALLOC_SIZE);
done.set(true);
synchronized (quiescent) {
try {
quiescent.wait();
} catch (Throwable ex) {
throw new AssertionError(ex);
}
}
System.out.println(duration(begin) + "ACCESSING SEGMENT");
for (int i = 0; i < 100_000; i++) {
if (i % 100 == 0) {
System.gc();
}
segment.get(ValueLayout.JAVA_BYTE, i % SMALL_ALLOC_SIZE);
}
System.out.println(duration(begin) + "DONE ACCESSING SEGMENT");
}
System.out.println(duration(begin) + "VT DONE");
completed.set(true);
});
long count;
do {
Thread.sleep(1000);
count = Thread.getAllStackTraces().keySet().stream()
.filter(t -> t instanceof ForkJoinWorkerThread)
.count();
} while (count > 0);
System.out.println(duration(begin) + "FJP HAS CONTRACTED");
synchronized (quiescent) {
quiescent.notify();
}
System.out.println(duration(begin) + "CLOSING EXECUTOR");
executor.close();
System.out.println(duration(begin) + "EXECUTOR CLOSED");
assertTrue(completed.get(), "The VT did not complete properly");
}
private static String duration(Long begin) {
var duration = Duration.of(System.nanoTime() - begin, ChronoUnit.NANOS);
long seconds = duration.toSeconds();
int nanos = duration.toNanosPart();
return (Thread.currentThread().isVirtual() ? "VT: " : "PT: ") +
String.format("%3d:%09d ", seconds, nanos);
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.
*/
#include "export.h"
typedef struct { double x, y, z; } HVAPoint3D;
EXPORT HVAPoint3D recurse(int depth, HVAPoint3D (*cb)(int)) {
if (depth == 0) {
HVAPoint3D result = { 2, 1, 0};
return result;
}
HVAPoint3D result = cb(depth - 1);
result.x += 1;
result.y += 1;
result.z += 1;
return result;
}

View File

@ -0,0 +1,79 @@
/*
* 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.BufferStack;
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.Setup;
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 = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 3, jvmArgs = {"--add-exports=java.base/jdk.internal.foreign=ALL-UNNAMED"})
public class BufferStackBench {
@Param({"8", "16", "32"})
public int ELEM_SIZE;
private BufferStack bufferStack;
@Setup
public void setup() {
bufferStack = BufferStack.of(128);
}
@Benchmark
public long confined() {
try (Arena arena = Arena.ofConfined()) {
return arena.allocate(ELEM_SIZE).address();
}
}
@Benchmark
public long buffer() {
try (Arena arena = bufferStack.pushFrame(64, 1)) {
return arena.allocate(ELEM_SIZE).address();
}
}
@Fork(value = 3, jvmArgsAppend = "-Djmh.executor=VIRTUAL")
public static class OfVirtual extends BufferStackBench {}
}

View File

@ -0,0 +1,100 @@
/*
* 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 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.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentAllocator;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.util.concurrent.TimeUnit;
import static org.openjdk.bench.java.lang.foreign.CLayouts.C_DOUBLE;
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@State(org.openjdk.jmh.annotations.Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 3, jvmArgs = { "--enable-native-access=ALL-UNNAMED", "-Djava.library.path=micro/native" })
public class CallOverheadByValue {
public static final MemoryLayout POINT_LAYOUT = MemoryLayout.structLayout(
C_DOUBLE, C_DOUBLE
);
private static final MethodHandle MH_UNIT_BY_VALUE;
private static final MethodHandle MH_UNIT_BY_PTR;
static {
Linker abi = Linker.nativeLinker();
System.loadLibrary("CallOverheadByValue");
SymbolLookup loaderLibs = SymbolLookup.loaderLookup();
MH_UNIT_BY_VALUE = abi.downcallHandle(
loaderLibs.findOrThrow("unit"),
FunctionDescriptor.of(POINT_LAYOUT)
);
MH_UNIT_BY_PTR = abi.downcallHandle(
loaderLibs.findOrThrow("unit_ptr"),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);
}
private static final Arena arena = Arena.ofConfined();
private static final MemorySegment point = arena.allocate(POINT_LAYOUT);
private static final SegmentAllocator BY_VALUE_ALLOCATOR = (SegmentAllocator) (_, _) -> point;
@TearDown
public void tearDown() {
arena.close();
}
@Benchmark
public void byValue() throws Throwable {
// point = unit();
MemorySegment unused = (MemorySegment) MH_UNIT_BY_VALUE.invokeExact(BY_VALUE_ALLOCATOR);
}
@Benchmark
public void byPtr() throws Throwable {
// unit_ptr(&point);
MH_UNIT_BY_PTR.invokeExact(point);
}
@Fork(value = 3, jvmArgsAppend = "-Djmh.executor=VIRTUAL")
public static class OfVirtual extends CallOverheadByValue {}
}

View File

@ -0,0 +1,38 @@
/*
* 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.
*/
#include "export.h"
typedef struct {
double x;
double y;
} DoublePoint;
EXPORT DoublePoint unit() {
DoublePoint result = { 1, 0 };
return result;
}
EXPORT void unit_ptr(DoublePoint* out) {
*out = unit();
}