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 0c889108d2d..4ef2c59bfba 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 @@ -304,8 +304,14 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append('~'); } cmd.append(getWallInterval(configProvider)).append('m'); + // ddprof quirk: if filter parameter is omitted, it defaults to "0" (enabled), + // not empty string (disabled). When enabled without tracing, no threads are added + // to the filter, resulting in zero samples. We must explicitly pass filter= (empty) + // to disable filtering and sample all threads. if (getWallContextFilter(configProvider)) { cmd.append(",filter=0"); + } else { + cmd.append(",filter="); } if (useJvmtiWallclockSampler(configProvider)) { cmd.append(",wallsampler=jvmti"); 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 d023d13b219..f9b980f486e 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 @@ -142,7 +142,25 @@ public static boolean getWallCollapsing(ConfigProvider configProvider) { PROFILING_DATADOG_PROFILER_WALL_COLLAPSING_DEFAULT); } + /** + * Checks whether the wall-clock context filter should be used.
+ * The context filter will make wall-clock profiler to pick candidate threads only from threads + * with attached tracing context.
+ * If context filter is not used (this method returns {@literal false}) all threads will be + * considered for wallclock sampling. + * + * @param configProvider the associated config provider + * @return {@literal true} if the wallclock sampler should use context filtering, {@literal false} + * otherwise + */ public static boolean getWallContextFilter(ConfigProvider configProvider) { + // Context filtering requires tracing to be enabled - without tracing, + // there are no span contexts to filter on, so threads would never be added + // to the filter, resulting in no walltime samples. + boolean isTracingEnabled = configProvider.getBoolean(TRACE_ENABLED, true); + if (!isTracingEnabled) { + return false; + } return getBoolean( configProvider, PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, 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 c14783629c2..a7b07a8a02e 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 @@ -11,6 +11,7 @@ import datadog.environment.OperatingSystem; import datadog.libs.ddprof.DdprofLibraryLoader; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.config.TraceInstrumentationConfig; import datadog.trace.api.profiling.ProfilingScope; import datadog.trace.api.profiling.RecordingData; import datadog.trace.bootstrap.config.provider.ConfigProvider; @@ -106,6 +107,63 @@ private static Stream profilingModes() { Arguments.of((x & 0x1000) != 0, (x & 0x100) != 0, (x & 0x10) != 0, (x & 0x1) != 0)); } + @ParameterizedTest + @MethodSource("wallContextFilterModes") + void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled) + throws Exception { + // Skip test if profiler native library is not available (e.g., on macOS) + try { + Throwable reason = DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(); + if (reason != null) { + Assumptions.assumeTrue(false, "Profiler not available: " + reason.getMessage()); + } + } catch (Throwable e) { + Assumptions.assumeTrue(false, "Profiler not available: " + e.getMessage()); + } + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED, "true"); + props.put(TraceInstrumentationConfig.TRACE_ENABLED, Boolean.toString(tracingEnabled)); + props.put( + ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, + Boolean.toString(contextFilterEnabled)); + + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + Path targetFile = Paths.get("/tmp/target.jfr"); + String cmd = profiler.cmdStartProfiling(targetFile); + + assertTrue(cmd.contains("wall="), "Command should contain wall profiling: " + cmd); + + if (tracingEnabled && contextFilterEnabled) { + assertTrue( + cmd.contains(",filter=0"), + "Command should contain ',filter=0' when tracing and context filter are enabled: " + cmd); + } else { + assertTrue( + cmd.contains(",filter="), + "Command should contain ',filter=' when tracing is disabled or context filter is disabled: " + + cmd); + if (cmd.contains(",filter=0")) { + throw new AssertionError( + "Command should not contain ',filter=0' when tracing is disabled or context filter is disabled: " + + cmd); + } + } + } + + private static Stream wallContextFilterModes() { + return Stream.of( + Arguments.of(true, true), // tracing enabled, context filter enabled -> filter=0 + Arguments.of(true, false), // tracing enabled, context filter disabled -> filter= + Arguments.of( + false, true), // tracing disabled, context filter enabled -> filter= (tracing disabled + // overrides) + Arguments.of(false, false) // tracing disabled, context filter disabled -> filter= + ); + } + @Test public void testContextRegistration() { // warning - the profiler is a process wide singleton and can't be reinitialised diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java index 5030247339d..7a123aea9d4 100644 --- a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java @@ -491,6 +491,80 @@ void testBogusApiKey(final TestInfo testInfo) throws Exception { 3); } + @Test + @DisplayName("Test wallclock profiling without tracing") + public void testWallclockProfilingWithoutTracing(final TestInfo testInfo) throws Exception { + Assumptions.assumeTrue(OperatingSystem.isLinux()); + testWithRetry( + () -> { + try { + targetProcess = + createProcessBuilder( + profilingServer.getPort(), + tracingServer.getPort(), + VALID_API_KEY, + 0, + PROFILING_START_DELAY_SECONDS, + PROFILING_UPLOAD_PERIOD_SECONDS, + false, + true, + "on", + 0, + logFilePath, + false) + .start(); + + Assumptions.assumeFalse(JavaVirtualMachine.isJ9()); + + final RecordedRequest request = retrieveRequest(); + assertNotNull(request); + + final List items = + FileUpload.parse( + request.getBody().readByteArray(), request.getHeader("Content-Type")); + + FileItem rawJfr = items.get(1); + assertEquals("main.jfr", rawJfr.getName()); + + assertFalse(logHasErrors(logFilePath)); + InputStream eventStream = new ByteArrayInputStream(rawJfr.get()); + eventStream = decompressStream("on", eventStream); + IItemCollection events = JfrLoaderToolkit.loadEvents(eventStream); + assertTrue(events.hasItems()); + + IItemCollection wallclockSamples = + events.apply(ItemFilters.type("datadog.MethodSample")); + assertTrue( + wallclockSamples.hasItems(), "Expected wallclock samples when tracing is disabled"); + + // Verify span context is not present + for (IItemIterable event : wallclockSamples) { + IMemberAccessor rootSpanIdAccessor = + LOCAL_ROOT_SPAN_ID.getAccessor(event.getType()); + IMemberAccessor spanIdAccessor = + SPAN_ID.getAccessor(event.getType()); + for (IItem sample : event) { + assertEquals( + 0, + rootSpanIdAccessor.getMember(sample).longValue(), + "rootSpanId should be 0 when tracing is disabled"); + assertEquals( + 0, + spanIdAccessor.getMember(sample).longValue(), + "spanId should be 0 when tracing is disabled"); + } + } + } finally { + if (targetProcess != null) { + targetProcess.destroyForcibly(); + } + targetProcess = null; + } + }, + testInfo, + 3); + } + @Test @DisplayName("Test shutdown") @Disabled("https://github.com/DataDog/dd-trace-java/pull/5213") @@ -771,7 +845,8 @@ private ProcessBuilder createProcessBuilder( asyncProfilerEnabled, withCompression, exitDelay, - logFilePath); + logFilePath, + true); } private static ProcessBuilder createProcessBuilder( @@ -785,50 +860,57 @@ private static ProcessBuilder createProcessBuilder( final boolean asyncProfilerEnabled, final String withCompression, final int exitDelay, - final Path logFilePath) { + final Path logFilePath, + final boolean tracingEnabled) { final String templateOverride = JFRBasedProfilingIntegrationTest.class .getClassLoader() .getResource("overrides.jfp") .getFile(); - final List command = - Arrays.asList( - javaPath(), - "-Xmx" + System.getProperty("datadog.forkedMaxHeapSize", "1024M"), - "-Xms" + System.getProperty("datadog.forkedMinHeapSize", "64M"), - "-javaagent:" + agentShadowJar(), - "-XX:ErrorFile=/tmp/hs_err_pid%p.log", - "-Ddd.trace.agent.port=" + tracerPort, - "-Ddd.service.name=smoke-test-java-app", - "-Ddd.env=smoketest", - "-Ddd.version=99", - "-Ddd.profiling.enabled=true", - "-Ddd.profiling.stackdepth=" + STACK_DEPTH_LIMIT, - "-Ddd.profiling.ddprof.enabled=" + asyncProfilerEnabled, - "-Ddd.profiling.ddprof.alloc.enabled=" + asyncProfilerEnabled, - "-Ddd.profiling.agentless=" + (apiKey != null), - "-Ddd.profiling.start-delay=" + profilingStartDelaySecs, - "-Ddd.profiling.upload.period=" + profilingUploadPeriodSecs, - "-Ddd.profiling.url=http://localhost:" + profilerPort, - "-Ddd.profiling.hotspots.enabled=true", - "-Ddd.profiling.endpoint.collection.enabled=" + endpointCollectionEnabled, - "-Ddd.profiling.upload.timeout=" + PROFILING_UPLOAD_TIMEOUT_SECONDS, - "-Ddd.profiling.debug.dump_path=/tmp/dd-profiler", - "-Ddd.profiling.queueing.time.enabled=true", - "-Ddd.profiling.queueing.time.threshold.millis=0", - "-Ddd.profiling.debug.upload.compression=" + withCompression, - "-Ddatadog.slf4j.simpleLogger.defaultLogLevel=debug", - "-Ddd.profiling.context.attributes=foo,bar", - "-Dorg.slf4j.simpleLogger.defaultLogLevel=debug", - "-XX:+IgnoreUnrecognizedVMOptions", - "-XX:+UnlockCommercialFeatures", - "-XX:+FlightRecorder", - "-Ddd." + ProfilingConfig.PROFILING_TEMPLATE_OVERRIDE_FILE + "=" + templateOverride, - "-Ddd.jmxfetch.start-delay=" + jmxFetchDelaySecs, - "-jar", - profilingShadowJar(), - Integer.toString(exitDelay)); + final List command = new java.util.ArrayList<>(); + command.add(javaPath()); + command.add("-Xmx" + System.getProperty("datadog.forkedMaxHeapSize", "1024M")); + command.add("-Xms" + System.getProperty("datadog.forkedMinHeapSize", "64M")); + command.add("-javaagent:" + agentShadowJar()); + command.add("-XX:ErrorFile=/tmp/hs_err_pid%p.log"); + command.add("-Ddd.trace.agent.port=" + tracerPort); + command.add("-Ddd.service.name=smoke-test-java-app"); + command.add("-Ddd.env=smoketest"); + command.add("-Ddd.version=99"); + command.add("-Ddd.trace.enabled=" + tracingEnabled); + command.add("-Ddd.profiling.enabled=true"); + command.add("-Ddd.profiling.stackdepth=" + STACK_DEPTH_LIMIT); + command.add("-Ddd.profiling.ddprof.enabled=" + asyncProfilerEnabled); + command.add("-Ddd.profiling.ddprof.alloc.enabled=" + asyncProfilerEnabled); + if (!tracingEnabled && asyncProfilerEnabled) { + command.add("-Ddd.profiling.ddprof.wall.enabled=true"); + } + command.add("-Ddd.profiling.agentless=" + (apiKey != null)); + command.add("-Ddd.profiling.start-delay=" + profilingStartDelaySecs); + command.add("-Ddd.profiling.upload.period=" + profilingUploadPeriodSecs); + command.add("-Ddd.profiling.url=http://localhost:" + profilerPort); + command.add("-Ddd.profiling.hotspots.enabled=true"); + command.add("-Ddd.profiling.endpoint.collection.enabled=" + endpointCollectionEnabled); + command.add("-Ddd.profiling.upload.timeout=" + PROFILING_UPLOAD_TIMEOUT_SECONDS); + command.add("-Ddd.profiling.debug.dump_path=/tmp/dd-profiler"); + if (tracingEnabled) { + command.add("-Ddd.profiling.queueing.time.enabled=true"); + command.add("-Ddd.profiling.queueing.time.threshold.millis=0"); + command.add("-Ddd.profiling.context.attributes=foo,bar"); + } + command.add("-Ddd.profiling.debug.upload.compression=" + withCompression); + command.add("-Ddatadog.slf4j.simpleLogger.defaultLogLevel=debug"); + command.add("-Dorg.slf4j.simpleLogger.defaultLogLevel=debug"); + command.add("-XX:+IgnoreUnrecognizedVMOptions"); + command.add("-XX:+UnlockCommercialFeatures"); + command.add("-XX:+FlightRecorder"); + command.add( + "-Ddd." + ProfilingConfig.PROFILING_TEMPLATE_OVERRIDE_FILE + "=" + templateOverride); + command.add("-Ddd.jmxfetch.start-delay=" + jmxFetchDelaySecs); + command.add("-jar"); + command.add(profilingShadowJar()); + command.add(Integer.toString(exitDelay)); final ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.directory(new File(buildDirectory()));