diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/JFRPartialEvaluationTest.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/JFRPartialEvaluationTest.java new file mode 100644 index 000000000000..8a756d951c2f --- /dev/null +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/JFRPartialEvaluationTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.graal.compiler.truffle.test; + +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.test.SubprocessTestUtils; +import com.oracle.truffle.runtime.OptimizedCallTarget; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.java.MethodCallTargetNode; +import jdk.graal.compiler.truffle.test.nodes.AbstractTestNode; +import jdk.graal.compiler.truffle.test.nodes.RootTestNode; +import jdk.jfr.Event; +import jdk.jfr.Name; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertNotNull; + +public class JFRPartialEvaluationTest extends PartialEvaluationTest { + + public static Object constant42() { + return 42; + } + + @Test + public void testException() throws IOException, InterruptedException { + runInSubprocessWithJFREnabled(this::performTestException); + } + + private void performTestException() { + RootTestNode root = new RootTestNode(new FrameDescriptor(), "NewException", new AbstractTestNode() { + @Override + public int execute(VirtualFrame frame) { + try { + throw new TruffleExceptionImpl(42); + } catch (TruffleExceptionImpl e) { + return e.value; + } + } + }); + if (Runtime.version().feature() < 22) { + Class throwableTracer = findThrowableTracerClass(); + ResolvedJavaMethod traceThrowable = getResolvedJavaMethod(throwableTracer, "traceThrowable"); + OptimizedCallTarget callTarget = (OptimizedCallTarget) root.getCallTarget(); + StructuredGraph graph = partialEval(callTarget, new Object[0]); + // The call from the exception constructor to the JFR tracing must not be inlined. + assertNotNull("The call to ThrowableTracer#traceThrowable was not inlined or is missing.", findInvoke(graph, traceThrowable)); + // Also make sure that the node count hasn't exploded. + assertTrue("The number of graal nodes for an exception instantiation exceeded 100.", graph.getNodeCount() < 100); + } else { + // On JDK-22+ JFR exception tracing is unconditionally disabled by PartialEvaluator + assertPartialEvalEquals(JFRPartialEvaluationTest::constant42, root); + } + } + + @SuppressWarnings("serial") + private static final class TruffleExceptionImpl extends AbstractTruffleException { + final int value; + + TruffleExceptionImpl(int value) { + this.value = value; + } + } + + private static Class findThrowableTracerClass() { + try { + return Class.forName("jdk.jfr.internal.instrument.ThrowableTracer"); + } catch (ClassNotFoundException cnf) { + throw new RuntimeException("ThrowableTracer not found", cnf); + } + } + + @Test + public void testError() throws IOException, InterruptedException { + runInSubprocessWithJFREnabled(this::performTestError); + } + + private void performTestError() { + RootTestNode root = new RootTestNode(new FrameDescriptor(), "NewError", new AbstractTestNode() { + @Override + public int execute(VirtualFrame frame) { + try { + throw new ErrorImpl(42); + } catch (ErrorImpl e) { + return e.value; + } + } + }); + if (Runtime.version().feature() < 22) { + Class throwableTracer = findThrowableTracerClass(); + ResolvedJavaMethod traceThrowable = getResolvedJavaMethod(throwableTracer, "traceThrowable"); + ResolvedJavaMethod traceError = getResolvedJavaMethod(throwableTracer, "traceError"); + OptimizedCallTarget callTarget = (OptimizedCallTarget) root.getCallTarget(); + StructuredGraph graph = partialEval(callTarget, new Object[0]); + // The call from the exception constructor to the JFR tracing must not be inlined. + assertNotNull("The call to ThrowableTracer#traceThrowable was not inlined or is missing.", findInvoke(graph, traceThrowable)); + assertNotNull("The call to ThrowableTracer#traceError was not inlined or is missing.", findInvoke(graph, traceError)); + // Also make sure that the node count hasn't exploded. + assertTrue("The number of graal nodes for an exception instantiation exceeded 100.", graph.getNodeCount() < 100); + } else { + // On JDK-22+ JFR exception tracing is unconditionally disabled by PartialEvaluator + assertPartialEvalEquals(JFRPartialEvaluationTest::constant42, root); + } + } + + @SuppressWarnings("serial") + private static final class ErrorImpl extends Error { + + final int value; + + ErrorImpl(int value) { + this.value = value; + } + + @Override + @SuppressWarnings("sync-override") + public Throwable fillInStackTrace() { + return this; + } + } + + @Test + public void testJFREvent() throws IOException, InterruptedException { + runInSubprocessWithJFREnabled(this::performTestJFREvent); + } + + private void performTestJFREvent() { + ResolvedJavaMethod shouldCommit = getResolvedJavaMethod(TestEvent.class, "shouldCommit"); + ResolvedJavaMethod commit = getResolvedJavaMethod(TestEvent.class, "commit"); + RootTestNode root = new RootTestNode(new FrameDescriptor(), "JFREvent", new AbstractTestNode() { + @Override + public int execute(VirtualFrame frame) { + TestEvent event = new TestEvent(); + if (event.shouldCommit()) { + event.commit(); + } + return 1; + } + }); + OptimizedCallTarget callTarget = (OptimizedCallTarget) root.getCallTarget(); + StructuredGraph graph = partialEval(callTarget, new Object[0]); + // Calls to JFR event methods must not be inlined. + assertNotNull("The call to TestEvent#shouldCommit was not inlined or is missing.", findInvoke(graph, shouldCommit)); + assertNotNull("The call to TestEvent#commit was not inlined or is missing.", findInvoke(graph, commit)); + // Also make sure that the node count hasn't exploded. + assertTrue("The number of graal nodes for an exception instantiation exceeded 100.", graph.getNodeCount() < 100); + } + + @Name("test.JFRPartialEvaluationTestEvent") + private static class TestEvent extends Event { + } + + private static MethodCallTargetNode findInvoke(StructuredGraph graph, ResolvedJavaMethod expectedMethod) { + for (MethodCallTargetNode node : graph.getNodes(MethodCallTargetNode.TYPE)) { + ResolvedJavaMethod targetMethod = node.targetMethod(); + if (expectedMethod.equals(targetMethod)) { + return node; + } + } + return null; + } + + private static void runInSubprocessWithJFREnabled(Runnable action) throws IOException, InterruptedException { + Path jfrFile; + if (SubprocessTestUtils.isSubprocess()) { + jfrFile = null; + } else { + jfrFile = Files.createTempFile("new_truffle_exception", ".jfr"); + } + try { + SubprocessTestUtils.newBuilder(JFRPartialEvaluationTest.class, action) // + .prefixVmOption(String.format("-XX:StartFlightRecording=exceptions=all,filename=%s", jfrFile)) // + .onExit((p) -> { + try { + assertTrue(String.format("JFR event file %s is missing", jfrFile), Files.size(jfrFile) > 0); + } catch (IOException ioe) { + throw new AssertionError("Failed to stat JFR event file " + jfrFile, ioe); + } + }) // + .run(); + } finally { + if (jfrFile != null) { + Files.deleteIfExists(jfrFile); + } + } + } +} diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/OWNERS.toml b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/OWNERS.toml new file mode 100644 index 000000000000..3fc574f543a2 --- /dev/null +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/OWNERS.toml @@ -0,0 +1,10 @@ +[[rule]] +files = "*" +any = [ + "christian.humer@oracle.com", + "jakub.chaloupka@oracle.com", + "martin.entlicher@oracle.com", + "tomas.zezula@oracle.com", + "boris.spasojevic@oracle.com", + "matt.dsouza@oracle.com", +] diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java index b099a6f59e1b..e9871042ea63 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java @@ -40,8 +40,6 @@ */ package com.oracle.truffle.runtime.debug; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -80,7 +78,6 @@ public final class JFRListener extends AbstractGraalTruffleRuntimeListener { // Support for JFRListener#isInstrumented private static final Set instrumentedMethodPatterns = createInstrumentedPatterns(); private static final AtomicReference instrumentedFilterState = new AtomicReference<>(InstrumentedFilterState.NEW); - private static volatile Class requiredAnnotation; private static volatile ResolvedJavaType resolvedJfrEventClass; private final ThreadLocal currentCompilation = new ThreadLocal<>(); @@ -288,7 +285,20 @@ private static boolean isInstrumentedImpl(ResolvedJavaMethod method, Instrumente return false; } - if ("traceThrowable".equals(method.getName()) && "Ljdk/jfr/internal/instrument/ThrowableTracer;".equals(method.getDeclaringClass().getName())) { + /* + * Between JDK-11 and JDK-21, JFR utilizes instrumentation to inject calls to + * jdk.jfr.internal.instrument.ThrowableTracer into constructors of Throwable and Error. + * These calls must never be inlined. However, in JDK-22, + * jdk.jfr.internal.instrument.ThrowableTracer was renamed to + * jdk.internal.event.ThrowableTracer, and the calls are no longer injected via + * instrumentation. Instead, a volatile field, Throwable#jfrTracing, is used. This field + * necessitates a volatile read. To prevent this in a PE code, PartialEvaluator modifies the + * reading of jfrTracing to a compilation constant `false`, effectively removing the JFR + * code during bytecode parsing. See PartialEvaluator#appendJFRTracingPlugin. This should + * not pose an issue because general-purpose exceptions are typically created after the + * TruffleBoundary. + */ + if (("traceThrowable".equals(method.getName()) || "traceError".equals(method.getName())) && "Ljdk/jfr/internal/instrument/ThrowableTracer;".equals(method.getDeclaringClass().getName())) { return true; } @@ -297,14 +307,11 @@ private static boolean isInstrumentedImpl(ResolvedJavaMethod method, Instrumente return false; } - ResolvedJavaType methodOwner = method.getDeclaringClass(); - if (getAnnotation(requiredAnnotation, methodOwner) == null) { - return false; - } - if (!instrumentedMethodPatterns.contains(new InstrumentedMethodPattern(method))) { return false; } + + ResolvedJavaType methodOwner = method.getDeclaringClass(); ResolvedJavaType patternOwner = getJFREventClass(methodOwner); return patternOwner != null && patternOwner.isAssignableFrom(methodOwner); } @@ -313,7 +320,6 @@ private static InstrumentedFilterState initializeInstrumentedFilter() { // Do not initialize during image building. if (!ImageInfo.inImageBuildtimeCode()) { if (factory != null) { - requiredAnnotation = factory.getRequiredAnnotation(); factory.addInitializationListener(() -> { instrumentedFilterState.set(InstrumentedFilterState.ACTIVE); }); @@ -329,7 +335,7 @@ private static InstrumentedFilterState initializeInstrumentedFilter() { private static ResolvedJavaType getJFREventClass(ResolvedJavaType accessingClass) { if (resolvedJfrEventClass == null) { try { - resolvedJfrEventClass = UnresolvedJavaType.create("Ljdk/jfr/Event;").resolve(accessingClass); + resolvedJfrEventClass = UnresolvedJavaType.create("Ljdk/internal/event/Event;").resolve(accessingClass); } catch (LinkageError e) { // May happen when declaringClass is not accessible from accessingClass } @@ -337,14 +343,6 @@ private static ResolvedJavaType getJFREventClass(ResolvedJavaType accessingClass return resolvedJfrEventClass; } - private static T getAnnotation(Class annotationClass, AnnotatedElement element) { - try { - return annotationClass.cast(element.getAnnotation(annotationClass)); - } catch (NoClassDefFoundError e) { - return null; - } - } - private static Set createInstrumentedPatterns() { Set patterns = new HashSet<>(); patterns.add(new InstrumentedMethodPattern("begin", "()V"));