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.Stream; 46 47 import jdk.jfr.EventType; 48 import jdk.jfr.Recording; 49 import jdk.jfr.consumer.RecordedEvent; 50 import jdk.jfr.consumer.RecordingFile; 51 52 import jdk.test.lib.thread.VThreadPinner; 53 import jdk.test.lib.thread.VThreadRunner; // ensureParallelism requires jdk.management 54 import jdk.test.lib.thread.VThreadScheduler; 55 import org.junit.jupiter.api.Test; 56 import org.junit.jupiter.api.BeforeAll; 57 import org.junit.jupiter.params.ParameterizedTest; 58 import org.junit.jupiter.params.provider.ValueSource; 59 import static org.junit.jupiter.api.Assertions.*; 60 61 class JfrEvents { 62 63 @BeforeAll 64 static void setup() { 65 // need at least two carriers to test pinning 66 VThreadRunner.ensureParallelism(2); 67 } 68 69 /** 70 * Test jdk.VirtualThreadStart and jdk.VirtualThreadEnd events. 71 */ 72 @Test 73 void testVirtualThreadStartAndEnd() throws Exception { 74 try (Recording recording = new Recording()) { 75 recording.enable("jdk.VirtualThreadStart"); 76 recording.enable("jdk.VirtualThreadEnd"); 77 78 // execute 100 tasks, each in their own virtual thread 79 recording.start(); 80 ThreadFactory factory = Thread.ofVirtual().factory(); 81 try (var executor = Executors.newThreadPerTaskExecutor(factory)) { 82 for (int i = 0; i < 100; i++) { 83 executor.submit(() -> { }); 84 } 85 Thread.sleep(1000); // give time for thread end events to be recorded 86 } finally { 87 recording.stop(); 88 } 89 90 Map<String, Integer> events = sumEvents(recording); 91 System.err.println(events); 92 93 int startCount = events.getOrDefault("jdk.VirtualThreadStart", 0); 94 int endCount = events.getOrDefault("jdk.VirtualThreadEnd", 0); 95 assertEquals(100, startCount); 96 assertEquals(100, endCount); 97 } 98 } 99 100 /** 101 * Test jdk.VirtualThreadPinned event when parking while pinned. 102 */ 103 @ParameterizedTest 104 @ValueSource(booleans = { true, false }) 105 void testParkWhenPinned(boolean timed) throws Exception { 106 try (Recording recording = new Recording()) { 107 recording.enable("jdk.VirtualThreadPinned"); 108 recording.start(); 109 110 var started = new AtomicBoolean(); 111 var done = new AtomicBoolean(); 112 var vthread = Thread.startVirtualThread(() -> { 113 VThreadPinner.runPinned(() -> { 114 started.set(true); 115 while (!done.get()) { 116 if (timed) { 117 LockSupport.parkNanos(Long.MAX_VALUE); 118 } else { 119 LockSupport.park(); 120 } 121 } 122 }); 123 }); 124 125 try { 126 // wait for thread to start and park 127 awaitTrue(started); 128 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 129 } finally { 130 done.set(true); 131 LockSupport.unpark(vthread); 132 vthread.join(); 133 recording.stop(); 134 } 135 136 assertContainsPinnedEvent(recording, vthread); 137 } 138 } 139 140 /** 141 * Test jdk.VirtualThreadPinned event when blocking on monitor while pinned. 142 */ 143 @Test 144 void testBlockWhenPinned() throws Exception { 145 try (Recording recording = new Recording()) { 146 recording.enable("jdk.VirtualThreadPinned"); 147 recording.start(); 148 149 Object lock = new Object(); 150 151 var started = new AtomicBoolean(); 152 var vthread = Thread.ofVirtual().unstarted(() -> { 153 VThreadPinner.runPinned(() -> { 154 started.set(true); 155 synchronized (lock) { } 156 }); 157 }); 158 159 try { 160 synchronized (lock) { 161 vthread.start(); 162 // wait for thread to start and block 163 awaitTrue(started); 164 await(vthread, Thread.State.BLOCKED); 165 } 166 } finally { 167 vthread.join(); 168 recording.stop(); 169 } 170 171 assertContainsPinnedEvent(recording, vthread); 172 } 173 } 174 175 /** 176 * Test jdk.VirtualThreadPinned event when waiting with Object.wait while pinned. 177 */ 178 @ParameterizedTest 179 @ValueSource(booleans = { true, false }) 180 void testObjectWaitWhenPinned(boolean timed) throws Exception { 181 try (Recording recording = new Recording()) { 182 recording.enable("jdk.VirtualThreadPinned"); 183 recording.start(); 184 185 Object lock = new Object(); 186 187 var started = new AtomicBoolean(); 188 var vthread = Thread.startVirtualThread(() -> { 189 VThreadPinner.runPinned(() -> { 190 started.set(true); 191 synchronized (lock) { 192 try { 193 if (timed) { 194 lock.wait(Long.MAX_VALUE); 195 } else { 196 lock.wait(); 197 } 198 } catch (InterruptedException e) { 199 fail(); 200 } 201 } 202 }); 203 }); 204 205 try { 206 // wait for thread to start and wait 207 awaitTrue(started); 208 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 209 } finally { 210 synchronized (lock) { 211 lock.notifyAll(); 212 } 213 vthread.join(); 214 recording.stop(); 215 } 216 217 assertContainsPinnedEvent(recording, vthread); 218 } 219 } 220 221 /** 222 * Test jdk.VirtualThreadPinned event when parking in a class initializer. 223 */ 224 @Test 225 void testParkInClassInitializer() throws Exception { 226 class TestClass { 227 static { 228 LockSupport.park(); 229 } 230 static void m() { 231 // do nothing 232 } 233 } 234 235 try (Recording recording = new Recording()) { 236 recording.enable("jdk.VirtualThreadPinned"); 237 recording.start(); 238 239 var started = new AtomicBoolean(); 240 Thread vthread = Thread.startVirtualThread(() -> { 241 started.set(true); 242 TestClass.m(); 243 }); 244 245 try { 246 // wait for it to start and park 247 awaitTrue(started); 248 await(vthread, Thread.State.WAITING); 249 } finally { 250 LockSupport.unpark(vthread); 251 vthread.join(); 252 recording.stop(); 253 } 254 255 assertContainsPinnedEvent(recording, vthread); 256 } 257 } 258 259 /** 260 * Test jdk.VirtualThreadPinned event when blocking on monitor in a class initializer. 261 */ 262 @Test 263 void testBlockInClassInitializer() throws Exception { 264 class LockHolder { 265 static final Object lock = new Object(); 266 } 267 class TestClass { 268 static { 269 synchronized (LockHolder.lock) { } 270 } 271 static void m() { 272 // no nothing 273 } 274 } 275 276 try (Recording recording = new Recording()) { 277 recording.enable("jdk.VirtualThreadPinned"); 278 recording.start(); 279 280 var started = new AtomicBoolean(); 281 Thread vthread = Thread.ofVirtual().unstarted(() -> { 282 started.set(true); 283 TestClass.m(); 284 }); 285 286 try { 287 synchronized (LockHolder.lock) { 288 vthread.start(); 289 // wait for thread to start and block 290 awaitTrue(started); 291 await(vthread, Thread.State.BLOCKED); 292 } 293 } finally { 294 vthread.join(); 295 recording.stop(); 296 } 297 298 assertContainsPinnedEvent(recording, vthread); 299 } 300 } 301 302 /** 303 * Test jdk.VirtualThreadPinned event when waiting for a class initializer while pinned. 304 */ 305 @Test 306 void testWaitingForClassInitializerWhenPinned() throws Exception { 307 class TestClass { 308 static { 309 LockSupport.park(); 310 } 311 static void m() { 312 // do nothing 313 } 314 } 315 316 try (Recording recording = new Recording()) { 317 recording.enable("jdk.VirtualThreadPinned"); 318 recording.start(); 319 320 var started1 = new AtomicBoolean(); 321 var started2 = new AtomicBoolean(); 322 323 Thread vthread1 = Thread.ofVirtual().unstarted(() -> { 324 started1.set(true); 325 TestClass.m(); 326 }); 327 Thread vthread2 = Thread.ofVirtual().unstarted(() -> { 328 started2.set(true); 329 VThreadPinner.runPinned(() -> { 330 TestClass.m(); 331 }); 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 in VM 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 }