diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java index 38892d8aa74..176dabd5913 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java @@ -3,16 +3,22 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.nio.channels.Selector; import java.util.concurrent.TimeUnit; -/** Helper for synchronously bracketing untraced {@code Thread.sleep} intervals. */ +/** + * Helper for synchronously bracketing untraced blocking intervals ({@code Thread.sleep}, {@code + * Selector.select}) with a {@code datadog.TaskBlock} JFR event. + */ public final class TaskBlockHelper { private TaskBlockHelper() {} - static ProfilingContextIntegration profiling() { + /** Returns the active profiling context integration, or {@code null} when unavailable. */ + public static ProfilingContextIntegration profiling() { try { return AgentTracer.get().getProfilingContext(); } catch (Throwable ignored) { @@ -20,7 +26,8 @@ static ProfilingContextIntegration profiling() { } } - static long begin(ProfilingContextIntegration profiling) { + /** Starts a TaskBlock interval, returning {@code 0} when the interval was not accepted. */ + public static long begin(ProfilingContextIntegration profiling) { if (profiling == null) { return 0L; } @@ -31,7 +38,8 @@ static long begin(ProfilingContextIntegration profiling) { } } - static void finish(ProfilingContextIntegration profiling, long token) { + /** Completes a TaskBlock interval previously accepted by {@link #begin}. */ + public static void finish(ProfilingContextIntegration profiling, long token) { if (profiling == null || token == 0L) { return; } @@ -86,6 +94,35 @@ static void sleep(ProfilingContextIntegration profiling, TimeUnit unit, long tim } } + /** Brackets {@link Selector#select()} with a synchronous TaskBlock interval. */ + public static int select(Selector selector) throws IOException { + return select(profiling(), selector); + } + + static int select(ProfilingContextIntegration profiling, Selector selector) throws IOException { + long token = begin(profiling); + try { + return selector.select(); + } finally { + finish(profiling, token); + } + } + + /** Brackets {@link Selector#select(long)} with a synchronous TaskBlock interval. */ + public static int select(Selector selector, long timeout) throws IOException { + return select(profiling(), selector, timeout); + } + + static int select(ProfilingContextIntegration profiling, Selector selector, long timeout) + throws IOException { + long token = begin(profiling); + try { + return selector.select(timeout); + } finally { + finish(profiling, token); + } + } + /** * Brackets {@code Thread.sleep(Duration)} without linking {@code Duration} on JDKs where that * overload is unavailable. diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.java index 35b08c4e42b..2f3c2521cbf 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.java @@ -12,6 +12,8 @@ import static org.mockito.Mockito.when; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import java.io.IOException; +import java.nio.channels.Selector; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -98,6 +100,41 @@ void longIntAndTimeUnitSleepsBalanceAcceptedTokens() throws InterruptedException verify(profiling, times(2)).endTaskBlock(TOKEN, 0L, 0L); } + @Test + void selectBalancesAcceptedTokenAndReturnsSelectorResult() throws IOException { + ProfilingContextIntegration profiling = acceptedIntegration(); + Selector selector = mock(Selector.class); + when(selector.select()).thenReturn(3); + + int ready = TaskBlockHelper.select(profiling, selector); + + assertEquals(3, ready); + verify(profiling).endTaskBlock(TOKEN, 0L, 0L); + } + + @Test + void selectWithTimeoutBalancesAcceptedTokenAndReturnsSelectorResult() throws IOException { + ProfilingContextIntegration profiling = acceptedIntegration(); + Selector selector = mock(Selector.class); + when(selector.select(5L)).thenReturn(2); + + int ready = TaskBlockHelper.select(profiling, selector, 5L); + + assertEquals(2, ready); + verify(profiling).endTaskBlock(TOKEN, 0L, 0L); + } + + @Test + void selectIOExceptionBalancesAcceptedTokenBeforeRethrowing() throws IOException { + ProfilingContextIntegration profiling = acceptedIntegration(); + Selector selector = mock(Selector.class); + when(selector.select()).thenThrow(new IOException("closed")); + + assertThrows(IOException.class, () -> TaskBlockHelper.select(profiling, selector)); + + verify(profiling).endTaskBlock(TOKEN, 0L, 0L); + } + private static ProfilingContextIntegration acceptedIntegration() { ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class); when(profiling.beginTaskBlock()).thenReturn(TOKEN); diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/build.gradle b/dd-java-agent/instrumentation/datadog/profiling/nio-select/build.gradle new file mode 100644 index 00000000000..5726db22f23 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/build.gradle @@ -0,0 +1,25 @@ +// Copyright 2026 Datadog, Inc. + +apply from: "$rootDir/gradle/java.gradle" + +muzzle { + pass { + coreJdk() + } +} + +addTestSuiteForDir('latestDepTest', 'test') +addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') + +dependencies { + testImplementation libs.bundles.junit5 + testImplementation libs.bundles.mockito + testImplementation libs.bytebuddy + testImplementation group: 'io.netty', name: 'netty-transport', version: '4.1.108.Final' + testImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.58.0' + + // Netty 4.2.x moved NioEventLoop's Selector.select() call site to a new NioIoHandler class; + // exercise the same forked tests against the latest Netty 4.x to cover both class layouts. + latestDepTestImplementation group: 'io.netty', name: 'netty-transport', version: '4.+' + latestDepTestImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.+' +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/main/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentation.java b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/main/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentation.java new file mode 100644 index 00000000000..834be7d993e --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/main/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentation.java @@ -0,0 +1,89 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nioselect; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; +import static datadog.trace.agent.tooling.csi.CallSiteAdvice.AdviceType.AROUND; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.agent.tooling.bytebuddy.csi.Advices; +import datadog.trace.agent.tooling.bytebuddy.csi.CallSiteTransformer; +import datadog.trace.agent.tooling.csi.CallSites; +import datadog.trace.api.Config; +import datadog.trace.api.profiling.TaskBlockInstrumentationConfig; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Brackets {@link java.nio.channels.Selector#select()}/{@code select(long)} call sites in Netty's + * own NIO event loop with a synchronous {@code datadog.TaskBlock} interval. + * + *

Scoped to Netty's own event-loop callers only, not arbitrary application callers: Netty never + * runs its event loop on a virtual thread, so every bracketed call is guaranteed to be a genuine + * platform-OS-thread block. {@code selectNow()} is non-blocking and intentionally excluded. + * + *

Netty 4.1.x calls {@code Selector.select()}/{@code select(long)} directly from {@code + * NioEventLoop}; Netty 4.2.x moved that call into a separate {@code NioIoHandler} class (used by + * {@code SingleThreadIoEventLoop}). Both caller classes (plain and gRPC-shaded) are matched so this + * instrumentation covers both Netty major versions. + */ +@AutoService(InstrumenterModule.class) +public class NioSelectProfilingInstrumentation extends InstrumenterModule.Profiling + implements Instrumenter.ForCallSite, Instrumenter.HasTypeAdvice { + + private static final String TASK_BLOCK_HELPER = + "datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper"; + + private static final String[] NIO_EVENT_LOOPS = { + "io.netty.channel.nio.NioEventLoop", + "io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop", + "io.netty.channel.nio.NioIoHandler", + "io.grpc.netty.shaded.io.netty.channel.nio.NioIoHandler" + }; + + public NioSelectProfilingInstrumentation() { + super("nio-select"); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() + && TaskBlockInstrumentationConfig.isEnabled(Config.get(), ConfigProvider.getInstance()); + } + + @Override + public ElementMatcher callerType() { + return namedOneOf(NIO_EVENT_LOOPS); + } + + @Override + public void typeAdvice(TypeTransformer transformer) { + transformer.applyAdvice(new CallSiteTransformer("nio-select", createAdvices())); + } + + static Advices createAdvices() { + return Advices.fromCallSites(new NioSelectCallSites()); + } + + public static final class NioSelectCallSites implements CallSites { + @Override + public void accept(Container container) { + container.addAdvice( + AROUND, + "java/nio/channels/Selector", + "select", + "()I", + (handler, opcode, owner, name, descriptor, isInterface) -> + handler.advice(TASK_BLOCK_HELPER, "select", "(Ljava/nio/channels/Selector;)I")); + container.addAdvice( + AROUND, + "java/nio/channels/Selector", + "select", + "(J)I", + (handler, opcode, owner, name, descriptor, isInterface) -> + handler.advice(TASK_BLOCK_HELPER, "select", "(Ljava/nio/channels/Selector;J)I")); + } + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/GrpcShadedNioSelectProfilingInstrumentationForkedTest.java b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/GrpcShadedNioSelectProfilingInstrumentationForkedTest.java new file mode 100644 index 00000000000..120491cf370 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/GrpcShadedNioSelectProfilingInstrumentationForkedTest.java @@ -0,0 +1,69 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nioselect; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.grpc.netty.shaded.io.netty.bootstrap.ServerBootstrap; +import io.grpc.netty.shaded.io.netty.channel.ChannelInitializer; +import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.socket.SocketChannel; +import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioServerSocketChannel; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** Proves the {@code namedOneOf(...)} shaded class name actually matches at runtime. */ +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class GrpcShadedNioSelectProfilingInstrumentationForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void shadedNettyEventLoopSelectDispatchesBalancedTaskBlocks() throws InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertTrue(testProfilingContextIntegration.getTaskBlockBeginCalls().get() > 0); + assertEquals( + testProfilingContextIntegration.getTaskBlockBeginCalls().get(), + testProfilingContextIntegration.getTaskBlockEndCalls().get()); + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectCallSiteTest.java b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectCallSiteTest.java new file mode 100644 index 00000000000..bcc67ca77ec --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectCallSiteTest.java @@ -0,0 +1,54 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nioselect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.tooling.bytebuddy.csi.Advices; +import java.lang.reflect.Modifier; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.jupiter.api.Test; + +class NioSelectCallSiteTest { + + @Test + void usesItsOwnInstrumentationName() { + assertEquals("nio-select", new NioSelectProfilingInstrumentation().name()); + } + + @Test + void registersBothSelectOverloads() { + Advices advices = NioSelectProfilingInstrumentation.createAdvices(); + + assertNotNull(advices.findAdvice("java/nio/channels/Selector", "select", "()I")); + assertNotNull(advices.findAdvice("java/nio/channels/Selector", "select", "(J)I")); + } + + @Test + void callSiteProviderIsAccessibleAcrossAgentClassLoaders() { + Class provider = NioSelectProfilingInstrumentation.NioSelectCallSites.class; + + assertTrue(Modifier.isPublic(provider.getModifiers())); + assertTrue(Modifier.isStatic(provider.getModifiers())); + } + + @Test + void callerTypeMatchesOnlyNettyEventLoops() { + ElementMatcher matcher = new NioSelectProfilingInstrumentation().callerType(); + + assertTrue(matcher.matches(named("io.netty.channel.nio.NioEventLoop"))); + assertTrue(matcher.matches(named("io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop"))); + assertTrue(matcher.matches(named("io.netty.channel.nio.NioIoHandler"))); + assertTrue(matcher.matches(named("io.grpc.netty.shaded.io.netty.channel.nio.NioIoHandler"))); + assertFalse(matcher.matches(named("com.example.MyApp"))); + assertFalse(matcher.matches(named("io.netty.channel.epoll.EpollEventLoop"))); + } + + private static TypeDescription named(String name) { + return new TypeDescription.Latent( + name, Modifier.PUBLIC, null, java.util.Collections.emptyList()); + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingDisabledForkedTest.java b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingDisabledForkedTest.java new file mode 100644 index 00000000000..eaf1d212949 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingDisabledForkedTest.java @@ -0,0 +1,65 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nioselect; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "false") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class NioSelectProfilingDisabledForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void disabledTaskBlockGateLeavesNettySelectUninstrumented() throws InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertEquals(0, testProfilingContextIntegration.getTaskBlockBeginCalls().get()); + assertEquals(0, testProfilingContextIntegration.getTaskBlockEndCalls().get()); + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentationForkedTest.java b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentationForkedTest.java new file mode 100644 index 00000000000..15641a3c48b --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/nio-select/src/test/java/datadog/trace/instrumentation/nioselect/NioSelectProfilingInstrumentationForkedTest.java @@ -0,0 +1,81 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nioselect; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import java.nio.channels.Selector; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class NioSelectProfilingInstrumentationForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void nettyEventLoopSelectDispatchesBalancedTaskBlocks() throws InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + // The event loop's own select() call sites are running in the background; give it time + // to accumulate a few idle wait cycles. + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertTrue(testProfilingContextIntegration.getTaskBlockBeginCalls().get() > 0); + assertEquals( + testProfilingContextIntegration.getTaskBlockBeginCalls().get(), + testProfilingContextIntegration.getTaskBlockEndCalls().get()); + assertEquals(0L, testProfilingContextIntegration.getLastTaskBlockBlocker().get()); + } + + @Test + void plainSelectorOutsideNettyIsNotInstrumented() throws Exception { + try (Selector selector = Selector.open()) { + selector.select(1L); + } + + assertEquals(0, testProfilingContextIntegration.getTaskBlockBeginCalls().get()); + } +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/build.gradle b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/build.gradle new file mode 100644 index 00000000000..d6e60313f2f --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/build.gradle @@ -0,0 +1,35 @@ +apply from: "$rootDir/gradle/java.gradle" + +muzzle { + pass { + group = "io.netty" + module = "netty-transport" + versions = "[4.1.0.Final,)" + } + pass { + group = "io.grpc" + module = "grpc-netty-shaded" + versions = "[,]" + assertInverse = false + } + pass { + coreJdk() + } +} + +addTestSuiteForDir('latestDepTest', 'test') +addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') + +dependencies { + testImplementation libs.bundles.junit5 + testImplementation libs.bundles.mockito + testImplementation group: 'io.netty', name: 'netty-transport', version: '4.1.108.Final' + testImplementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.108.Final', classifier: 'linux-x86_64' + testImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.58.0' + + // Netty 4.2.x moved EpollEventLoop's wait methods to a new EpollIoHandler class; exercise the + // same forked tests against the latest Netty 4.x to cover both class layouts. + latestDepTestImplementation group: 'io.netty', name: 'netty-transport', version: '4.+' + latestDepTestImplementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.+', classifier: 'linux-x86_64' + latestDepTestImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.+' +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/main/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentation.java b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/main/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentation.java new file mode 100644 index 00000000000..d464a2443b5 --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/main/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentation.java @@ -0,0 +1,93 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nettyepoll; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; +import datadog.trace.api.profiling.TaskBlockInstrumentationConfig; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import datadog.trace.bootstrap.instrumentation.java.concurrent.TaskBlockHelper; +import net.bytebuddy.asm.Advice; + +/** + * Brackets Netty's native-epoll event loop wait calls with a synchronous {@code datadog.TaskBlock} + * interval. + * + *

Targets the private wait methods ({@code epollWait}, {@code epollWaitNow}, {@code + * epollWaitNoTimerChange}, {@code epollWaitTimeboxed}, {@code epollBusyWait}) rather than {@code + * Native}'s static methods: which {@code Native.epollWait} overload is called, and its descriptor, + * both drift across Netty versions (a package-private, 6-arg, threshold-aware overload replaced the + * public 5-arg one around 4.1.53), but these private method names are stable across versions. + * Matching by method name (not descriptor) and instrumenting the caller's own method also reaches + * package-private overloads used by busy-loop/low-latency configurations, which an external + * call-site rewrite of {@code Native} could not. + * + *

Netty 4.1.x declares these methods directly on {@code EpollEventLoop}; Netty 4.2.x moved the + * wait loop into a separate {@code EpollIoHandler} class (used by {@code SingleThreadIoEventLoop}), + * keeping the same method names. Both classes are matched so this instrumentation covers both Netty + * major versions. + * + *

The advice itself references no Netty types, so the identical advice class also covers gRPC's + * shaded copy of the same classes, matching the class-list pattern already established by {@code + * EnableWallclockProfilingInstrumentation}. + * + *

Scoped to Netty's own event-loop classes only (never arbitrary application code): Netty never + * runs its event loop on a virtual thread, so every bracketed call is guaranteed to be a genuine + * platform-OS-thread block. + */ +@AutoService(InstrumenterModule.class) +public class NettyEpollProfilingInstrumentation extends InstrumenterModule.Profiling + implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { + + private static final String[] EPOLL_EVENT_LOOPS = { + "io.netty.channel.epoll.EpollEventLoop", + "io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoop", + "io.netty.channel.epoll.EpollIoHandler", + "io.grpc.netty.shaded.io.netty.channel.epoll.EpollIoHandler" + }; + + public NettyEpollProfilingInstrumentation() { + super("netty-epoll"); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() + && TaskBlockInstrumentationConfig.isEnabled(Config.get(), ConfigProvider.getInstance()); + } + + @Override + public String[] knownMatchingTypes() { + return EPOLL_EVENT_LOOPS; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(nameStartsWith("epollWait").or(named("epollBusyWait"))) + .and(isDeclaredBy(namedOneOf(EPOLL_EVENT_LOOPS))), + getClass().getName() + "$EpollWaitAdvice"); + } + + public static final class EpollWaitAdvice { + /** Starts a TaskBlock interval before the native epoll wait. */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static long before() { + return TaskBlockHelper.begin(TaskBlockHelper.profiling()); + } + + /** Completes the TaskBlock interval after the native epoll wait returns. */ + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void after(@Advice.Enter long token) { + TaskBlockHelper.finish(TaskBlockHelper.profiling(), token); + } + } +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/GrpcShadedNettyEpollProfilingInstrumentationForkedTest.java b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/GrpcShadedNettyEpollProfilingInstrumentationForkedTest.java new file mode 100644 index 00000000000..a5d7b78e1ea --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/GrpcShadedNettyEpollProfilingInstrumentationForkedTest.java @@ -0,0 +1,72 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nettyepoll; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.grpc.netty.shaded.io.netty.bootstrap.ServerBootstrap; +import io.grpc.netty.shaded.io.netty.channel.ChannelInitializer; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.socket.SocketChannel; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +/** Proves the {@code namedOneOf(...)} shaded class name actually matches at runtime. */ +@EnabledOnOs(OS.LINUX) +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class GrpcShadedNettyEpollProfilingInstrumentationForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void shadedNettyEpollEventLoopWaitDispatchesBalancedTaskBlocks() throws InterruptedException { + EpollEventLoopGroup group = new EpollEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(EpollServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertTrue(testProfilingContextIntegration.getTaskBlockBeginCalls().get() > 0); + assertEquals( + testProfilingContextIntegration.getTaskBlockBeginCalls().get(), + testProfilingContextIntegration.getTaskBlockEndCalls().get()); + } +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingDisabledForkedTest.java b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingDisabledForkedTest.java new file mode 100644 index 00000000000..bad49a4b770 --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingDisabledForkedTest.java @@ -0,0 +1,68 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nettyepoll; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +@EnabledOnOs(OS.LINUX) +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "false") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class NettyEpollProfilingDisabledForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void disabledTaskBlockGateLeavesEpollWaitUninstrumented() throws InterruptedException { + EpollEventLoopGroup group = new EpollEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(EpollServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertEquals(0, testProfilingContextIntegration.getTaskBlockBeginCalls().get()); + assertEquals(0, testProfilingContextIntegration.getTaskBlockEndCalls().get()); + } +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationForkedTest.java b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationForkedTest.java new file mode 100644 index 00000000000..1c914f81f79 --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationForkedTest.java @@ -0,0 +1,74 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nettyepoll; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.test.junit.utils.config.WithConfig; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +@EnabledOnOs(OS.LINUX) +@WithConfig(key = PROFILING_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_ENABLED, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_PRECHECK, value = "true") +@WithConfig(key = PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, value = "false") +class NettyEpollProfilingInstrumentationForkedTest extends AbstractInstrumentationTest { + + @BeforeEach + void clearProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @AfterEach + void resetProfilingContextIntegration() { + testProfilingContextIntegration.clear(); + } + + @Test + @Timeout(30) + void nettyEpollEventLoopWaitDispatchesBalancedTaskBlocks() throws InterruptedException { + EpollEventLoopGroup group = new EpollEventLoopGroup(1); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(EpollServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + + // The event loop's own epollWait() call sites run in the background; give it time to + // accumulate a few idle wait cycles. + TimeUnit.MILLISECONDS.sleep(500); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + + assertTrue(testProfilingContextIntegration.getTaskBlockBeginCalls().get() > 0); + assertEquals( + testProfilingContextIntegration.getTaskBlockBeginCalls().get(), + testProfilingContextIntegration.getTaskBlockEndCalls().get()); + assertEquals(0L, testProfilingContextIntegration.getLastTaskBlockBlocker().get()); + } +} diff --git a/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationTest.java b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationTest.java new file mode 100644 index 00000000000..729844e325e --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-epoll-4.1/src/test/java/datadog/trace/instrumentation/nettyepoll/NettyEpollProfilingInstrumentationTest.java @@ -0,0 +1,93 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.instrumentation.nettyepoll; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.tooling.Instrumenter; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.jupiter.api.Test; + +/** + * Uses {@link MethodDescription.Latent}/{@link TypeDescription.Latent} fixtures named exactly like + * the real Netty types, rather than loading the real (native-library-backed) {@code EpollEventLoop} + * classes, so the test runs on any OS/arch. + */ +class NettyEpollProfilingInstrumentationTest { + + private static final String PLAIN = "io.netty.channel.epoll.EpollEventLoop"; + private static final String SHADED = "io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoop"; + private static final String PLAIN_IO_HANDLER = "io.netty.channel.epoll.EpollIoHandler"; + private static final String SHADED_IO_HANDLER = + "io.grpc.netty.shaded.io.netty.channel.epoll.EpollIoHandler"; + + @Test + void usesItsOwnInstrumentationName() { + assertEquals("netty-epoll", new NettyEpollProfilingInstrumentation().name()); + } + + @Test + void matchesPlainAndShadedEpollEventLoopAndEpollIoHandler() { + assertArrayEquals( + new String[] {PLAIN, SHADED, PLAIN_IO_HANDLER, SHADED_IO_HANDLER}, + new NettyEpollProfilingInstrumentation().knownMatchingTypes()); + } + + @Test + void matchesEveryKnownEpollWaitVariantByNameOnly() { + ElementMatcher matcher = capturedMatcher(); + + for (String name : + new String[] { + "epollWait", + "epollWaitNow", + "epollWaitNoTimerChange", + "epollWaitTimeboxed", + "epollBusyWait" + }) { + for (String declaringClass : + new String[] {PLAIN, SHADED, PLAIN_IO_HANDLER, SHADED_IO_HANDLER}) { + assertTrue(matcher.matches(methodOf(declaringClass, name)), declaringClass + "#" + name); + } + } + } + + @Test + void doesNotMatchUnrelatedMethodsOrClasses() { + ElementMatcher matcher = capturedMatcher(); + + assertFalse(matcher.matches(methodOf(PLAIN, "run"))); + assertFalse(matcher.matches(methodOf("io.netty.channel.epoll.Native", "epollWait"))); + } + + private static ElementMatcher capturedMatcher() { + AtomicReference> captured = new AtomicReference<>(); + Instrumenter.MethodTransformer transformer = + (matcher, adviceClass, additionalAdviceClasses) -> captured.set(matcher); + new NettyEpollProfilingInstrumentation().methodAdvice(transformer); + return captured.get(); + } + + private static MethodDescription methodOf(String declaringClass, String name) { + TypeDescription declaring = + new TypeDescription.Latent(declaringClass, Modifier.PUBLIC, null, Collections.emptyList()); + return new MethodDescription.Latent( + declaring, + name, + Modifier.PRIVATE, + Collections.emptyList(), + TypeDescription.Generic.VOID, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + null, + null); + } +} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/TestProfilingContextIntegration.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/TestProfilingContextIntegration.java index 61b44d294d2..5f45a2f00cc 100644 --- a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/TestProfilingContextIntegration.java +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/TestProfilingContextIntegration.java @@ -39,6 +39,16 @@ public class TestProfilingContextIntegration implements ProfilingContextIntegrat private final BlockingDeque closedTimings = new LinkedBlockingDeque<>(); private final Logger logger = LoggerFactory.getLogger(TestProfilingContextIntegration.class); private volatile boolean acceptParkEntries = true; + private final AtomicInteger taskBlockBeginCalls = new AtomicInteger(); + private final AtomicInteger taskBlockEndCalls = new AtomicInteger(); + private final ConcurrentMap taskBlockBeginCallsByThread = + new ConcurrentHashMap<>(); + private final ConcurrentMap taskBlockEndCallsByThread = + new ConcurrentHashMap<>(); + private final AtomicLong nextTaskBlockToken = new AtomicLong(); + private final AtomicLong lastTaskBlockBlocker = new AtomicLong(); + private final AtomicLong lastTaskBlockUnblockingSpanId = new AtomicLong(); + private volatile boolean acceptTaskBlockEntries = true; @Override public void onAttach() { @@ -63,6 +73,14 @@ public void clear() { lastUnblockingSpanId.set(0); parkExitThreads.clear(); acceptParkEntries = true; + taskBlockBeginCalls.set(0); + taskBlockEndCalls.set(0); + taskBlockBeginCallsByThread.clear(); + taskBlockEndCallsByThread.clear(); + nextTaskBlockToken.set(0); + lastTaskBlockBlocker.set(0); + lastTaskBlockUnblockingSpanId.set(0); + acceptTaskBlockEntries = true; } @Override @@ -92,6 +110,29 @@ public void parkExit(long blocker, long unblockingSpanId) { parkExitThreads.add(Thread.currentThread()); } + @Override + public long beginTaskBlock() { + taskBlockBeginCalls.incrementAndGet(); + taskBlockBeginCallsByThread + .computeIfAbsent(Thread.currentThread(), ignored -> new AtomicInteger()) + .incrementAndGet(); + return acceptTaskBlockEntries ? nextTaskBlockToken.incrementAndGet() : 0L; + } + + @Override + public boolean endTaskBlock(long token, long blocker, long unblockingSpanId) { + if (token == 0L) { + return false; + } + taskBlockEndCalls.incrementAndGet(); + taskBlockEndCallsByThread + .computeIfAbsent(Thread.currentThread(), ignored -> new AtomicInteger()) + .incrementAndGet(); + lastTaskBlockBlocker.set(blocker); + lastTaskBlockUnblockingSpanId.set(unblockingSpanId); + return true; + } + @Override public String name() { return "test"; @@ -182,6 +223,36 @@ public void setAcceptParkEntries(boolean acceptParkEntries) { this.acceptParkEntries = acceptParkEntries; } + public AtomicInteger getTaskBlockBeginCalls() { + return taskBlockBeginCalls; + } + + public AtomicInteger getTaskBlockEndCalls() { + return taskBlockEndCalls; + } + + public int getTaskBlockBeginCalls(Thread thread) { + AtomicInteger calls = taskBlockBeginCallsByThread.get(thread); + return calls == null ? 0 : calls.get(); + } + + public int getTaskBlockEndCalls(Thread thread) { + AtomicInteger calls = taskBlockEndCallsByThread.get(thread); + return calls == null ? 0 : calls.get(); + } + + public AtomicLong getLastTaskBlockBlocker() { + return lastTaskBlockBlocker; + } + + public AtomicLong getLastTaskBlockUnblockingSpanId() { + return lastTaskBlockUnblockingSpanId; + } + + public void setAcceptTaskBlockEntries(boolean acceptTaskBlockEntries) { + this.acceptTaskBlockEntries = acceptTaskBlockEntries; + } + public boolean isBalanced() { return counter.get() == 0; } diff --git a/dd-smoke-tests/profiling-integration-tests/build.gradle b/dd-smoke-tests/profiling-integration-tests/build.gradle index be0506df3e2..aabd0606165 100644 --- a/dd-smoke-tests/profiling-integration-tests/build.gradle +++ b/dd-smoke-tests/profiling-integration-tests/build.gradle @@ -34,6 +34,8 @@ dependencies { testImplementation libs.bundles.jmc testImplementation libs.aircompressor testImplementation libs.jackson.databind + testImplementation group: 'io.netty', name: 'netty-transport', version: '4.1.108.Final' + testImplementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.108.Final', classifier: 'linux-x86_64' } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NettyEpollTaskBlockForkedApp.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NettyEpollTaskBlockForkedApp.java new file mode 100644 index 00000000000..bce5d7cb5c6 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NettyEpollTaskBlockForkedApp.java @@ -0,0 +1,42 @@ +// Copyright 2026 Datadog, Inc. +package com.datadog.smoketest.profiling; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** Forked workload idling a native-epoll Netty event loop so it repeatedly blocks in epollWait. */ +public final class NettyEpollTaskBlockForkedApp { + public static final String EVENT_LOOP_THREAD = "netty-epoll-taskblock"; + + private static final long PROFILING_STARTUP_DELAY_MILLIS = 1500L; + private static final long IDLE_MILLIS = 3000L; + + private NettyEpollTaskBlockForkedApp() {} + + public static void main(String[] args) throws Exception { + Thread.sleep(PROFILING_STARTUP_DELAY_MILLIS); + ThreadFactory threadFactory = runnable -> new Thread(runnable, EVENT_LOOP_THREAD); + EpollEventLoopGroup group = new EpollEventLoopGroup(1, threadFactory); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(EpollServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + TimeUnit.MILLISECONDS.sleep(IDLE_MILLIS); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + Thread.sleep(1500L); + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NioSelectTaskBlockForkedApp.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NioSelectTaskBlockForkedApp.java new file mode 100644 index 00000000000..6c1e0d0489a --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NioSelectTaskBlockForkedApp.java @@ -0,0 +1,42 @@ +// Copyright 2026 Datadog, Inc. +package com.datadog.smoketest.profiling; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** Forked workload idling a NIO Netty event loop so it repeatedly blocks in Selector.select. */ +public final class NioSelectTaskBlockForkedApp { + public static final String EVENT_LOOP_THREAD = "netty-nio-select-taskblock"; + + private static final long PROFILING_STARTUP_DELAY_MILLIS = 1500L; + private static final long IDLE_MILLIS = 3000L; + + private NioSelectTaskBlockForkedApp() {} + + public static void main(String[] args) throws Exception { + Thread.sleep(PROFILING_STARTUP_DELAY_MILLIS); + ThreadFactory threadFactory = runnable -> new Thread(runnable, EVENT_LOOP_THREAD); + NioEventLoopGroup group = new NioEventLoopGroup(1, threadFactory); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) {} + }); + bootstrap.bind(0).sync().channel(); + TimeUnit.MILLISECONDS.sleep(IDLE_MILLIS); + } finally { + group.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + Thread.sleep(1500L); + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NettyEpollTaskBlockProfilingTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NettyEpollTaskBlockProfilingTest.java new file mode 100644 index 00000000000..e9891179080 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NettyEpollTaskBlockProfilingTest.java @@ -0,0 +1,107 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.TaskBlockProfilingTestSupport.BLOCKER; +import static datadog.smoketest.TaskBlockProfilingTestSupport.LOCAL_ROOT_SPAN_ID; +import static datadog.smoketest.TaskBlockProfilingTestSupport.SPAN_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.datadog.smoketest.profiling.NettyEpollTaskBlockForkedApp; +import java.io.IOException; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.item.ItemFilters; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** Linux smoke coverage for TaskBlocks emitted around Netty's native-epoll event loop wait. */ +@DisabledOnJ9 +@EnabledOnOs(OS.LINUX) +final class NettyEpollTaskBlockProfilingTest { + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + logFilePath = + TaskBlockProfilingTestSupport.buildLogFilePath( + NettyEpollTaskBlockProfilingTest.class, testInfo, "nettyEpoll"); + dumpDir = TaskBlockProfilingTestSupport.createDumpDir("dd-profiler-nettyepoll-"); + } + + @AfterEach + void tearDown() throws IOException { + TaskBlockProfilingTestSupport.deleteRecursively(dumpDir); + } + + @Test + void idleNettyEpollEventLoopEmitsSpanlessTaskBlocks() throws Exception { + Process targetProcess = + TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-netty-epoll-taskblock", + NettyEpollTaskBlockForkedApp.class.getName(), + dumpDir, + logFilePath) + .start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + + assertTrue(stats.count > 0, "Expected TaskBlocks from the idle Netty epoll event loop"); + assertEquals(0, stats.nonZeroBlockerCount, "Blocker attribution is out of scope; expected 0"); + assertFalse(stats.hasNonZeroSpanId, "Spanless epoll TaskBlocks must keep spanId zero"); + assertFalse( + stats.hasNonZeroLocalRootSpanId, "Spanless epoll TaskBlocks must keep root spanId zero"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertFalse( + TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, "NoClassDefFoundError", "Failed to handle exception"), + "Netty epoll TaskBlock instrumentation failed"); + } + + private static final class JfrStats { + long count; + long nonZeroBlockerCount; + boolean hasNonZeroSpanId; + boolean hasNonZeroLocalRootSpanId; + boolean hasMissingEventThread; + + void add(IItemCollection events) { + for (IItemIterable items : events.apply(ItemFilters.type("datadog.TaskBlock"))) { + TaskBlockProfilingTestSupport.assertFinalTaskBlockSchema(items); + IMemberAccessor spanId = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanId = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blocker = BLOCKER.getAccessor(items.getType()); + IMemberAccessor eventThread = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + for (IItem item : items) { + String thread = eventThread.getMember(item); + if (!NettyEpollTaskBlockForkedApp.EVENT_LOOP_THREAD.equals(thread)) { + continue; + } + count++; + nonZeroBlockerCount += blocker.getMember(item).longValue() == 0 ? 0 : 1; + hasNonZeroSpanId |= spanId.getMember(item).longValue() != 0; + hasNonZeroLocalRootSpanId |= rootSpanId.getMember(item).longValue() != 0; + hasMissingEventThread |= thread.isEmpty(); + } + } + } + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NioSelectTaskBlockProfilingTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NioSelectTaskBlockProfilingTest.java new file mode 100644 index 00000000000..8d9fce34d83 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NioSelectTaskBlockProfilingTest.java @@ -0,0 +1,104 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.TaskBlockProfilingTestSupport.BLOCKER; +import static datadog.smoketest.TaskBlockProfilingTestSupport.LOCAL_ROOT_SPAN_ID; +import static datadog.smoketest.TaskBlockProfilingTestSupport.SPAN_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.datadog.smoketest.profiling.NioSelectTaskBlockForkedApp; +import java.io.IOException; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.item.ItemFilters; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** Smoke coverage for TaskBlocks emitted around Netty's NIO event loop Selector.select wait. */ +@DisabledOnJ9 +final class NioSelectTaskBlockProfilingTest { + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + logFilePath = + TaskBlockProfilingTestSupport.buildLogFilePath( + NioSelectTaskBlockProfilingTest.class, testInfo, "nioSelect"); + dumpDir = TaskBlockProfilingTestSupport.createDumpDir("dd-profiler-nioselect-"); + } + + @AfterEach + void tearDown() throws IOException { + TaskBlockProfilingTestSupport.deleteRecursively(dumpDir); + } + + @Test + void idleNettyNioEventLoopEmitsSpanlessTaskBlocks() throws Exception { + Process targetProcess = + TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-netty-nio-select-taskblock", + NioSelectTaskBlockForkedApp.class.getName(), + dumpDir, + logFilePath) + .start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + + assertTrue(stats.count > 0, "Expected TaskBlocks from the idle Netty NIO event loop"); + assertEquals(0, stats.nonZeroBlockerCount, "Blocker attribution is out of scope; expected 0"); + assertFalse(stats.hasNonZeroSpanId, "Spanless select TaskBlocks must keep spanId zero"); + assertFalse( + stats.hasNonZeroLocalRootSpanId, "Spanless select TaskBlocks must keep root spanId zero"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertFalse( + TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, "NoClassDefFoundError", "Failed to handle exception"), + "Netty NIO select TaskBlock instrumentation failed"); + } + + private static final class JfrStats { + long count; + long nonZeroBlockerCount; + boolean hasNonZeroSpanId; + boolean hasNonZeroLocalRootSpanId; + boolean hasMissingEventThread; + + void add(IItemCollection events) { + for (IItemIterable items : events.apply(ItemFilters.type("datadog.TaskBlock"))) { + TaskBlockProfilingTestSupport.assertFinalTaskBlockSchema(items); + IMemberAccessor spanId = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanId = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blocker = BLOCKER.getAccessor(items.getType()); + IMemberAccessor eventThread = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + for (IItem item : items) { + String thread = eventThread.getMember(item); + if (!NioSelectTaskBlockForkedApp.EVENT_LOOP_THREAD.equals(thread)) { + continue; + } + count++; + nonZeroBlockerCount += blocker.getMember(item).longValue() == 0 ? 0 : 1; + hasNonZeroSpanId |= spanId.getMember(item).longValue() != 0; + hasNonZeroLocalRootSpanId |= rootSpanId.getMember(item).longValue() != 0; + hasMissingEventThread |= thread.isEmpty(); + } + } + } + } +} diff --git a/docs/netty-wallclock-coverage-notes.md b/docs/netty-wallclock-coverage-notes.md new file mode 100644 index 00000000000..db0fed109ce --- /dev/null +++ b/docs/netty-wallclock-coverage-notes.md @@ -0,0 +1,70 @@ +# Netty wall-clock coverage: two independent, separable paths + +Context: investigation on `paul.fournillon/wallclock-threadsleep-taskblock`, prompted by +Native Socket I/O events (`datadog.NativeSocketEvent`) never firing for `prof-java`'s +AWS/Azure clients, which use `netty-transport-native-epoll`. + +Root cause: java-profiler's native socket interposer +(`ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp`, `socket_patch_target_for_library`) +only PLT/GOT-patches `libnet.so`, `libnio.so`, or the IBM `libjava.so` JCL bridge, and only +when physically located inside the JDK's own lib directory (`resolve_jdk_library_directory`). +Netty's native epoll `.so` is never eligible, so none of `send`/`recv`/`read`/`write`/ +`epoll_wait`/`poll`/`select` get intercepted in it. + +This gap has **two structurally different fixes**, targeting different mechanisms in +ddprof. Do not conflate them — they have different implementation cost, different risk, +and arguably different value. + +## Path A — Native Socket I/O for Netty (extend the interposer allowlist) + +- Add Netty's native epoll `.so` (or a broader same-directory/companion-jar heuristic) to + `socket_patch_target_for_library` in `libraryPatcher_linux.cpp`. +- Native/C++ only change, in a security-sensitive allowlist (the JDK-directory check + exists specifically to avoid patching arbitrary application/JNI DSOs). +- Produces `NativeSocketEvent` (`nativeSocketSampler.cpp`), which is per-call and carries + a `remoteAddress` resolved via `getpeername`. +- **Weak fit for `epoll_wait` specifically**: `epoll_wait` blocks across many + simultaneously-registered fds/peers, so there's no single attributable remote address, + and most `epoll_wait` returns are healthy idle waiting, not a stalled peer. The + send/recv/read/write case (single fd, single peer) is the strong fit; the multiplexed + wait call is not. + +## Path B — TaskBlock + signal suppression for Netty (bytecode call-site bracketing) + +- Independent mechanism. `beginTaskBlock`/`endTaskBlock` + (`ddprof-lib/src/main/java/com/datadoghq/profiler/TaskBlockBridge.java` → + `javaApi.cpp:Java_com_datadoghq_profiler_JavaProfiler_beginTaskBlock0`/`endTaskBlock0` → + `ThreadFilter::activeOwnedBlockGeneration`/`isOwnedBlockSuppressionCandidate` in + `threadFilter.cpp`) has nothing to do with the socket interposer or `IO_WAIT`. +- This is the exact same primitive already used by + `ThreadSleepProfilingInstrumentation`/the LockSupport instrumentation + (`dd-java-agent/instrumentation/datadog/profiling/thread-sleep/...`): a + `CallSiteAdvice`-based bytecode rewrite that brackets a target call with + `begin()` / `try { ... } finally { finish() }`. +- For Netty: bracket the Java-level blocking entry point (e.g. + `io.netty.channel.epoll.EpollEventLoop`'s wait call, or NIO `SelectorImpl`'s select) + the same way `ThreadSleepCallSites` brackets `Thread.sleep`. +- **All-Java**, no native/allowlist changes, no dependency on which native transport + Netty is using (epoll vs kqueue vs NIO) since the bracket is at the Java call site, not + the native syscall. +- Signal suppression and the TaskBlock backfill are bundled atomically by construction + (same as sleep/park today) — no separate coverage-gap risk to reason about. +- Same open question as Path A applies here too: is bracketing a multiplexed, + mostly-healthy wait as a single "blocked" interval a meaningful signal, or just noise + with a fancy name? Worth prototyping cheaply (it's one instrumentation module) before + deciding. + +## Bottom line for whoever picks this up + +- These are not two ways to reach the same goal — Path A is a native interposer scope + extension for per-connection I/O attribution; Path B is a Java bytecode + instrumentation for suppress-and-backfill signal accounting. They can be pursued + independently, in either order, or not at all. +- Path B is cheaper and lower-risk to prototype (no C++ changes) and is structurally + identical to already-shipped code (`ThreadSleepProfilingInstrumentation`). +- Neither path is currently justified as "filling an existing gap" — today, Netty's + `epoll_wait`-blocked threads are *not* precheck-suppressed at all (they fall through to + ddprof's generic HotSpot-reported native/`SYSCALL` state, which is not in + `isPrecheckSuppressionState`), so ordinary signal-based wall-clock sampling already + covers them. Building either path *introduces* a new suppression category together + with its own backfill — it is not closing a hole that exists today. diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 4d63f386eab..bd716dd8588 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -8409,6 +8409,14 @@ "aliases": ["DD_TRACE_INTEGRATION_NETTY_ENABLED", "DD_INTEGRATION_NETTY_ENABLED"] } ], + "DD_TRACE_NETTY_EPOLL_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_NETTY_EPOLL_ENABLED", "DD_INTEGRATION_NETTY_EPOLL_ENABLED"] + } + ], "DD_TRACE_NETTY_EVENT_EXECUTOR_ENABLED": [ { "version": "A", @@ -8457,6 +8465,14 @@ "aliases": ["DD_TRACE_INTEGRATION_NING_ENABLED", "DD_INTEGRATION_NING_ENABLED"] } ], + "DD_TRACE_NIO_SELECT_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_NIO_SELECT_ENABLED", "DD_INTEGRATION_NIO_SELECT_ENABLED"] + } + ], "DD_TRACE_NOT_NOT_TRACE_ENABLED": [ { "version": "A", diff --git a/settings.gradle.kts b/settings.gradle.kts index 599f795e49d..e3dd3da47a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -341,6 +341,7 @@ include( ":dd-java-agent:instrumentation:datadog:profiling:enable-wallclock-profiling", ":dd-java-agent:instrumentation:datadog:profiling:exception-profiling", ":dd-java-agent:instrumentation:datadog:profiling:lock-support", + ":dd-java-agent:instrumentation:datadog:profiling:nio-select", ":dd-java-agent:instrumentation:datadog:profiling:thread-sleep", ":dd-java-agent:instrumentation:datadog:tracing:trace-annotation", ":dd-java-agent:instrumentation:datanucleus-4.0.5", @@ -504,6 +505,7 @@ include( ":dd-java-agent:instrumentation:netty:netty-buffer-4.0", ":dd-java-agent:instrumentation:netty:netty-common", ":dd-java-agent:instrumentation:netty:netty-concurrent-4.0", + ":dd-java-agent:instrumentation:netty:netty-epoll-4.1", ":dd-java-agent:instrumentation:netty:netty-promise-4.0", ":dd-java-agent:instrumentation:ognl-appsec-3.3.2", ":dd-java-agent:instrumentation:okhttp:okhttp-2.2",