diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java index c1305763464..6360f809ac2 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java @@ -9,6 +9,7 @@ import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getLogLevel; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getNativeMemoryInterval; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getNativeSocketInterval; +import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getProfilerOptions; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getSafeMode; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getSchedulingEvent; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getSchedulingEventInterval; @@ -37,6 +38,7 @@ import static datadog.trace.api.config.ProfilingConfig.PROFILING_DETAILED_DEBUG_LOGGING_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_QUEUEING_TIME_THRESHOLD_MILLIS; import static datadog.trace.api.config.ProfilingConfig.PROFILING_QUEUEING_TIME_THRESHOLD_MILLIS_DEFAULT; +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import com.datadog.profiling.controller.OngoingRecording; import com.datadog.profiling.utils.ProfilingMode; @@ -56,7 +58,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.LockSupport; @@ -77,9 +82,15 @@ public final class DatadogProfiler { private static final String OPERATION = "_dd.trace.operation"; private static final String RESOURCE = "_dd.trace.resource"; + static final String PROFILER_OPTIONS_WARNING = + "The profiling.experimental.ddprof.options configuration directly modifies Datadog " + + "profiler options, overrides conflicting derived options, and may cause unexpected " + + "behavior."; + private static final int MAX_NUM_ENDPOINTS = 8192; private final boolean detailedDebugLogging; + private final String profilerOptions; /** * Creates a profiler API with default configuration, may result in loading the profiler native @@ -109,6 +120,7 @@ public static DatadogProfiler newInstance(ConfigProvider configProvider) { private final AtomicBoolean recordingFlag = new AtomicBoolean(false); private final ConfigProvider configProvider; private final JavaProfiler profiler; + private final Set configuredProfilingModes = EnumSet.noneOf(ProfilingMode.class); private final Set profilingModes = EnumSet.noneOf(ProfilingMode.class); private final ContextSetter contextSetter; @@ -219,6 +231,8 @@ void reset() { private DatadogProfiler(ConfigProvider configProvider) { this.configProvider = configProvider; + this.profilerOptions = getProfilerOptions(configProvider); + warnIfProfilerOptionsConfigured(log, profilerOptions); this.profiler = DdprofLibraryLoader.javaProfiler().getComponent(); this.detailedDebugLogging = configProvider.getBoolean( @@ -232,23 +246,25 @@ private DatadogProfiler(ConfigProvider configProvider) { // TODO enable/disable events by name (e.g. datadog.ExecutionSample), not flag, so configuration // can be consistent with JFR event control if (isAllocationProfilingEnabled(configProvider)) { - profilingModes.add(ALLOCATION); + configuredProfilingModes.add(ALLOCATION); } if (isMemoryLeakProfilingEnabled(configProvider)) { - profilingModes.add(MEMLEAK); + configuredProfilingModes.add(MEMLEAK); } if (isCpuProfilerEnabled(configProvider)) { - profilingModes.add(CPU); + configuredProfilingModes.add(CPU); } if (isWallClockProfilerEnabled(configProvider)) { - profilingModes.add(WALL); + configuredProfilingModes.add(WALL); } if (isNativeMemoryProfilingEnabled(configProvider)) { - profilingModes.add(NATIVEMEM); + configuredProfilingModes.add(NATIVEMEM); } if (isNativeSocketProfilingEnabled(configProvider)) { - profilingModes.add(NATIVESOCKET); + configuredProfilingModes.add(NATIVESOCKET); } + profilingModes.addAll(configuredProfilingModes); + applyProfilerOptionModes(profilingModes, profilerOptions); Set contextAttributes = getContextAttributes(configProvider); this.orderedContextAttributes = getOrderedContextAttributes(contextAttributes, configProvider); this.contextSetter = new ContextSetter(profiler, orderedContextAttributes); @@ -398,6 +414,9 @@ void dump(Path path) { String cmdStartProfiling(Path file) throws IllegalStateException { // 'start' = start, 'jfr=7' = store in JFR format ready for concatenation + StringBuilder cmd = new StringBuilder("start,jfr"); + cmd.append(",file=").append(file.toAbsolutePath()); + int derivedOptionsStart = cmd.length(); int safemode = getSafeMode(configProvider); if (safemode != ProfilingConfig.PROFILING_DATADOG_PROFILER_SAFEMODE_DEFAULT) { // be very vocal about messing around with the profiler safemode as it may induce crashes @@ -406,8 +425,6 @@ String cmdStartProfiling(Path file) throws IllegalStateException { + "This is not recommended and may cause instability and crashes.", safemode); } - StringBuilder cmd = new StringBuilder("start,jfr"); - cmd.append(",file=").append(file.toAbsolutePath()); cmd.append(",loglevel=").append(getLogLevel(configProvider)); cmd.append(",jstackdepth=").append(getStackDepth(configProvider)); cmd.append(",cstack=").append(getCStack(configProvider)); @@ -423,7 +440,7 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append(",fjmethodid=false"); } - if (profilingModes.contains(CPU)) { + if (configuredProfilingModes.contains(CPU)) { // cpu profiling is enabled. String schedulingEvent = getSchedulingEvent(configProvider); if (schedulingEvent != null && !schedulingEvent.isEmpty()) { @@ -444,7 +461,7 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append(",cpu=").append(interval).append('m'); } } - if (profilingModes.contains(WALL)) { + if (configuredProfilingModes.contains(WALL)) { // wall profiling is enabled. cmd.append(",wall="); if (getWallCollapsing(configProvider)) { @@ -462,24 +479,25 @@ String cmdStartProfiling(Path file) throws IllegalStateException { } } cmd.append(",loglevel=").append(getLogLevel(configProvider)); - if (profilingModes.contains(ALLOCATION) || profilingModes.contains(MEMLEAK)) { + if (configuredProfilingModes.contains(ALLOCATION) + || configuredProfilingModes.contains(MEMLEAK)) { // allocation profiling or live heap profiling is enabled cmd.append(",memory=").append(getAllocationInterval(configProvider)).append('b'); cmd.append(':'); - if (profilingModes.contains(ALLOCATION)) { + if (configuredProfilingModes.contains(ALLOCATION)) { cmd.append('a'); } - if (profilingModes.contains(MEMLEAK)) { + if (configuredProfilingModes.contains(MEMLEAK)) { cmd.append(isLiveHeapSizeTrackingEnabled(configProvider) ? 'L' : 'l'); cmd.append(':') .append(String.format("%.2f", getLiveHeapSamplePercent(configProvider) / 100.0d)); } } - if (profilingModes.contains(NATIVEMEM)) { + if (configuredProfilingModes.contains(NATIVEMEM)) { // native memory (malloc) profiling is enabled cmd.append(",nativemem=").append(getNativeMemoryInterval(configProvider)); } - if (profilingModes.contains(NATIVESOCKET)) { + if (configuredProfilingModes.contains(NATIVESOCKET)) { // native socket I/O profiling is enabled String interval = getNativeSocketInterval(configProvider); cmd.append(",natsock"); @@ -487,11 +505,199 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append('=').append(interval); } } + if (profilerOptions != null && !profilerOptions.isEmpty()) { + String mergedOptions = + mergeProfilerOptions(cmd.substring(derivedOptionsStart), profilerOptions); + cmd.setLength(derivedOptionsStart); + cmd.append(mergedOptions); + } String cmdString = cmd.toString(); log.debug("Datadog profiler command line: {}", cmdString); return cmdString; } + static String mergeProfilerOptions(String derivedOptions, String configuredOptions) { + Map configuredByName = configuredProfilerOptions(configuredOptions); + + StringBuilder mergedOptions = new StringBuilder(); + for (int start = 0; start <= derivedOptions.length(); ) { + int end = derivedOptions.indexOf(',', start); + if (end < 0) { + end = derivedOptions.length(); + } + String trimmedOption = derivedOptions.substring(start, end).trim(); + if (!trimmedOption.isEmpty() + && !overridesDerivedProfilerOption(configuredByName, profilerOptionName(trimmedOption))) { + mergedOptions.append(',').append(trimmedOption); + } + if (end == derivedOptions.length()) { + break; + } + start = end + 1; + } + for (String option : configuredByName.values()) { + mergedOptions.append(',').append(option); + } + return mergedOptions.toString(); + } + + private static Map configuredProfilerOptions(String configuredOptions) { + Map configuredByName = new LinkedHashMap<>(); + for (int start = 0; start <= configuredOptions.length(); ) { + int end = configuredOptions.indexOf(',', start); + if (end < 0) { + end = configuredOptions.length(); + } + String trimmedOption = configuredOptions.substring(start, end).trim(); + if (!trimmedOption.isEmpty()) { + String name = profilerOptionName(trimmedOption); + if (!isReservedProfilerOption(name)) { + if (isCpuEngineOption(name)) { + configuredByName.remove("cpu"); + configuredByName.remove("event"); + } + // Reinsert duplicate keys to order them by their last occurrence. + configuredByName.remove(name); + configuredByName.put(name, normalizeProfilerOption(name, trimmedOption)); + } + } + if (end == configuredOptions.length()) { + break; + } + start = end + 1; + } + return configuredByName; + } + + private static boolean overridesDerivedProfilerOption( + Map configuredByName, String derivedName) { + if (configuredByName.containsKey(derivedName)) { + return true; + } + if (isCpuEngineOption(derivedName)) { + return configuredByName.containsKey("cpu") || configuredByName.containsKey("event"); + } + return "interval".equals(derivedName) && configuredByName.containsKey("cpu"); + } + + private static boolean isCpuEngineOption(String name) { + return "cpu".equals(name) || "event".equals(name); + } + + private static String normalizeProfilerOption(String name, String option) { + int equals = option.indexOf('='); + return equals < 0 ? name : name + '=' + option.substring(equals + 1).trim(); + } + + static void warnIfProfilerOptionsConfigured(Logger logger, String configuredOptions) { + if (configuredOptions != null && !configuredOptions.isEmpty()) { + logger.warn(SEND_TELEMETRY, PROFILER_OPTIONS_WARNING); + } + } + + private static String profilerOptionName(String option) { + int equals = option.indexOf('='); + String name = equals < 0 ? option : option.substring(0, equals).trim(); + return name.toLowerCase(Locale.ROOT); + } + + private static boolean isReservedProfilerOption(String name) { + switch (name) { + case "start": + case "resume": + case "stop": + case "check": + case "status": + case "list": + case "version": + case "jfr": + case "file": + case "attributes": + return true; + default: + return false; + } + } + + static void applyProfilerOptionModes(Set modes, String configuredOptions) { + if (configuredOptions == null || configuredOptions.isEmpty()) { + return; + } + Map configuredByName = configuredProfilerOptions(configuredOptions); + if (configuredByName.containsKey("cpu") || configuredByName.containsKey("event")) { + modes.remove(CPU); + } + for (Map.Entry option : configuredByName.entrySet()) { + String name = option.getKey(); + switch (name) { + case "cpu": + modes.add(CPU); + break; + case "event": + String event = profilerOptionValue(option.getValue()); + if (!event.isEmpty() && !"alloc".equals(event) && !"noop".equals(event)) { + modes.add(CPU); + } + break; + case "wall": + modes.add(WALL); + break; + case "memory": + applyMemoryProfilerModes(modes, profilerOptionValue(option.getValue())); + break; + case "nativemem": + modes.add(NATIVEMEM); + break; + case "natsock": + modes.add(NATIVESOCKET); + break; + default: + break; + } + } + String generations = configuredByName.get("generations"); + if (generations != null && profilerBooleanValue(generations)) { + modes.add(MEMLEAK); + } + } + + private static void applyMemoryProfilerModes(Set modes, String value) { + modes.remove(ALLOCATION); + modes.remove(MEMLEAK); + int separator = value.indexOf(':'); + if (separator < 0) { + modes.add(ALLOCATION); + modes.add(MEMLEAK); + return; + } + String flags = value.substring(separator + 1); + int nextSeparator = flags.indexOf(':'); + if (nextSeparator >= 0) { + flags = flags.substring(0, nextSeparator); + } + if (flags.indexOf('a') >= 0) { + modes.add(ALLOCATION); + } + if (flags.indexOf('l') >= 0 || flags.indexOf('L') >= 0) { + modes.add(MEMLEAK); + } + } + + private static String profilerOptionValue(String option) { + int equals = option.indexOf('='); + return equals < 0 ? "" : option.substring(equals + 1).trim(); + } + + private static boolean profilerBooleanValue(String option) { + String value = profilerOptionValue(option); + return value.isEmpty() + || "y".equals(value) + || "yes".equals(value) + || "t".equals(value) + || "true".equals(value) + || "1".equals(value); + } + public void recordTraceRoot(long rootSpanId, String endpoint, String operation) { if (!profiler.recordTraceRoot(rootSpanId, endpoint, operation, MAX_NUM_ENDPOINTS)) { log.debug( diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java index b54b069fe55..518c048993e 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java @@ -40,6 +40,7 @@ import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_NATIVESOCKET_ENABLED; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_NATIVESOCKET_ENABLED_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_NATIVESOCKET_INTERVAL; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_SAFEMODE; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_SAFEMODE_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_SCHEDULING_EVENT; @@ -90,6 +91,10 @@ public static String getLibPath() { return getLibPath(ConfigProvider.getInstance()); } + public static String getProfilerOptions(ConfigProvider configProvider) { + return configProvider.getString(PROFILING_DATADOG_PROFILER_OPTIONS); + } + public static boolean isCpuProfilerEnabled() { return isCpuProfilerEnabled(ConfigProvider.getInstance()); } diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerConfigTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerConfigTest.java index ed4b4476606..f140b67fa2a 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerConfigTest.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerConfigTest.java @@ -1,14 +1,22 @@ package com.datadog.profiling.ddprof; import static com.datadog.profiling.controller.ProfilingSupport.isOldObjectSampleAvailable; +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import com.datadog.profiling.utils.ProfilingMode; import datadog.environment.JavaVirtualMachine; import datadog.trace.api.config.ProfilingConfig; import datadog.trace.bootstrap.config.provider.ConfigProvider; +import java.util.EnumSet; import java.util.Properties; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -16,6 +24,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; class DatadogProfilerConfigTest { private String originalVmName; @@ -97,6 +106,136 @@ void testGetCStackWithHotspot(String configuredStackwalker, String expectedStack + "'"); } + @Test + void profilerOptionsReadDdprofKey() { + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "cpu=10m"); + + assertEquals( + "cpu=10m", + DatadogProfilerConfig.getProfilerOptions(ConfigProvider.withPropertiesOverride(props))); + } + + @Test + void profilerOptionsAreNullWhenUnset() { + assertNull( + DatadogProfilerConfig.getProfilerOptions( + ConfigProvider.withPropertiesOverride(new Properties()))); + } + + @ParameterizedTest + @MethodSource("profilerOptionMergeCases") + void mergeProfilerOptions( + String derivedOptions, String configuredOptions, String expectedOptions) { + assertEquals( + expectedOptions, DatadogProfiler.mergeProfilerOptions(derivedOptions, configuredOptions)); + } + + @Test + void warningIsNotEmittedWithoutProfilerOptions() { + Logger logger = mock(Logger.class); + + DatadogProfiler.warnIfProfilerOptionsConfigured(logger, null); + DatadogProfiler.warnIfProfilerOptionsConfigured(logger, ""); + + verifyNoInteractions(logger); + } + + @Test + void warningIsMarkedForTelemetryWhenProfilerOptionsAreConfigured() { + Logger logger = mock(Logger.class); + + DatadogProfiler.warnIfProfilerOptionsConfigured(logger, "cpu=7m"); + + verify(logger).warn(SEND_TELEMETRY, DatadogProfiler.PROFILER_OPTIONS_WARNING); + } + + private static Stream profilerOptionMergeCases() { + return Stream.of( + Arguments.of( + ",cpu=10m,wall=50m,loglevel=INFO", + "cpu=7m,nativemem=131072", + ",wall=50m,loglevel=INFO,cpu=7m,nativemem=131072"), + Arguments.of( + ",loglevel=INFO,cpu=10m,loglevel=INFO", "loglevel=DEBUG", ",cpu=10m,loglevel=DEBUG"), + Arguments.of(",natsock=100us,wall=50m", "natsock", ",wall=50m,natsock"), + Arguments.of( + ",cpu=10m,wall=50m", + " , cpu=7m, , nativemem=131072, ", + ",wall=50m,cpu=7m,nativemem=131072"), + Arguments.of(",cpu=10m,wall=50m", " ", ",cpu=10m,wall=50m"), + Arguments.of(",cpu=10m,wall=50m", "cpu=5m,cpu=7m", ",wall=50m,cpu=7m"), + Arguments.of(",cpu=10m", "CPU=7m", ",cpu=7m"), + Arguments.of(",cpu=10m,wall=50m", "cpu = 7m", ",wall=50m,cpu=7m"), + Arguments.of(",cpu=10m,wall=50m", "event=cycles", ",wall=50m,event=cycles"), + Arguments.of(",event=cycles,interval=1000000", "cpu=7m", ",cpu=7m"), + Arguments.of(",cpu=10m", "cpu=7m,event=cycles", ",event=cycles"), + Arguments.of( + ",event=cycles,interval=1000000", + "event=instructions", + ",interval=1000000,event=instructions"), + Arguments.of( + ",cpu=10m,attributes=foo", + "START,Resume,stop,CHECK,status,List,VERSION,JFR,File=other.jfr,ATTRIBUTES=bar,cpu=7m", + ",attributes=foo,cpu=7m")); + } + + @ParameterizedTest + @MethodSource("profilerOptionModeCases") + void applyProfilerOptionModes( + Set initialModes, String configuredOptions, Set expectedModes) { + DatadogProfiler.applyProfilerOptionModes(initialModes, configuredOptions); + + assertEquals(expectedModes, initialModes); + } + + private static Stream profilerOptionModeCases() { + return Stream.of( + Arguments.of( + EnumSet.noneOf(ProfilingMode.class), + "CPU=7m,event=cycles,WALL=50m,nativemem=131072,NATSOCK", + EnumSet.of( + ProfilingMode.CPU, + ProfilingMode.WALL, + ProfilingMode.NATIVEMEM, + ProfilingMode.NATIVESOCKET)), + Arguments.of( + EnumSet.of(ProfilingMode.ALLOCATION), + "memory=262144b:l:0.1", + EnumSet.of(ProfilingMode.MEMLEAK)), + Arguments.of( + EnumSet.of(ProfilingMode.MEMLEAK), + "memory=262144b:a", + EnumSet.of(ProfilingMode.ALLOCATION)), + Arguments.of( + EnumSet.of(ProfilingMode.ALLOCATION, ProfilingMode.MEMLEAK), + "memory=262144b:", + EnumSet.noneOf(ProfilingMode.class)), + Arguments.of( + EnumSet.noneOf(ProfilingMode.class), + "memory=262144b:aL:0.1", + EnumSet.of(ProfilingMode.ALLOCATION, ProfilingMode.MEMLEAK)), + Arguments.of( + EnumSet.noneOf(ProfilingMode.class), + "memory=262144b:a,memory=262144b:l", + EnumSet.of(ProfilingMode.MEMLEAK)), + Arguments.of( + EnumSet.noneOf(ProfilingMode.class), + "event=noop,memory=262144b:,generations=true", + EnumSet.of(ProfilingMode.MEMLEAK)), + Arguments.of( + EnumSet.of(ProfilingMode.CPU), "event=noop", EnumSet.noneOf(ProfilingMode.class)), + Arguments.of(EnumSet.of(ProfilingMode.CPU), "event=cycles", EnumSet.of(ProfilingMode.CPU)), + Arguments.of( + EnumSet.of(ProfilingMode.CPU), + "cpu=7m,event=noop", + EnumSet.noneOf(ProfilingMode.class)), + Arguments.of( + EnumSet.noneOf(ProfilingMode.class), + "event=noop,cpu=7m", + EnumSet.of(ProfilingMode.CPU))); + } + private static Stream j9StackwalkerTestCases() { return Stream.of( // Unsupported stackwalkers - should fall back to dwarf diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java index c09e6ce8fb9..021f36b331d 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java @@ -121,6 +121,94 @@ void testStartCmdEnableJMethodIDOptim() throws Exception { assertTrue(startCmd(profiler).contains(",fjmethodid=false")); } + @Test + void testStartCmdExperimentalOptionsExtendAndOverrideDerivedOptions() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "cpu=7m,nativemem=131072"); + // wall clock is disabled by default on J9, so opt in explicitly to keep the derived-option + // assertions JVM-independent + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED, "true"); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_LOG_LEVEL, "DEBUG"); + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + Path targetFile = Paths.get("/tmp/target.jfr"); + String cmd = profiler.cmdStartProfiling(targetFile); + assertTrue(cmd.startsWith("start,jfr,file=" + targetFile.toAbsolutePath())); + int explicitCpuOption = cmd.lastIndexOf(",cpu=7m"); + assertTrue(explicitCpuOption >= 0); + assertEquals(explicitCpuOption, cmd.indexOf(",cpu=")); + assertTrue(cmd.contains(",wall=")); + assertTrue(cmd.contains(",loglevel=DEBUG")); + assertTrue(cmd.endsWith(",cpu=7m,nativemem=131072")); + assertTrue(profiler.enabledModes().contains(ProfilingMode.NATIVEMEM)); + } + + @Test + void testExperimentalEventOptionOverridesDerivedCpuOption() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "event=cycles"); + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + String cmd = startCmd(profiler); + assertFalse(cmd.contains(",cpu="), cmd); + assertTrue(cmd.endsWith(",event=cycles"), cmd); + assertTrue(profiler.enabledModes().contains(ProfilingMode.CPU)); + } + + @Test + void testExperimentalNoopEventDisablesCpuMode() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_SCHEDULING_EVENT, "cycles"); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "event=noop"); + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + String cmd = startCmd(profiler); + assertFalse(cmd.contains(",event=cycles"), cmd); + assertTrue(cmd.endsWith(",event=noop"), cmd); + assertFalse(profiler.enabledModes().contains(ProfilingMode.CPU)); + } + + @Test + void testExperimentalMemoryOptionUpdatesEnabledModes() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_ALLOCATION_ENABLED, "false"); + props.put(ProfilingConfig.PROFILING_HEAP_ENABLED, "false"); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "memory=262144b:a"); + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + assertTrue(profiler.enabledModes().contains(ProfilingMode.ALLOCATION)); + assertFalse(profiler.enabledModes().contains(ProfilingMode.MEMLEAK)); + String cmd = startCmd(profiler); + assertTrue(cmd.endsWith(",memory=262144b:a"), cmd); + assertEquals(cmd.indexOf(",memory="), cmd.lastIndexOf(",memory="), cmd); + + props.put(ProfilingConfig.PROFILING_ALLOCATION_ENABLED, "true"); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_OPTIONS, "memory=262144b:l:0.1"); + profiler = DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + assertFalse(profiler.enabledModes().contains(ProfilingMode.ALLOCATION)); + assertTrue(profiler.enabledModes().contains(ProfilingMode.MEMLEAK)); + cmd = startCmd(profiler); + assertTrue(cmd.endsWith(",memory=262144b:l:0.1"), cmd); + assertEquals(cmd.indexOf(",memory="), cmd.lastIndexOf(",memory="), cmd); + } + @Test void testStartCmdNativeMemDisabledByDefault() throws Exception { assertDoesNotThrow( diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java index 37dc6c3c500..6bfae88186e 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java @@ -131,6 +131,9 @@ public final class ProfilingConfig { public static final String PROFILING_DATADOG_PROFILER_SCHEDULING_EVENT_INTERVAL = "profiling.experimental.ddprof.scheduling.event.interval"; + public static final String PROFILING_DATADOG_PROFILER_OPTIONS = + "profiling.experimental.ddprof.options"; + public static final String PROFILING_PROCESS_CONTEXT_ENABLED = "profiling.experimental.process_context.enabled"; public static final boolean PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT = true; diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 45e12e44c85..602133f28d9 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -3337,6 +3337,14 @@ "aliases": [] } ], + "DD_PROFILING_EXPERIMENTAL_DDPROF_OPTIONS": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], "DD_PROFILING_EXPERIMENTAL_DDPROF_SCHEDULING_EVENT": [ { "version": "A",