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