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 }