1 /* 2 * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /** 25 * @test 26 * @summary Basic test for JFR jdk.VirtualThreadXXX events 27 * @requires vm.continuations & vm.hasJFR 28 * @modules jdk.jfr java.base/java.lang:+open jdk.management 29 * @library /test/lib 30 * @run junit/othervm/native --enable-native-access=ALL-UNNAMED JfrEvents 31 */ 32 33 import java.io.IOException; 34 import java.nio.file.Path; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.concurrent.Executor; 38 import java.util.concurrent.ExecutorService; 39 import java.util.concurrent.Executors; 40 import java.util.concurrent.RejectedExecutionException; 41 import java.util.concurrent.ThreadFactory; 42 import java.util.concurrent.atomic.AtomicBoolean; 43 import java.util.concurrent.locks.LockSupport; 44 import java.util.stream.Collectors; 45 import java.util.stream.IntStream; 46 import java.util.stream.Stream; 47 48 import jdk.jfr.EventType; 49 import jdk.jfr.Recording; 50 import jdk.jfr.consumer.RecordedEvent; 51 import jdk.jfr.consumer.RecordingFile; 52 53 import jdk.test.lib.thread.VThreadPinner; 54 import jdk.test.lib.thread.VThreadRunner; // ensureParallelism requires jdk.management 55 import jdk.test.lib.thread.VThreadScheduler; 56 import org.junit.jupiter.api.Test; 57 import org.junit.jupiter.api.BeforeAll; 58 import org.junit.jupiter.params.ParameterizedTest; 59 import org.junit.jupiter.params.provider.ValueSource; 60 import static org.junit.jupiter.api.Assertions.*; 61 62 class JfrEvents { 63 64 @BeforeAll 65 static void setup() { 66 // need at least two carriers to test pinning 67 VThreadRunner.ensureParallelism(2); 68 } 69 70 /** 71 * Test jdk.VirtualThreadStart and jdk.VirtualThreadEnd events. 72 */ 73 @Test 74 void testVirtualThreadStartAndEnd() throws Exception { 75 try (Recording recording = new Recording()) { 76 recording.enable("jdk.VirtualThreadStart"); 77 recording.enable("jdk.VirtualThreadEnd"); 78 79 // execute 100 tasks, each in their own virtual thread 80 recording.start(); 81 try { 82 List<Thread> threads = IntStream.range(0, 100) 83 .mapToObj(_ -> Thread.startVirtualThread(() -> { })) 84 .toList(); 85 for (Thread t : threads) { 86 t.join(); 87 } 88 } finally { 89 recording.stop(); 90 } 91 92 Map<String, Integer> events = sumEvents(recording); 93 System.err.println(events); 94 95 int startCount = events.getOrDefault("jdk.VirtualThreadStart", 0); 96 int endCount = events.getOrDefault("jdk.VirtualThreadEnd", 0); 97 assertEquals(100, startCount); 98 assertEquals(100, endCount); 99 } 100 } 101 102 /** 103 * Test jdk.VirtualThreadPinned event when parking while pinned. 104 */ 105 @ParameterizedTest 106 @ValueSource(booleans = { true, false }) 107 void testParkWhenPinned(boolean timed) throws Exception { 108 try (Recording recording = new Recording()) { 109 recording.enable("jdk.VirtualThreadPinned"); 110 recording.start(); 111 112 var started = new AtomicBoolean(); 113 var done = new AtomicBoolean(); 114 var vthread = Thread.startVirtualThread(() -> { 115 VThreadPinner.runPinned(() -> { 116 started.set(true); 117 while (!done.get()) { 118 if (timed) { 119 LockSupport.parkNanos(Long.MAX_VALUE); 120 } else { 121 LockSupport.park(); 122 } 123 } 124 }); 125 }); 126 127 try { 128 // wait for thread to start and park 129 awaitTrue(started); 130 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 131 } finally { 132 done.set(true); 133 LockSupport.unpark(vthread); 134 vthread.join(); 135 recording.stop(); 136 } 137 138 assertContainsPinnedEvent(recording, vthread); 139 } 140 } 141 142 /** 143 * Test jdk.VirtualThreadPinned event when blocking on monitor while pinned. 144 */ 145 @Test 146 void testBlockWhenPinned() throws Exception { 147 try (Recording recording = new Recording()) { 148 recording.enable("jdk.VirtualThreadPinned"); 149 recording.start(); 150 151 Object lock = new Object(); 152 153 var started = new AtomicBoolean(); 154 var vthread = Thread.ofVirtual().unstarted(() -> { 155 VThreadPinner.runPinned(() -> { 156 started.set(true); 157 synchronized (lock) { } 158 }); 159 }); 160 161 try { 162 synchronized (lock) { 163 vthread.start(); 164 // wait for thread to start and block 165 awaitTrue(started); 166 await(vthread, Thread.State.BLOCKED); 167 } 168 } finally { 169 vthread.join(); 170 recording.stop(); 171 } 172 173 assertContainsPinnedEvent(recording, vthread); 174 } 175 } 176 177 /** 178 * Test jdk.VirtualThreadPinned event when waiting with Object.wait while pinned. 179 */ 180 @ParameterizedTest 181 @ValueSource(booleans = { true, false }) 182 void testObjectWaitWhenPinned(boolean timed) throws Exception { 183 try (Recording recording = new Recording()) { 184 recording.enable("jdk.VirtualThreadPinned"); 185 recording.start(); 186 187 Object lock = new Object(); 188 189 var started = new AtomicBoolean(); 190 var vthread = Thread.startVirtualThread(() -> { 191 VThreadPinner.runPinned(() -> { 192 started.set(true); 193 synchronized (lock) { 194 try { 195 if (timed) { 196 lock.wait(Long.MAX_VALUE); 197 } else { 198 lock.wait(); 199 } 200 } catch (InterruptedException e) { 201 fail(); 202 } 203 } 204 }); 205 }); 206 207 try { 208 // wait for thread to start and wait 209 awaitTrue(started); 210 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 211 } finally { 212 synchronized (lock) { 213 lock.notifyAll(); 214 } 215 vthread.join(); 216 recording.stop(); 217 } 218 219 assertContainsPinnedEvent(recording, vthread); 220 } 221 } 222 223 /** 224 * Test jdk.VirtualThreadPinned event when parking in a class initializer. 225 */ 226 @Test 227 void testParkInClassInitializer() throws Exception { 228 class TestClass { 229 static { 230 LockSupport.park(); 231 } 232 static void m() { 233 // do nothing 234 } 235 } 236 237 try (Recording recording = new Recording()) { 238 recording.enable("jdk.VirtualThreadPinned"); 239 recording.start(); 240 241 var started = new AtomicBoolean(); 242 Thread vthread = Thread.startVirtualThread(() -> { 243 started.set(true); 244 TestClass.m(); 245 }); 246 247 try { 248 // wait for it to start and park 249 awaitTrue(started); 250 await(vthread, Thread.State.WAITING); 251 } finally { 252 LockSupport.unpark(vthread); 253 vthread.join(); 254 recording.stop(); 255 } 256 257 assertContainsPinnedEvent(recording, vthread); 258 } 259 } 260 261 /** 262 * Test jdk.VirtualThreadPinned event when blocking on monitor in a class initializer. 263 */ 264 @Test 265 void testBlockInClassInitializer() throws Exception { 266 class LockHolder { 267 static final Object lock = new Object(); 268 } 269 class TestClass { 270 static { 271 synchronized (LockHolder.lock) { } 272 } 273 static void m() { 274 // no nothing 275 } 276 } 277 278 try (Recording recording = new Recording()) { 279 recording.enable("jdk.VirtualThreadPinned"); 280 recording.start(); 281 282 var started = new AtomicBoolean(); 283 Thread vthread = Thread.ofVirtual().unstarted(() -> { 284 started.set(true); 285 TestClass.m(); 286 }); 287 288 try { 289 synchronized (LockHolder.lock) { 290 vthread.start(); 291 // wait for thread to start and block 292 awaitTrue(started); 293 await(vthread, Thread.State.BLOCKED); 294 } 295 } finally { 296 vthread.join(); 297 recording.stop(); 298 } 299 300 assertContainsPinnedEvent(recording, vthread); 301 } 302 } 303 304 /** 305 * Test jdk.VirtualThreadPinned event when waiting for a class initializer. 306 */ 307 @Test 308 void testWaitingForClassInitializer() throws Exception { 309 class TestClass { 310 static { 311 LockSupport.park(); 312 } 313 static void m() { 314 // do nothing 315 } 316 } 317 318 try (Recording recording = new Recording()) { 319 recording.enable("jdk.VirtualThreadPinned"); 320 recording.start(); 321 322 var started1 = new AtomicBoolean(); 323 var started2 = new AtomicBoolean(); 324 325 Thread vthread1 = Thread.ofVirtual().unstarted(() -> { 326 started1.set(true); 327 TestClass.m(); 328 }); 329 Thread vthread2 = Thread.ofVirtual().unstarted(() -> { 330 started2.set(true); 331 TestClass.m(); 332 }); 333 334 try { 335 // start first virtual thread and wait for it to start + park 336 vthread1.start(); 337 awaitTrue(started1); 338 await(vthread1, Thread.State.WAITING); 339 340 // start second virtual thread and wait for it to start 341 vthread2.start(); 342 awaitTrue(started2); 343 344 // give time for second virtual thread to wait on the MutexLocker 345 Thread.sleep(3000); 346 347 } finally { 348 LockSupport.unpark(vthread1); 349 vthread1.join(); 350 vthread2.join(); 351 recording.stop(); 352 } 353 354 // the recording should have a pinned event for vthread2 355 assertContainsPinnedEvent(recording, vthread2); 356 } 357 } 358 359 /** 360 * Test jdk.VirtualThreadSubmitFailed event. 361 */ 362 @Test 363 void testVirtualThreadSubmitFailed() throws Exception { 364 try (Recording recording = new Recording()) { 365 recording.enable("jdk.VirtualThreadSubmitFailed"); 366 367 recording.start(); 368 try (ExecutorService pool = Executors.newCachedThreadPool()) { 369 Executor scheduler = task -> pool.execute(task); 370 371 // create virtual thread that uses custom scheduler 372 ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); 373 374 // start a thread 375 Thread thread = factory.newThread(LockSupport::park); 376 thread.start(); 377 378 // wait for thread to park 379 await(thread, Thread.State.WAITING); 380 381 // shutdown scheduler 382 pool.shutdown(); 383 384 // unpark, the submit should fail 385 try { 386 LockSupport.unpark(thread); 387 fail(); 388 } catch (RejectedExecutionException expected) { } 389 390 // start another thread, it should fail and an event should be recorded 391 try { 392 factory.newThread(LockSupport::park).start(); 393 throw new RuntimeException("RejectedExecutionException expected"); 394 } catch (RejectedExecutionException expected) { } 395 } finally { 396 recording.stop(); 397 } 398 399 List<RecordedEvent> submitFailedEvents = find(recording, "jdk.VirtualThreadSubmitFailed"); 400 System.err.println(submitFailedEvents); 401 assertTrue(submitFailedEvents.size() == 2, "Expected two events"); 402 } 403 } 404 405 /** 406 * Returns the list of events in the given recording with the given name. 407 */ 408 private static List<RecordedEvent> find(Recording recording, String name) throws IOException { 409 Path recordingFile = recordingFile(recording); 410 return RecordingFile.readAllEvents(recordingFile) 411 .stream() 412 .filter(e -> e.getEventType().getName().equals(name)) 413 .toList(); 414 } 415 416 /** 417 * Read the events from the recording and return a map of event name to count. 418 */ 419 private static Map<String, Integer> sumEvents(Recording recording) throws IOException { 420 Path recordingFile = recordingFile(recording); 421 List<RecordedEvent> events = RecordingFile.readAllEvents(recordingFile); 422 return events.stream() 423 .map(RecordedEvent::getEventType) 424 .collect(Collectors.groupingBy(EventType::getName, 425 Collectors.summingInt(x -> 1))); 426 } 427 428 /** 429 * Return the file path to the recording file. 430 */ 431 private static Path recordingFile(Recording recording) throws IOException { 432 Path recordingFile = recording.getDestination(); 433 if (recordingFile == null) { 434 ProcessHandle h = ProcessHandle.current(); 435 recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); 436 recording.dump(recordingFile); 437 } 438 return recordingFile; 439 } 440 441 /** 442 * Assert that a recording contains a jdk.VirtualThreadPinned event on the given thread. 443 */ 444 private void assertContainsPinnedEvent(Recording recording, Thread thread) throws IOException { 445 List<RecordedEvent> pinnedEvents = find(recording, "jdk.VirtualThreadPinned"); 446 assertTrue(pinnedEvents.size() > 0, "No jdk.VirtualThreadPinned events in recording"); 447 System.err.println(pinnedEvents); 448 449 long tid = thread.threadId(); 450 assertTrue(pinnedEvents.stream() 451 .anyMatch(e -> e.getThread().getJavaThreadId() == tid), 452 "jdk.VirtualThreadPinned for javaThreadId = " + tid + " not found"); 453 } 454 455 /** 456 * Waits for the given boolean to be set to true. 457 */ 458 private void awaitTrue(AtomicBoolean b) throws InterruptedException { 459 while (!b.get()) { 460 Thread.sleep(10); 461 } 462 } 463 464 /** 465 * Waits for the given thread to reach a given state. 466 */ 467 private static void await(Thread thread, Thread.State expectedState) throws InterruptedException { 468 Thread.State state = thread.getState(); 469 while (state != expectedState) { 470 Thread.sleep(10); 471 state = thread.getState(); 472 } 473 } 474 }