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 while pinned. 306 */ 307 @Test 308 void testWaitingForClassInitializerWhenPinned() 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 VThreadPinner.runPinned(() -> { 332 TestClass.m(); 333 }); 334 }); 335 336 try { 337 // start first virtual thread and wait for it to start + park 338 vthread1.start(); 339 awaitTrue(started1); 340 await(vthread1, Thread.State.WAITING); 341 342 // start second virtual thread and wait for it to start 343 vthread2.start(); 344 awaitTrue(started2); 345 346 // give time for second virtual thread to wait in VM 347 Thread.sleep(3000); 348 349 } finally { 350 LockSupport.unpark(vthread1); 351 vthread1.join(); 352 vthread2.join(); 353 recording.stop(); 354 } 355 356 // the recording should have a pinned event for vthread2 357 assertContainsPinnedEvent(recording, vthread2); 358 } 359 } 360 361 /** 362 * Test jdk.VirtualThreadSubmitFailed event. 363 */ 364 @Test 365 void testVirtualThreadSubmitFailed() throws Exception { 366 try (Recording recording = new Recording()) { 367 recording.enable("jdk.VirtualThreadSubmitFailed"); 368 369 recording.start(); 370 try (ExecutorService pool = Executors.newCachedThreadPool()) { 371 Executor scheduler = task -> pool.execute(task); 372 373 // create virtual thread that uses custom scheduler 374 ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); 375 376 // start a thread 377 Thread thread = factory.newThread(LockSupport::park); 378 thread.start(); 379 380 // wait for thread to park 381 await(thread, Thread.State.WAITING); 382 383 // shutdown scheduler 384 pool.shutdown(); 385 386 // unpark, the submit should fail 387 try { 388 LockSupport.unpark(thread); 389 fail(); 390 } catch (RejectedExecutionException expected) { } 391 392 // start another thread, it should fail and an event should be recorded 393 try { 394 factory.newThread(LockSupport::park).start(); 395 throw new RuntimeException("RejectedExecutionException expected"); 396 } catch (RejectedExecutionException expected) { } 397 } finally { 398 recording.stop(); 399 } 400 401 List<RecordedEvent> submitFailedEvents = find(recording, "jdk.VirtualThreadSubmitFailed"); 402 System.err.println(submitFailedEvents); 403 assertTrue(submitFailedEvents.size() == 2, "Expected two events"); 404 } 405 } 406 407 /** 408 * Returns the list of events in the given recording with the given name. 409 */ 410 private static List<RecordedEvent> find(Recording recording, String name) throws IOException { 411 Path recordingFile = recordingFile(recording); 412 return RecordingFile.readAllEvents(recordingFile) 413 .stream() 414 .filter(e -> e.getEventType().getName().equals(name)) 415 .toList(); 416 } 417 418 /** 419 * Read the events from the recording and return a map of event name to count. 420 */ 421 private static Map<String, Integer> sumEvents(Recording recording) throws IOException { 422 Path recordingFile = recordingFile(recording); 423 List<RecordedEvent> events = RecordingFile.readAllEvents(recordingFile); 424 return events.stream() 425 .map(RecordedEvent::getEventType) 426 .collect(Collectors.groupingBy(EventType::getName, 427 Collectors.summingInt(x -> 1))); 428 } 429 430 /** 431 * Return the file path to the recording file. 432 */ 433 private static Path recordingFile(Recording recording) throws IOException { 434 Path recordingFile = recording.getDestination(); 435 if (recordingFile == null) { 436 ProcessHandle h = ProcessHandle.current(); 437 recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); 438 recording.dump(recordingFile); 439 } 440 return recordingFile; 441 } 442 443 /** 444 * Assert that a recording contains a jdk.VirtualThreadPinned event on the given thread. 445 */ 446 private void assertContainsPinnedEvent(Recording recording, Thread thread) throws IOException { 447 List<RecordedEvent> pinnedEvents = find(recording, "jdk.VirtualThreadPinned"); 448 assertTrue(pinnedEvents.size() > 0, "No jdk.VirtualThreadPinned events in recording"); 449 System.err.println(pinnedEvents); 450 451 long tid = thread.threadId(); 452 assertTrue(pinnedEvents.stream() 453 .anyMatch(e -> e.getThread().getJavaThreadId() == tid), 454 "jdk.VirtualThreadPinned for javaThreadId = " + tid + " not found"); 455 } 456 457 /** 458 * Waits for the given boolean to be set to true. 459 */ 460 private void awaitTrue(AtomicBoolean b) throws InterruptedException { 461 while (!b.get()) { 462 Thread.sleep(10); 463 } 464 } 465 466 /** 467 * Waits for the given thread to reach a given state. 468 */ 469 private static void await(Thread thread, Thread.State expectedState) throws InterruptedException { 470 Thread.State state = thread.getState(); 471 while (state != expectedState) { 472 Thread.sleep(10); 473 state = thread.getState(); 474 } 475 } 476 }