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 super MethodDescription> 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 super MethodDescription> matcher = capturedMatcher();
+
+ assertFalse(matcher.matches(methodOf(PLAIN, "run")));
+ assertFalse(matcher.matches(methodOf("io.netty.channel.epoll.Native", "epollWait")));
+ }
+
+ private static ElementMatcher super MethodDescription> 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/settings.gradle.kts b/settings.gradle.kts
index f0bb9fa098a..a381c2d8e7c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -340,6 +340,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",
@@ -503,6 +504,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",