1 /* 2 * Copyright (c) 2021, 2024, 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 28 * @modules jdk.jfr java.base/java.lang:+open 29 * @library /test/lib 30 * @run junit/othervm --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; 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 int minParallelism = 2; 66 if (Thread.currentThread().isVirtual()) { 67 minParallelism++; 68 } 69 VThreadRunner.ensureParallelism(minParallelism); 70 } 71 72 /** 73 * Test jdk.VirtualThreadStart and jdk.VirtualThreadEnd events. 74 */ 75 @Test 76 void testVirtualThreadStartAndEnd() throws Exception { 77 try (Recording recording = new Recording()) { 78 recording.enable("jdk.VirtualThreadStart"); 79 recording.enable("jdk.VirtualThreadEnd"); 80 81 // execute 100 tasks, each in their own virtual thread 82 recording.start(); 83 ThreadFactory factory = Thread.ofVirtual().factory(); 84 try (var executor = Executors.newThreadPerTaskExecutor(factory)) { 85 for (int i = 0; i < 100; i++) { 86 executor.submit(() -> { }); 87 } 88 Thread.sleep(1000); // give time for thread end events to be recorded 89 } finally { 90 recording.stop(); 91 } 92 93 Map<String, Integer> events = sumEvents(recording); 94 System.err.println(events); 95 96 int startCount = events.getOrDefault("jdk.VirtualThreadStart", 0); 97 int endCount = events.getOrDefault("jdk.VirtualThreadEnd", 0); 98 assertEquals(100, startCount); 99 assertEquals(100, endCount); 100 } 101 } 102 103 /** 104 * Test jdk.VirtualThreadPinned event when parking while pinned. 105 */ 106 @ParameterizedTest 107 @ValueSource(booleans = { true, false }) 108 void testParkWhenPinned(boolean timed) throws Exception { 109 try (Recording recording = new Recording()) { 110 recording.enable("jdk.VirtualThreadPinned"); 111 recording.start(); 112 113 var started = new AtomicBoolean(); 114 var done = new AtomicBoolean(); 115 var vthread = Thread.startVirtualThread(() -> { 116 VThreadPinner.runPinned(() -> { 117 started.set(true); 118 while (!done.get()) { 119 if (timed) { 120 LockSupport.parkNanos(Long.MAX_VALUE); 121 } else { 122 LockSupport.park(); 123 } 124 } 125 }); 126 }); 127 128 try { 129 // wait for thread to start and park 130 awaitTrue(started); 131 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 132 } finally { 133 done.set(true); 134 LockSupport.unpark(vthread); 135 vthread.join(); 136 recording.stop(); 137 } 138 139 assertContainsPinnedEvent(recording, vthread); 140 } 141 } 142 143 /** 144 * Test jdk.VirtualThreadPinned event when blocking on monitor while pinned. 145 */ 146 @Test 147 void testBlockWhenPinned() throws Exception { 148 try (Recording recording = new Recording()) { 149 recording.enable("jdk.VirtualThreadPinned"); 150 recording.start(); 151 152 Object lock = new Object(); 153 154 var started = new AtomicBoolean(); 155 var vthread = Thread.ofVirtual().unstarted(() -> { 156 VThreadPinner.runPinned(() -> { 157 started.set(true); 158 synchronized (lock) { } 159 }); 160 }); 161 162 try { 163 synchronized (lock) { 164 vthread.start(); 165 // wait for thread to start and block 166 awaitTrue(started); 167 await(vthread, Thread.State.BLOCKED); 168 } 169 } finally { 170 vthread.join(); 171 recording.stop(); 172 } 173 174 assertContainsPinnedEvent(recording, vthread); 175 } 176 } 177 178 /** 179 * Test jdk.VirtualThreadPinned event when waiting with Object.wait while pinned. 180 */ 181 @ParameterizedTest 182 @ValueSource(booleans = { true, false }) 183 void testObjectWaitWhenPinned(boolean timed) throws Exception { 184 try (Recording recording = new Recording()) { 185 recording.enable("jdk.VirtualThreadPinned"); 186 recording.start(); 187 188 Object lock = new Object(); 189 190 var started = new AtomicBoolean(); 191 var vthread = Thread.startVirtualThread(() -> { 192 VThreadPinner.runPinned(() -> { 193 started.set(true); 194 synchronized (lock) { 195 try { 196 if (timed) { 197 lock.wait(Long.MAX_VALUE); 198 } else { 199 lock.wait(); 200 } 201 } catch (InterruptedException e) { 202 fail(); 203 } 204 } 205 }); 206 }); 207 208 try { 209 // wait for thread to start and wait 210 awaitTrue(started); 211 await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING); 212 } finally { 213 synchronized (lock) { 214 lock.notifyAll(); 215 } 216 vthread.join(); 217 recording.stop(); 218 } 219 220 assertContainsPinnedEvent(recording, vthread); 221 } 222 } 223 224 /** 225 * Test jdk.VirtualThreadPinned event when parking in a class initializer. 226 */ 227 @Test 228 void testParkInClassInitializer() throws Exception { 229 class TestClass { 230 static { 231 LockSupport.park(); 232 } 233 static void m() { 234 // do nothing 235 } 236 } 237 238 try (Recording recording = new Recording()) { 239 recording.enable("jdk.VirtualThreadPinned"); 240 recording.start(); 241 242 var started = new AtomicBoolean(); 243 Thread vthread = Thread.startVirtualThread(() -> { 244 started.set(true); 245 TestClass.m(); 246 }); 247 248 try { 249 // wait for it to start and park 250 awaitTrue(started); 251 await(vthread, Thread.State.WAITING); 252 } finally { 253 LockSupport.unpark(vthread); 254 vthread.join(); 255 recording.stop(); 256 } 257 258 assertContainsPinnedEvent(recording, vthread); 259 } 260 } 261 262 /** 263 * Test jdk.VirtualThreadPinned event when blocking on monitor in a class initializer. 264 */ 265 @Test 266 void testBlockInClassInitializer() throws Exception { 267 class LockHolder { 268 static final Object lock = new Object(); 269 } 270 class TestClass { 271 static { 272 synchronized (LockHolder.lock) { } 273 } 274 static void m() { 275 // no nothing 276 } 277 } 278 279 try (Recording recording = new Recording()) { 280 recording.enable("jdk.VirtualThreadPinned"); 281 recording.start(); 282 283 var started = new AtomicBoolean(); 284 Thread vthread = Thread.ofVirtual().unstarted(() -> { 285 started.set(true); 286 TestClass.m(); 287 }); 288 289 try { 290 synchronized (LockHolder.lock) { 291 vthread.start(); 292 // wait for thread to start and block 293 awaitTrue(started); 294 await(vthread, Thread.State.BLOCKED); 295 } 296 } finally { 297 vthread.join(); 298 recording.stop(); 299 } 300 301 assertContainsPinnedEvent(recording, vthread); 302 } 303 } 304 305 /** 306 * Test jdk.VirtualThreadPinned event when waiting for a class initializer. 307 */ 308 @Test 309 void testWaitingForClassInitializer() throws Exception { 310 class TestClass { 311 static { 312 LockSupport.park(); 313 } 314 static void m() { 315 // do nothing 316 } 317 } 318 319 try (Recording recording = new Recording()) { 320 recording.enable("jdk.VirtualThreadPinned"); 321 recording.start(); 322 323 var started1 = new AtomicBoolean(); 324 var started2 = new AtomicBoolean(); 325 326 Thread vthread1 = Thread.ofVirtual().unstarted(() -> { 327 started1.set(true); 328 TestClass.m(); 329 }); 330 Thread vthread2 = Thread.ofVirtual().unstarted(() -> { 331 started2.set(true); 332 TestClass.m(); 333 }); 334 335 try { 336 // start first virtual thread and wait for it to start + park 337 vthread1.start(); 338 awaitTrue(started1); 339 await(vthread1, Thread.State.WAITING); 340 341 // start second virtual thread and wait for it to start 342 vthread2.start(); 343 awaitTrue(started2); 344 345 // give time for second virtual thread to wait on the MutexLocker 346 Thread.sleep(3000); 347 348 } finally { 349 LockSupport.unpark(vthread1); 350 vthread1.join(); 351 vthread2.join(); 352 recording.stop(); 353 } 354 355 // the recording should have a pinned event for vthread2 356 assertContainsPinnedEvent(recording, vthread2); 357 } 358 } 359 360 /** 361 * Test jdk.VirtualThreadSubmitFailed event. 362 */ 363 @Test 364 void testVirtualThreadSubmitFailed() throws Exception { 365 try (Recording recording = new Recording()) { 366 recording.enable("jdk.VirtualThreadSubmitFailed"); 367 368 recording.start(); 369 try (ExecutorService pool = Executors.newCachedThreadPool()) { 370 Executor scheduler = task -> pool.execute(task); 371 372 // create virtual thread that uses custom scheduler 373 ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); 374 375 // start a thread 376 Thread thread = factory.newThread(LockSupport::park); 377 thread.start(); 378 379 // wait for thread to park 380 await(thread, Thread.State.WAITING); 381 382 // shutdown scheduler 383 pool.shutdown(); 384 385 // unpark, the submit should fail 386 try { 387 LockSupport.unpark(thread); 388 fail(); 389 } catch (RejectedExecutionException expected) { } 390 391 // start another thread, it should fail and an event should be recorded 392 try { 393 factory.newThread(LockSupport::park).start(); 394 throw new RuntimeException("RejectedExecutionException expected"); 395 } catch (RejectedExecutionException expected) { } 396 } finally { 397 recording.stop(); 398 } 399 400 List<RecordedEvent> submitFailedEvents = find(recording, "jdk.VirtualThreadSubmitFailed"); 401 System.err.println(submitFailedEvents); 402 assertTrue(submitFailedEvents.size() == 2, "Expected two events"); 403 } 404 } 405 406 /** 407 * Returns the list of events in the given recording with the given name. 408 */ 409 private static List<RecordedEvent> find(Recording recording, String name) throws IOException { 410 Path recordingFile = recordingFile(recording); 411 return RecordingFile.readAllEvents(recordingFile) 412 .stream() 413 .filter(e -> e.getEventType().getName().equals(name)) 414 .toList(); 415 } 416 417 /** 418 * Read the events from the recording and return a map of event name to count. 419 */ 420 private static Map<String, Integer> sumEvents(Recording recording) throws IOException { 421 Path recordingFile = recordingFile(recording); 422 List<RecordedEvent> events = RecordingFile.readAllEvents(recordingFile); 423 return events.stream() 424 .map(RecordedEvent::getEventType) 425 .collect(Collectors.groupingBy(EventType::getName, 426 Collectors.summingInt(x -> 1))); 427 } 428 429 /** 430 * Return the file path to the recording file. 431 */ 432 private static Path recordingFile(Recording recording) throws IOException { 433 Path recordingFile = recording.getDestination(); 434 if (recordingFile == null) { 435 ProcessHandle h = ProcessHandle.current(); 436 recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); 437 recording.dump(recordingFile); 438 } 439 return recordingFile; 440 } 441 442 /** 443 * Assert that a recording contains a jdk.VirtualThreadPinned event on the given thread. 444 */ 445 private void assertContainsPinnedEvent(Recording recording, Thread thread) throws IOException { 446 List<RecordedEvent> pinnedEvents = find(recording, "jdk.VirtualThreadPinned"); 447 assertTrue(pinnedEvents.size() > 0, "No jdk.VirtualThreadPinned events in recording"); 448 System.err.println(pinnedEvents); 449 450 long tid = thread.threadId(); 451 assertTrue(pinnedEvents.stream() 452 .anyMatch(e -> e.getThread().getJavaThreadId() == tid), 453 "jdk.VirtualThreadPinned for javaThreadId = " + tid + " not found"); 454 } 455 456 /** 457 * Waits for the given boolean to be set to true. 458 */ 459 private void awaitTrue(AtomicBoolean b) throws InterruptedException { 460 while (!b.get()) { 461 Thread.sleep(10); 462 } 463 } 464 465 /** 466 * Waits for the given thread to reach a given state. 467 */ 468 private static void await(Thread thread, Thread.State expectedState) throws InterruptedException { 469 Thread.State state = thread.getState(); 470 while (state != expectedState) { 471 Thread.sleep(10); 472 state = thread.getState(); 473 } 474 } 475 }