1 /* 2 * Copyright (c) 2021, 2023, 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.atomic.AtomicReference; 44 import java.util.concurrent.locks.LockSupport; 45 import java.util.function.Consumer; 46 import java.util.stream.Collectors; 47 import java.util.stream.Stream; 48 49 import jdk.jfr.EventType; 50 import jdk.jfr.Recording; 51 import jdk.jfr.consumer.RecordedEvent; 52 import jdk.jfr.consumer.RecordingFile; 53 54 import jdk.test.lib.thread.VThreadPinner; 55 import jdk.test.lib.thread.VThreadRunner.ThrowingRunnable; 56 import org.junit.jupiter.api.Test; 57 import org.junit.jupiter.params.ParameterizedTest; 58 import org.junit.jupiter.params.provider.Arguments; 59 import org.junit.jupiter.params.provider.MethodSource; 60 import static org.junit.jupiter.api.Assertions.*; 61 62 class JfrEvents { 63 64 /** 65 * Test jdk.VirtualThreadStart and jdk.VirtualThreadEnd events. 66 */ 67 @Test 68 void testVirtualThreadStartAndEnd() throws Exception { 69 try (Recording recording = new Recording()) { 70 recording.enable("jdk.VirtualThreadStart"); 71 recording.enable("jdk.VirtualThreadEnd"); 72 73 // execute 100 tasks, each in their own virtual thread 74 recording.start(); 75 ThreadFactory factory = Thread.ofVirtual().factory(); 76 try (var executor = Executors.newThreadPerTaskExecutor(factory)) { 77 for (int i = 0; i < 100; i++) { 78 executor.submit(() -> { }); 79 } 80 Thread.sleep(1000); // give time for thread end events to be recorded 81 } finally { 82 recording.stop(); 83 } 84 85 Map<String, Integer> events = sumEvents(recording); 86 System.err.println(events); 87 88 int startCount = events.getOrDefault("jdk.VirtualThreadStart", 0); 89 int endCount = events.getOrDefault("jdk.VirtualThreadEnd", 0); 90 assertEquals(100, startCount); 91 assertEquals(100, endCount); 92 } 93 } 94 95 /** 96 * Arguments for testVirtualThreadPinned to test jdk.VirtualThreadPinned event. 97 * [0] label/description 98 * [1] the operation to park/wait 99 * [2] the Thread.State when parked/waiting 100 * [3] the action to unpark/notify the thread 101 */ 102 static Stream<Arguments> pinnedCases() { 103 Object lock = new Object(); 104 105 // park with native frame on stack 106 var finish1 = new AtomicBoolean(); 107 var parkWhenPinned = Arguments.of( 108 "LockSupport.park when pinned", 109 (ThrowingRunnable<Exception>) () -> { 110 VThreadPinner.runPinned(() -> { 111 while (!finish1.get()) { 112 LockSupport.park(); 113 } 114 }); 115 }, 116 Thread.State.WAITING, 117 (Consumer<Thread>) t -> { 118 finish1.set(true); 119 LockSupport.unpark(t); 120 } 121 ); 122 123 // timed park with native frame on stack 124 var finish2 = new AtomicBoolean(); 125 var timedParkWhenPinned = Arguments.of( 126 "LockSupport.parkNanos when pinned", 127 (ThrowingRunnable<Exception>) () -> { 128 VThreadPinner.runPinned(() -> { 129 while (!finish2.get()) { 130 LockSupport.parkNanos(Long.MAX_VALUE); 131 } 132 }); 133 }, 134 Thread.State.TIMED_WAITING, 135 (Consumer<Thread>) t -> { 136 finish2.set(true); 137 LockSupport.unpark(t); 138 } 139 ); 140 141 return Stream.of(parkWhenPinned, timedParkWhenPinned); 142 } 143 144 /** 145 * Test jdk.VirtualThreadPinned event. 146 */ 147 @ParameterizedTest 148 @MethodSource("pinnedCases") 149 void testVirtualThreadPinned(String label, 150 ThrowingRunnable<Exception> parker, 151 Thread.State expectedState, 152 Consumer<Thread> unparker) throws Exception { 153 154 try (Recording recording = new Recording()) { 155 recording.enable("jdk.VirtualThreadPinned"); 156 157 recording.start(); 158 try { 159 var exception = new AtomicReference<Throwable>(); 160 var thread = Thread.ofVirtual().start(() -> { 161 try { 162 parker.run(); 163 } catch (Throwable e) { 164 exception.set(e); 165 } 166 }); 167 try { 168 // wait for thread to park/wait 169 Thread.State state = thread.getState(); 170 while (state != expectedState) { 171 assertTrue(state != Thread.State.TERMINATED, thread.toString()); 172 Thread.sleep(10); 173 state = thread.getState(); 174 } 175 } finally { 176 unparker.accept(thread); 177 thread.join(); 178 assertNull(exception.get()); 179 } 180 } finally { 181 recording.stop(); 182 } 183 184 Map<String, Integer> events = sumEvents(recording); 185 System.err.println(events); 186 187 // should have at least one pinned event 188 int pinnedCount = events.getOrDefault("jdk.VirtualThreadPinned", 0); 189 assertTrue(pinnedCount >= 1, "Expected one or more events"); 190 } 191 } 192 193 /** 194 * Test jdk.VirtualThreadSubmitFailed event. 195 */ 196 @Test 197 void testVirtualThreadSubmitFailed() throws Exception { 198 try (Recording recording = new Recording()) { 199 recording.enable("jdk.VirtualThreadSubmitFailed"); 200 201 recording.start(); 202 try (ExecutorService pool = Executors.newCachedThreadPool()) { 203 Executor scheduler = task -> pool.execute(task); 204 205 // create virtual thread that uses custom scheduler 206 ThreadFactory factory = ThreadBuilders.virtualThreadBuilder(scheduler).factory(); 207 208 // start a thread 209 Thread thread = factory.newThread(LockSupport::park); 210 thread.start(); 211 212 // wait for thread to park 213 while (thread.getState() != Thread.State.WAITING) { 214 Thread.sleep(10); 215 } 216 217 // shutdown scheduler 218 pool.shutdown(); 219 220 // unpark, the submit should fail 221 try { 222 LockSupport.unpark(thread); 223 fail(); 224 } catch (RejectedExecutionException expected) { } 225 226 // start another thread, it should fail and an event should be recorded 227 try { 228 factory.newThread(LockSupport::park).start(); 229 throw new RuntimeException("RejectedExecutionException expected"); 230 } catch (RejectedExecutionException expected) { } 231 } finally { 232 recording.stop(); 233 } 234 235 Map<String, Integer> events = sumEvents(recording); 236 System.err.println(events); 237 238 int count = events.getOrDefault("jdk.VirtualThreadSubmitFailed", 0); 239 assertEquals(2, count); 240 } 241 } 242 243 /** 244 * Read the events from the recording and return a map of event name to count. 245 */ 246 private static Map<String, Integer> sumEvents(Recording recording) throws IOException { 247 Path recordingFile = recordingFile(recording); 248 List<RecordedEvent> events = RecordingFile.readAllEvents(recordingFile); 249 return events.stream() 250 .map(RecordedEvent::getEventType) 251 .collect(Collectors.groupingBy(EventType::getName, 252 Collectors.summingInt(x -> 1))); 253 } 254 255 /** 256 * Return the file path to the recording file. 257 */ 258 private static Path recordingFile(Recording recording) throws IOException { 259 Path recordingFile = recording.getDestination(); 260 if (recordingFile == null) { 261 ProcessHandle h = ProcessHandle.current(); 262 recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); 263 recording.dump(recordingFile); 264 } 265 return recordingFile; 266 } 267 }