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.VThreadPinner.ThrowingAction; 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 (ThrowingAction) () -> { 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 (ThrowingAction) () -> { 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 // untimed Object.wait 142 var waitWhenPinned = Arguments.of( 143 "Object.wait", 144 (ThrowingAction) () -> { 145 synchronized (lock) { 146 lock.wait(); 147 } 148 }, 149 Thread.State.WAITING, 150 (Consumer<Thread>) t -> { 151 synchronized (lock) { 152 lock.notifyAll(); 153 } 154 } 155 ); 156 157 // timed Object.wait 158 var timedWaitWhenPinned = Arguments.of( 159 "Object.wait(millis)", 160 (ThrowingAction) () -> { 161 synchronized (lock) { 162 lock.wait(Long.MAX_VALUE); 163 } 164 }, 165 Thread.State.TIMED_WAITING, 166 (Consumer<Thread>) t -> { 167 synchronized (lock) { 168 lock.notifyAll(); 169 } 170 } 171 ); 172 173 return Stream.of(parkWhenPinned, timedParkWhenPinned, waitWhenPinned, timedWaitWhenPinned); 174 } 175 176 /** 177 * Test jdk.VirtualThreadPinned event. 178 */ 179 @ParameterizedTest 180 @MethodSource("pinnedCases") 181 void testVirtualThreadPinned(String label, 182 ThrowingAction<Exception> parker, 183 Thread.State expectedState, 184 Consumer<Thread> unparker) throws Exception { 185 186 try (Recording recording = new Recording()) { 187 recording.enable("jdk.VirtualThreadPinned"); 188 189 recording.start(); 190 try { 191 var exception = new AtomicReference<Throwable>(); 192 var thread = Thread.ofVirtual().start(() -> { 193 try { 194 parker.run(); 195 } catch (Throwable e) { 196 exception.set(e); 197 } 198 }); 199 try { 200 // wait for thread to park/wait 201 Thread.State state = thread.getState(); 202 while (state != expectedState) { 203 assertTrue(state != Thread.State.TERMINATED, thread.toString()); 204 Thread.sleep(10); 205 state = thread.getState(); 206 } 207 } finally { 208 unparker.accept(thread); 209 thread.join(); 210 assertNull(exception.get()); 211 } 212 } finally { 213 recording.stop(); 214 } 215 216 Map<String, Integer> events = sumEvents(recording); 217 System.err.println(events); 218 219 // should have at least one pinned event 220 int pinnedCount = events.getOrDefault("jdk.VirtualThreadPinned", 0); 221 assertTrue(pinnedCount >= 1, "Expected one or more events"); 222 } 223 } 224 225 /** 226 * Test jdk.VirtualThreadSubmitFailed event. 227 */ 228 @Test 229 void testVirtualThreadSubmitFailed() throws Exception { 230 try (Recording recording = new Recording()) { 231 recording.enable("jdk.VirtualThreadSubmitFailed"); 232 233 recording.start(); 234 try (ExecutorService pool = Executors.newCachedThreadPool()) { 235 Executor scheduler = task -> pool.execute(task); 236 237 // create virtual thread that uses custom scheduler 238 ThreadFactory factory = ThreadBuilders.virtualThreadBuilder(scheduler).factory(); 239 240 // start a thread 241 Thread thread = factory.newThread(LockSupport::park); 242 thread.start(); 243 244 // wait for thread to park 245 while (thread.getState() != Thread.State.WAITING) { 246 Thread.sleep(10); 247 } 248 249 // shutdown scheduler 250 pool.shutdown(); 251 252 // unpark, the submit should fail 253 try { 254 LockSupport.unpark(thread); 255 fail(); 256 } catch (RejectedExecutionException expected) { } 257 258 // start another thread, it should fail and an event should be recorded 259 try { 260 factory.newThread(LockSupport::park).start(); 261 throw new RuntimeException("RejectedExecutionException expected"); 262 } catch (RejectedExecutionException expected) { } 263 } finally { 264 recording.stop(); 265 } 266 267 Map<String, Integer> events = sumEvents(recording); 268 System.err.println(events); 269 270 int count = events.getOrDefault("jdk.VirtualThreadSubmitFailed", 0); 271 assertEquals(2, count); 272 } 273 } 274 275 /** 276 * Read the events from the recording and return a map of event name to count. 277 */ 278 private static Map<String, Integer> sumEvents(Recording recording) throws IOException { 279 Path recordingFile = recordingFile(recording); 280 List<RecordedEvent> events = RecordingFile.readAllEvents(recordingFile); 281 return events.stream() 282 .map(RecordedEvent::getEventType) 283 .collect(Collectors.groupingBy(EventType::getName, 284 Collectors.summingInt(x -> 1))); 285 } 286 287 /** 288 * Return the file path to the recording file. 289 */ 290 private static Path recordingFile(Recording recording) throws IOException { 291 Path recordingFile = recording.getDestination(); 292 if (recordingFile == null) { 293 ProcessHandle h = ProcessHandle.current(); 294 recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); 295 recording.dump(recordingFile); 296 } 297 return recordingFile; 298 } 299 }