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.
476 lines
19 KiB
Java
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();
|
|
}
|
|
}
|
|
}
|