java-topology/test/jdk/java/lang/Thread/virtual/MonitorPinnedEvents.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

476 lines
19 KiB
Java

/*
* 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 JFR jdk.VirtualThreadPinned event recorded for contended monitor enter
* and Object.wait when pinned
* @requires vm.continuations & vm.hasJFR
* @modules jdk.jfr jdk.management
* @library /test/lib
* @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorPinnedEvents
*/
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordedFrame;
import jdk.jfr.consumer.RecordedMethod;
import jdk.jfr.consumer.RecordedThread;
import jdk.jfr.consumer.RecordingFile;
import jdk.test.lib.thread.VThreadPinner;
import jdk.test.lib.thread.VThreadRunner; // ensureParallelism requires jdk.management
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class MonitorPinnedEvents {
// log to System.err so inlined with JUnit output
private static final PrintStream log = System.err;
// expected values for "blockingOperation" field in event
private static final String CONTENDED_MONITOR_ENTER = "Contended monitor enter";
private static final String OBJECT_WAIT = "Object.wait";
// expected value for "pinnedReason" field in event
private static final String NATIVE_FRAME = "Native or VM frame on stack";
// block or wait for 2s to allow minimum event duration be tested
private static final int DELAY = 2000;
@BeforeAll
static void setup() {
// need at least two carriers to test pinning
VThreadRunner.ensureParallelism(2);
}
/**
* Test jdk.VirtualThreadPinned recorded for a contended monitor enter.
*/
@Test
void testContentedMonitorEnter() throws Exception {
try (Recording recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned");
recording.start();
Thread vthread;
try {
Object lock = new Object();
synchronized (lock) {
// start virtual thread that blocks trying to acquire monitor while pinned
var ready = new AtomicBoolean();
vthread = Thread.ofVirtual().start(() -> {
VThreadPinner.runPinned(() -> {
ready.set(true);
synchronized (lock) { // <--- blocks here while pinned
}
});
});
await(ready, vthread, Thread.State.BLOCKED);
// sleep before releasing to ensure virtual thread is blocked for >= 2s
Thread.sleep(DELAY);
}
vthread.join();
} finally {
recording.stop();
}
// jdk.VirtualThreadPinned event should be recorded
RecordedEvent event = findPinnedEvent(recording);
testEvent(event, vthread, CONTENDED_MONITOR_ENTER, NATIVE_FRAME, DELAY);
}
}
/**
* Test jdk.VirtualThreadPinned recorded for a contended monitor enter where
* another thread may acquire the monitor when the main thread releases it. If
* the other thread acquires the monitor before the virtual thread then the event
* duration should include the time blocked until the other thread releases it.
*/
@Test
void testContentedMonitorEnter2() throws Exception {
try (Recording recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned");
recording.start();
// the thread that acquires the monitor after the main thread releases it
var nextOwner = new AtomicReference<Thread>();
Thread vthread;
try {
Object lock = new Object();
Thread thread2;
synchronized (lock) {
// start virtual thread that blocks trying to acquire monitor while pinned
var ready1 = new AtomicBoolean();
vthread = Thread.ofVirtual().start(() -> {
VThreadPinner.runPinned(() -> {
ready1.set(true);
synchronized (lock) { // <--- blocks here while pinned
nextOwner.compareAndSet(null, Thread.currentThread());
}
});
});
await(ready1, vthread, Thread.State.BLOCKED);
// start platform thread that blocks trying to acquire monitor
thread2 = Thread.ofPlatform().start(() -> {
synchronized (lock) {
if (nextOwner.compareAndSet(null, Thread.currentThread())) {
// delayed release of monitor if acquired before virtual thread
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
}
}
}
});
} // release lock, vthread or thread2 will acquire
vthread.join();
thread2.join();
} finally {
recording.stop();
}
// jdk.VirtualThreadPinned event should be recorded. If the platform thread
// acquired the monitor before the virtual thread then the event duration
// should be >= DELAY.
RecordedEvent event = findPinnedEvent(recording);
Thread winner = nextOwner.get();
assertNotNull(winner);
int minDuration = winner.isVirtual() ? 0 : DELAY;
testEvent(event, vthread, CONTENDED_MONITOR_ENTER, NATIVE_FRAME, minDuration);
}
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait when notified.
*/
@ParameterizedTest
@ValueSource(booleans = { true, false })
void testObjectWaitNotify(boolean timed) throws Exception {
testObjectWait(timed, false);
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait when interrupted.
*/
@ParameterizedTest
@ValueSource(booleans = { true, false })
void testObjectWaitInterrupt(boolean timed) throws Exception {
testObjectWait(timed, true);
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait.
* @param timed true for a timed-wait, false for an untimed-wait
* @param interrupt true to interrupt the thread, false to notify
*/
private void testObjectWait(boolean timed, boolean interrupt) throws Exception {
try (Recording recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned");
recording.start();
Thread vthread;
try {
Object lock = new Object();
// start virtual thread that waits in Object.wait while pinned
var ready = new AtomicBoolean();
vthread = Thread.ofVirtual().start(() -> {
VThreadPinner.runPinned(() -> {
synchronized (lock) {
ready.set(true);
try {
// wait while pinned
if (timed) {
lock.wait(Long.MAX_VALUE);
} else {
lock.wait();
}
} catch (InterruptedException e) {
}
}
});
});
await(ready, vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
// sleep to ensure virtual thread waits for >= 2s
Thread.sleep(DELAY);
// interrupt or notify thread so it resumes execution
if (interrupt) {
vthread.interrupt();
} else {
synchronized (lock) {
lock.notify();
}
}
vthread.join();
} finally {
recording.stop();
}
// jdk.VirtualThreadPinned event should be recorded
RecordedEvent event = findPinnedEvent(recording);
testEvent(event, vthread, OBJECT_WAIT, NATIVE_FRAME, DELAY);
}
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait where the virtual thread
* is notified but may block when attempting to reenter. One event should be
* recorded. The event duration should include both the waiting time and the time
* blocked to reenter.
*/
@ParameterizedTest
@ValueSource(booleans = { true, false })
void testObjectWaitNotify2(boolean timed) throws Exception {
testObjectWait2(timed, false);
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait where the virtual thread
* is interrupted but may block when attempting to reenter. One event should be
* recorded. The event duration should include both the waiting time and the time
* blocked to reenter.
*/
@ParameterizedTest
@ValueSource(booleans = { true, false })
void testObjectWaitInterrupt2(boolean timed) throws Exception {
testObjectWait2(timed, true);
}
/**
* Test jdk.VirtualThreadPinned recorded for Object.wait where the virtual thread
* blocks when attempting to reenter.
* @param timed true for a timed-wait, false for an untimed-wait
* @param interrupt true to interrupt the thread, false to notify
*/
private void testObjectWait2(boolean timed, boolean interrupt) throws Exception {
try (Recording recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned");
recording.start();
// the thread that acquires the monitor after the main thread releases it
var nextOwner = new AtomicReference<Thread>();
Thread vthread;
try {
Object lock = new Object();
// start virtual thread that waits in Object.wait while pinned
var ready1 = new AtomicBoolean();
vthread = Thread.ofVirtual().start(() -> {
VThreadPinner.runPinned(() -> {
boolean notify = !interrupt;
synchronized (lock) {
ready1.set(true);
try {
// wait while pinned
if (timed) {
lock.wait(Long.MAX_VALUE);
} else {
lock.wait();
}
if (notify) {
nextOwner.compareAndSet(null, Thread.currentThread());
}
} catch (InterruptedException e) {
if (interrupt) {
nextOwner.compareAndSet(null, Thread.currentThread());
}
}
}
});
});
await(ready1, vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
// platform thread that blocks on monitor enter
Thread thread2;
synchronized (lock) {
thread2 = Thread.ofPlatform().start(() -> {
synchronized (lock) { // <--- blocks here
if (nextOwner.compareAndSet(null, Thread.currentThread())) {
// delayed release of monitor if acquired before virtual thread
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
}
}
}
});
// interrupt/notify and release monitor to allow one of the threads to acquire
if (interrupt) {
vthread.interrupt();
} else {
lock.notify();
}
}
vthread.join();
thread2.join();
} finally {
recording.stop();
}
// jdk.VirtualThreadPinned event should be recorded. If the platform thread
// acquired the monitor before the virtual thread re-entered then the event
// duration should be >= DELAY.
RecordedEvent event = findPinnedEvent(recording);
Thread winner = nextOwner.get();
assertNotNull(winner);
int minDuration = winner.isVirtual() ? 0 : DELAY;
testEvent(event, vthread, OBJECT_WAIT, NATIVE_FRAME, minDuration);
}
}
/**
* Test that the event was recorded by the expected virtual thread, the event has a
* "carrierThread", the "blockingOperation", and "pinnedReason" fields have the
* expected values, and the event duration is >= minDuration.
*/
private void testEvent(RecordedEvent event,
Thread vthread,
String expectedBlockingOp,
String expectedPinnedReason,
int minDuration) {
assertTrue(vthread.isVirtual());
assertEquals(vthread.threadId(), event.getThread().getId());
RecordedThread carrier = event.getValue("carrierThread");
assertFalse(carrier.isVirtual());
assertEquals(expectedBlockingOp, event.getString("blockingOperation"));
assertEquals(expectedPinnedReason, event.getString("pinnedReason"));
long duration = event.getDuration().toMillis();
assertTrue(duration >= (minDuration - 100),
"Duration " + duration + "ms, expected >= " + minDuration + "ms");
}
/**
* Find the expected jdk.VirtualThreadPinned event in the recording. There may be
* several pinned events recorded by other parts of the system that need to be
* filtered out.
*/
private RecordedEvent findPinnedEvent(Recording recording) throws IOException {
Map<Boolean, List<RecordedEvent>> events = find(recording, "jdk.VirtualThreadPinned")
.collect(Collectors.partitioningBy(e -> filtered(topFrameMethod(e)), Collectors.toList()));
List<RecordedEvent> filteredEvents = events.get(Boolean.TRUE);
List<RecordedEvent> pinnedEvents = events.get(Boolean.FALSE);
log.format("%d event(s) recorded%n", filteredEvents.size() + pinnedEvents.size());
log.println("-- filtered events --");
log.println(filteredEvents);
log.println("-- remaining pinned events --");
log.println(pinnedEvents);
assertEquals(1, pinnedEvents.size());
return pinnedEvents.get(0);
}
/**
* Pinned events recorded at the following classes/methods should be ignored.
*/
private final Set<String> FILTERED = Set.of(
"java.lang.VirtualThread.*",
"java.lang.ref.ReferenceQueue.poll",
"java.lang.invoke.*",
"jdk.internal.ref.*"
);
/**
* Returns true if the given top-frame {@code class.method} should be ignored.
*/
private boolean filtered(String topFrameMethod) {
for (String s : FILTERED) {
if (s.endsWith("*")) {
String prefix = s.substring(0, s.length()-1);
if (topFrameMethod.startsWith(prefix)) {
return true;
}
} else if (s.equals(topFrameMethod)) {
return true;
}
}
return false;
}
/**
* Returns the {@code class.method} of the top frame in the event's stack trace.
*/
private String topFrameMethod(RecordedEvent event) {
RecordedFrame topFrame = event.getStackTrace().getFrames().get(0);
RecordedMethod method = topFrame.getMethod();
return method.getType().getName() + "." + method.getName();
}
/**
* Returns the events with the given name in the recording.
*/
private Stream<RecordedEvent> find(Recording recording, String name) throws IOException {
Path recordingFile = recording.getDestination();
if (recordingFile == null) {
ProcessHandle h = ProcessHandle.current();
recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr");
recording.dump(recordingFile);
}
return RecordingFile.readAllEvents(recordingFile)
.stream()
.filter(e -> e.getEventType().getName().equals(name));
}
/**
* Waits for ready to become true, then waits for the thread to get to the given state.
*/
private void await(AtomicBoolean ready,
Thread thread,
Thread.State expectedState) throws InterruptedException {
while (!ready.get()) {
Thread.sleep(10);
}
Thread.State state = thread.getState();
while (state != expectedState) {
Thread.sleep(10);
state = thread.getState();
}
}
}