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 }