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