Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,31 @@

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) {
return null;
}
}

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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.+'
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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<TypeDescription> 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"));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SocketChannel>() {
@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());
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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());
}
}
Loading
Loading