Skip to content

Commit

Permalink
Allow setting JFR stackdepth value from agent (#6335)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbachorik authored Dec 13, 2023
1 parent b0cf14d commit 541bf19
Show file tree
Hide file tree
Showing 19 changed files with 393 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public static void start(final Instrumentation inst, final URL agentJarURL, Stri
// multiple times
// If early profiling is enabled then this call will start profiling.
// If early profiling is disabled then later call will do this.
startProfilingAgent(true);
startProfilingAgent(true, inst);
} else {
log.debug("Oracle JDK 8 detected. Delaying profiler initialization.");
// Profiling can not run early on Oracle JDK 8 because it will cause JFR initialization
Expand All @@ -230,7 +230,7 @@ public static void start(final Instrumentation inst, final URL agentJarURL, Stri
new Runnable() {
@Override
public void run() {
startProfilingAgent(false);
startProfilingAgent(false, inst);
}
};
}
Expand Down Expand Up @@ -309,9 +309,9 @@ public void run() {

if (delayOkHttp) {
log.debug("Custom logger detected. Delaying Profiling initialization.");
registerLogManagerCallback(new StartProfilingAgentCallback());
registerLogManagerCallback(new StartProfilingAgentCallback(inst));
} else {
startProfilingAgent(false);
startProfilingAgent(false, inst);
// only enable instrumentation based profilers when we know JFR is ready
InstrumentationBasedProfiling.enableInstrumentationBasedProfiling();
}
Expand Down Expand Up @@ -480,14 +480,20 @@ public void execute() {
}

protected static class StartProfilingAgentCallback extends ClassLoadCallBack {
private final Instrumentation inst;

protected StartProfilingAgentCallback(Instrumentation inst) {
this.inst = inst;
}

@Override
public AgentThread agentThread() {
return PROFILER_STARTUP;
}

@Override
public void execute() {
startProfilingAgent(false);
startProfilingAgent(false, inst);
// only enable instrumentation based profilers when we know JFR is ready
InstrumentationBasedProfiling.enableInstrumentationBasedProfiling();
}
Expand Down Expand Up @@ -881,7 +887,7 @@ private static ProfilingContextIntegration createProfilingContextIntegration() {
return ProfilingContextIntegration.NoOp.INSTANCE;
}

private static void startProfilingAgent(final boolean isStartingFirst) {
private static void startProfilingAgent(final boolean isStartingFirst, Instrumentation inst) {
StaticEventLogger.begin("ProfilingAgent");

if (isAwsLambdaRuntime()) {
Expand All @@ -895,8 +901,9 @@ private static void startProfilingAgent(final boolean isStartingFirst) {
final Class<?> profilingAgentClass =
AGENT_CLASSLOADER.loadClass("com.datadog.profiling.agent.ProfilingAgent");
final Method profilingInstallerMethod =
profilingAgentClass.getMethod("run", Boolean.TYPE, ClassLoader.class);
profilingInstallerMethod.invoke(null, isStartingFirst, AGENT_CLASSLOADER);
profilingAgentClass.getMethod(
"run", Boolean.TYPE, ClassLoader.class, Instrumentation.class);
profilingInstallerMethod.invoke(null, isStartingFirst, AGENT_CLASSLOADER, inst);
/*
* Install the tracer hooks only when not using 'early start'.
* The 'early start' is happening so early that most of the infrastructure has not been set up yet.
Expand Down
2 changes: 2 additions & 0 deletions dd-java-agent/agent-profiling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ dependencies {
api project(':dd-java-agent:agent-profiling:profiling-auxiliary-ddprof')
api project(':dd-java-agent:agent-profiling:profiling-uploader')
api project(':dd-java-agent:agent-profiling:profiling-controller')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr:implementation')
api project(':dd-java-agent:agent-profiling:profiling-controller-ddprof')
api project(':dd-java-agent:agent-profiling:profiling-controller-openjdk')
api project(':dd-java-agent:agent-profiling:profiling-controller-oracle')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ public void publish() {
datadogProfiler.recordSetting(PERF_EVENTS_PARANOID_KEY, perfEventsParanoid);
datadogProfiler.recordSetting(NATIVE_STACKS_KEY, String.valueOf(hasNativeStacks));
datadogProfiler.recordSetting(JFR_IMPLEMENTATION_KEY, "ddprof");
datadogProfiler.recordSetting(STACK_DEPTH_KEY, String.valueOf(stackDepth));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Set properties before any plugins get loaded
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8

// need access to jdk.jfr package
skipSettingCompilerRelease = true
}

apply from: "$rootDir/gradle/java.gradle"
apply plugin: 'idea'


sourceSets {
"main_java11" {
java.srcDirs "${project.projectDir}/src/main/java11"
}
}

compileMain_java11Java.configure {
setJavaVersion(it, 11)
sourceCompatibility = JavaVersion.VERSION_1_9
targetCompatibility = JavaVersion.VERSION_1_9
}

dependencies {
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr')
main_java11CompileOnly project(':dd-java-agent:agent-profiling:profiling-controller-jfr')

implementation deps.slf4j

testImplementation deps.mockito
testImplementation deps.junit5
testImplementation sourceSets.main_java11.output
}

excludedClassesCoverage += [
'com.datadog.profiling.controller.jfr.JPMSJFRAccess*',
// The tests will be run only on Java 8 so we exclude the coverage check to prevent failures for other Java versions
'com.datadog.profiling.controller.jfr.SimpleJFRAccess*'
]

jar {
from sourceSets.main_java11.output
}

forbiddenApisMain {
failOnMissingClasses = false
}

idea {
module {
jdkName = '11'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.datadog.profiling.controller.jfr;

import datadog.trace.api.Platform;
import java.lang.instrument.Instrumentation;
import jdk.jfr.internal.JVM;

public class SimpleJFRAccess extends JFRAccess {
public static class FactoryImpl implements JFRAccess.Factory {
@Override
public JFRAccess create(Instrumentation inst) {
return !Platform.isJ9() && Platform.isJavaVersion(8) ? new SimpleJFRAccess() : null;
}
}

@Override
public boolean setStackDepth(int depth) {
JVM.getJVM().setStackDepth(depth);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.datadog.profiling.controller.jfr;

import datadog.trace.api.Platform;
import java.lang.instrument.Instrumentation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import jdk.jfr.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JPMSJFRAccess extends JFRAccess {
private static final Logger log = LoggerFactory.getLogger(JPMSJFRAccess.class);

public static final class FactoryImpl implements JFRAccess.Factory {
private static final Logger log = LoggerFactory.getLogger(FactoryImpl.class);

@Override
public JFRAccess create(Instrumentation inst) {
if (!Platform.isJ9() && Platform.isJavaVersionAtLeast(9)) {
try {
return new JPMSJFRAccess(inst);
} catch (Exception e) {
log.debug("Unable to obtain JFR internal access", e);
}
}
return null;
}
}

private final MethodHandle setStackDepthMH;

public JPMSJFRAccess(Instrumentation inst) throws Exception {
patchModuleAccess(inst);

Class<?> jvmClass = JFRAccess.class.getClassLoader().loadClass("jdk.jfr.internal.JVM");
Method m = jvmClass.getMethod("setStackDepth", int.class);
m.setAccessible(true);
MethodHandle mh = MethodHandles.publicLookup().unreflect(m);
if (!Modifier.isStatic(m.getModifiers())) {
// instance method - need to call JVM.getJVM() and bind the instance
Object jvm = jvmClass.getMethod("getJVM").invoke(null);
mh = mh.bindTo(jvm);
}
setStackDepthMH = mh;
}

private static void patchModuleAccess(Instrumentation inst) {
Module unnamedModule = JFRAccess.class.getClassLoader().getUnnamedModule();
Module targetModule = Event.class.getModule();

Map<String, Set<Module>> extraOpens = Map.of("jdk.jfr.internal", Set.of(unnamedModule));

// Redefine the module
inst.redefineModule(
targetModule,
Collections.emptySet(),
extraOpens,
extraOpens,
Collections.emptySet(),
Collections.emptyMap());
}

@Override
public boolean setStackDepth(int depth) {
try {
setStackDepthMH.invokeExact(depth);
return true;
} catch (Throwable throwable) {
log.warn("Unable to set JFR stack depth", throwable);
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
com.datadog.profiling.controller.jfr.SimpleJFRAccess$FactoryImpl
com.datadog.profiling.controller.jfr.JPMSJFRAccess$FactoryImpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.datadog.profiling.controller.jfr;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import datadog.trace.api.Platform;
import org.junit.jupiter.api.Test;

public class JFRAccessTest {
@Test
void testJava8JFRAccess() {
// For Java 9 and above, the JFR access requires instrumentation in order to patch the module
// access
assumeTrue(Platform.isJavaVersion(8) && !Platform.isJ9());

// just do a sanity check that it is possible to instantiate the class and call
// 'setStackDepth()'
SimpleJFRAccess jfrAccess = new SimpleJFRAccess();
assertTrue(jfrAccess.setStackDepth(42));
}

@Test
void testJ9JFRAccess() {
assumeTrue(Platform.isJ9());

// need to run a bogus setup first
JFRAccess.setup(null);
// make sure that an attempt to get the instance returns the NOOP implementation and does not
// throw exceptions
assertEquals(JFRAccess.NOOP, JFRAccess.instance());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.datadog.profiling.controller.jfr;

import java.lang.instrument.Instrumentation;
import java.util.ServiceLoader;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* JFR access support.<br>
* Provides access to the JFR internal API. For Java 9 and newer, the JFR access requires
* instrumentation in order to patch the module access.
*/
public abstract class JFRAccess {
private static final Logger log = LoggerFactory.getLogger(JFRAccess.class);

/** No-op JFR access implementation. */
public static final JFRAccess NOOP =
new JFRAccess() {
@Override
public boolean setStackDepth(int depth) {
return false;
}
};

/**
* Factory for JFR access.<br>
* The factory is expected to return {@code null} if the JFR access is not available. The factory
* is to be registered in {@code
* META-INF/services/com.datadog.profiling.controller.jfr.JFRAccess$Factory} and will be
* discovered using {@link ServiceLoader}.
*/
public interface Factory {
@Nullable
JFRAccess create(@Nullable Instrumentation inst);
}

private static volatile JFRAccess INSTANCE = NOOP;

/**
* Returns the JFR access instance.
*
* @return the JFR access instance or {@link #NOOP} if the JFR access is not available
*/
public static JFRAccess instance() {
return INSTANCE;
}

/**
* Sets up the JFR access.<br>
* The method is expected to be called once, before any other method of this class is called.
*
* @param inst the instrumentation instance, may be {@code null}
*/
public static void setup(@Nullable Instrumentation inst) {
JFRAccess access = NOOP;
if (inst != null) {
for (Factory factory : ServiceLoader.load(Factory.class, JFRAccess.class.getClassLoader())) {
JFRAccess candidate = factory.create(inst);
if (candidate != null) {
access = candidate;
break;
}
}
}
log.debug("JFR access: {}", access.getClass().getName());
INSTANCE = access;
}

/**
* Sets the stack depth for the JFR recordings.<br>
* It needs to be called before the recording is started.
*
* @param depth the stack depth
* @return {@code true} if the stack depth was set successfully, {@code false} if not or it is not
* possible to tell
*/
public abstract boolean setStackDepth(int depth);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ final class JfrProfilerSettings extends ProfilerSettingsSupport {
private static final String EXCEPTION_HISTO_SIZE_LIMIT_KEY = "Exception Histo Size Limit";
private final String jfrImplementation;

private static final class SingletonHolder {
private static final JfrProfilerSettings INSTANCE = new JfrProfilerSettings();
}

public JfrProfilerSettings() {
super(ConfigProvider.getInstance());
this.jfrImplementation =
Expand All @@ -22,6 +26,10 @@ public JfrProfilerSettings() {
: (Platform.isOracleJDK8() ? "oracle" : "openjdk");
}

public static JfrProfilerSettings instance() {
return SingletonHolder.INSTANCE;
}

public void publish() {
if (new ProfilerSettingEvent(null, null, null).isEnabled()) {
new ProfilerSettingEvent(UPLOAD_PERIOD_KEY, String.valueOf(uploadPeriod), "seconds").commit();
Expand Down Expand Up @@ -52,6 +60,9 @@ public void publish() {
new ProfilerSettingEvent(PERF_EVENTS_PARANOID_KEY, perfEventsParanoid).commit();
new ProfilerSettingEvent(NATIVE_STACKS_KEY, String.valueOf(hasNativeStacks)).commit();
new ProfilerSettingEvent(JFR_IMPLEMENTATION_KEY, jfrImplementation).commit();
if (hasJfrStackDepthApplied) {
new ProfilerSettingEvent(STACK_DEPTH_KEY, String.valueOf(stackDepth)).commit();
}
}
}
}
Loading

0 comments on commit 541bf19

Please sign in to comment.