Skip to content

Commit

Permalink
[GR-52113] PartialEvaluation test for implicit truffle boundaries on …
Browse files Browse the repository at this point in the history
…JFR methods.

PullRequest: graal/16816
  • Loading branch information
tzezula committed Feb 16, 2024
2 parents c2b403a + 6279770 commit 6b2a631
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,7 +78,6 @@ public final class JFRListener extends AbstractGraalTruffleRuntimeListener {
// Support for JFRListener#isInstrumented
private static final Set<InstrumentedMethodPattern> instrumentedMethodPatterns = createInstrumentedPatterns();
private static final AtomicReference<InstrumentedFilterState> instrumentedFilterState = new AtomicReference<>(InstrumentedFilterState.NEW);
private static volatile Class<? extends Annotation> requiredAnnotation;
private static volatile ResolvedJavaType resolvedJfrEventClass;

private final ThreadLocal<CompilationData> currentCompilation = new ThreadLocal<>();
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand All @@ -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);
});
Expand All @@ -329,22 +335,14 @@ 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
}
}
return resolvedJfrEventClass;
}

private static <T extends Annotation> T getAnnotation(Class<T> annotationClass, AnnotatedElement element) {
try {
return annotationClass.cast(element.getAnnotation(annotationClass));
} catch (NoClassDefFoundError e) {
return null;
}
}

private static Set<InstrumentedMethodPattern> createInstrumentedPatterns() {
Set<InstrumentedMethodPattern> patterns = new HashSet<>();
patterns.add(new InstrumentedMethodPattern("begin", "()V"));
Expand Down

0 comments on commit 6b2a631

Please sign in to comment.