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 }