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 }