feat: native socket I/O tracking via PLT hooks (PROF-10637)#488
Conversation
CI Test ResultsRun: #27286948682 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-06-10 15:43:02 UTC |
d4feae7 to
7c6488b
Compare
6bec1dd to
ddc2292
Compare
a12cc1e to
44be5c7
Compare
9955e7d to
17dd7e0
Compare
17dd7e0 to
cecb1bd
Compare
|
Could not apply automatically — The reviewer comment (ID 4261434558) is an automated CI test-results report showing all 32 jobs passed with no failures. There is no code concern, requested change, or actionable finding to address with a patch.. Manual attention needed. |
Ports native socket sampling on top of MallocTracer's native_allocs pattern. Key design alignments with MallocTracer: - recordEvent passes NULL ucontext to recordSample (was: synthesized via getcontext, which confused walkVM's signal-context invariants) - Uses OS::threadId() instead of ProfiledThread::currentTid() - profiler.cpp recordSample BCI_NATIVE_SOCKET routes through walkVM with NULL ucontext (DWARF/FP + JavaFrameAnchor fallback) - Dlopen chain uses Profiler::dlopen_hook -> LibraryPatcher::install_socket_hooks (removes our custom dlopen_hook that previously clobbered Profiler::dlopen_hook) Removed: - Custom dlopen_hook and dlopen PLT patching in libraryPatcher_linux.cpp - recordEvent ucontext synthesis via getcontext() Kept: - PoissonSampler + RateLimiter (time-weighted sampling with PID) - Four hooks (send/recv/write/read) with per-thread Poisson state - fd-type cache, fd->addr cache Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tests assert the accessor for 'bytesTransferred' but I had renamed it to 'bytes' during the native_allocs rebase. Restore original field name and category/description to match existing tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebase onto the merged-main base replaced Contexts::get()/writeContext with the writeCurrentContext helper used by sibling event writers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allow BCI_NATIVE_SOCKET in J9/Zing walkJavaStack assert; skip cpu/wall config check when those intervals are not requested; drop Enabled/ EventFields iteration count from 5000 to 128. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accept optional time unit on 'natsock' (e.g. natsock=100us); converted to TSC ticks at start. NativeSocketSendRecvSeparateTest uses 100us to avoid Poisson-sampling variance at the default 1ms period. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Short-lived 64KB transfers at the default 1ms Poisson period yield too few samples to reliably match the server port. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the natsock=100us override consistently to positive event tests and UdpExcluded (stronger negative). RateLimitTest and Disabled keep the 1ms default on purpose: rate-limiter PID stress and feature-off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cached_send (send_fn) and cached_recv (recv_fn) differ in their second parameter (const void* vs void*); chaining the assignment forced an implicit recv_fn -> send_fn conversion that clang rejects. Split into two statements to match the line below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80d35d8 to
ce1f2f6
Compare
This comment has been minimized.
This comment has been minimized.
- profiler.cpp: stopRefresher() before NativeSocketSampler::stop() to prevent refresher from re-installing PLT hooks after unpatch - libraryPatcher_linux.cpp: protect dlsym statics with std::call_once; set _socket_active=false before restoring PLT slots in unpatch; re-check _socket_active under lock in patch_socket_functions - poissonSampler.h: cap double before u64 cast in nextExp() to avoid UB when interval=LONG_MAX; fix inaccurate U_min comment - arguments.cpp: add errno=0/ERANGE check and multiplication overflow guard in parseUnits - nativeSocketSampler.cpp: fix secs*tsc_freq u64 overflow before clamp; move _fd_cache_gen bump inside clearFdCache() under mutex; fix write_hook/read_hook double-counted debug counters; fix shouldSample op=2 routing to _send_sampler; call clearFdCache() in start() - nativeSocketSampler.h: update shouldSample op comment (0/1/2/3) - rateLimiter.h: clamp PID signal before long cast to avoid UB - NativeSocketEventFieldsTest: guard ip:port check for AF_UNIX events - NativeSocketRestartTest: createDirectories before createTempFile - nativeSocketSampler_ut.cpp: fix EmptyValueRejected→BareNatsockEnables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
patch_socket_functions() returned false on first call because the _socket_active guard fired before any patching occurred (_socket_active is only set at the end of the function). Guard now requires _socket_size>0 so the initial call from NativeSocketSampler::start() proceeds. recordEvent() lacked an isRunning() guard: with _rate_limiter._interval defaulting to 1, shouldSample() fires immediately in gtests, leading to Profiler::recordSample() with _calltrace_buffer==NULL -> SIGSEGV. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb894e92c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Adds an opt-in native socket I/O profiling feature on Linux by PLT-hooking libc send/recv/write/read and emitting a new datadog.NativeSocketEvent JFR event type with time-weighted Poisson sampling and PID-based global rate control.
Changes:
- Introduces
NativeSocketSampler(Poisson sampler + PIDRateLimiter) and integrates it into the profiler start/stop lifecycle and JFR writer/metadata. - Extends Linux
LibraryPatcher/CodeCacheimport scanning to patch GOT/PLT slots forsend/recv/write/read, including repatching on library refresh. - Adds C++ unit tests plus Java integration tests and a JMH benchmark for coverage and overhead evaluation.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketUdpExcludedTest.java | Adds UDP negative test for zero NativeSocketEvent emission. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketTestBase.java | Shared TCP workload helper for nativesocket tests. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketStackTraceTest.java | Validates stack traces are present on socket events across cstack modes. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketSendRecvSeparateTest.java | Validates SEND/RECV directionality is tracked independently. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRestartTest.java | Validates stop/restart lifecycle continues to produce events. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRemoteAddressTest.java | Validates remoteAddress formatting and matching server port. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRateLimitTest.java | Validates subsampling/rate limiting and weight behavior. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketMacOsNoOpTest.java | Validates macOS no-op behavior produces no events. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventThreadTest.java | Validates eventThread population. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventFieldsTest.java | Validates required event fields exist and are valid. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEnabledTest.java | Smoke test: events emitted when enabled. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketDisabledTest.java | Smoke test: no events when not enabled. |
| ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketBytesAccuracyTest.java | Validates sum(weight × duration) is within reasonable bounds. |
| ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/NativeSocketOverheadBenchmark.java | Adds JMH benchmark to quantify hook overhead for file/socket/NIO writes. |
| ddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp | Adds gtest coverage for hooks, PoissonSampler, and args parsing. |
| ddprof-lib/src/main/cpp/vmEntry.h | Adds BCI_NATIVE_SOCKET frame type. |
| ddprof-lib/src/main/cpp/rateLimiter.h | Adds shared PID-based rate limiter used by socket sampler. |
| ddprof-lib/src/main/cpp/profiler.cpp | Integrates nativesocket engine into start/stop/check and refresher ordering. |
| ddprof-lib/src/main/cpp/poissonSampler.h | Adds thread-local Poisson-process sampler with inverse-probability weights. |
| ddprof-lib/src/main/cpp/nativeSocketSampler.h | Declares sampler engine, caches, and hook entry points. |
| ddprof-lib/src/main/cpp/nativeSocketSampler.cpp | Implements hooks, fd classification/address caching, sampling, and event recording. |
| ddprof-lib/src/main/cpp/livenessTracker.cpp | Fixes _record_heap_usage initialization on reused singleton. |
| ddprof-lib/src/main/cpp/libraryPatcher.h | Adds socket patching APIs and _socket_active state. |
| ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp | Implements GOT/PLT patch/unpatch for send/recv/write/read with dlopen refresh support. |
| ddprof-lib/src/main/cpp/libraries.cpp | Triggers socket hook installation during library refresh. |
| ddprof-lib/src/main/cpp/jvmSupport.cpp | Allows OpenJ9/Zing stack walking for BCI_NATIVE_SOCKET. |
| ddprof-lib/src/main/cpp/jfrMetadata.h | Adds new JFR type id for native socket event. |
| ddprof-lib/src/main/cpp/jfrMetadata.cpp | Registers datadog.NativeSocketEvent fields in metadata. |
| ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | Extends stack walking logic to handle BCI_NATIVE_SOCKET. |
| ddprof-lib/src/main/cpp/flightRecorder.h | Adds recording API for native socket samples. |
| ddprof-lib/src/main/cpp/flightRecorder.cpp | Serializes native socket event payload into JFR buffers. |
| ddprof-lib/src/main/cpp/event.h | Adds NativeSocketEvent struct used by recorder. |
| ddprof-lib/src/main/cpp/codeCache.h | Adds import IDs for send/recv/write/read. |
| ddprof-lib/src/main/cpp/codeCache.cpp | Detects and stores GOT imports for send/recv/write/read. |
| ddprof-lib/src/main/cpp/arguments.h | Adds _nativesocket + _nativesocket_interval and event mask bit. |
| ddprof-lib/src/main/cpp/arguments.cpp | Parses natsock[=<duration>] and hardens parseUnits overflow handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
rkennke
left a comment
There was a problem hiding this comment.
This is a nice feature! I have a few requests.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Revalidate cached SOCKET fd-type verdicts to prevent reused-fd misclassification - Drain crossed Poisson thresholds (rebase on _used) to avoid burst-sampling bias - Clamp TSC duration subtraction for socket/lock/park events on core migration - Declare _orig_* libc pointers std::atomic with acquire/release - Log when the fd->addr cache latches full - Remove redundant second clearFdCache() in start() - Revert stray _record_heap_usage assignment pulled in by rebase - Fix benchmark profiler arg natsock; drop unused imports; correct time-weighted wording Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2a04e9ea9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Clamp TSC inversion in shouldSample() (pass 0 on t1<t0) - Defer getsockopt revalidation to sampled write/read events only - Extract insertFdAddrLocked(); add LRU unit tests + test seams - Add RateLimiter upper clamp (1L<<40, mirrors MallocTracer) - Deduplicate doTcpTransfer via NativeSocketTestBase - Tighten rate-limit assertion: operations/5 instead of operations/2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0287441 to
57b4dfa
Compare
…d/recv - Always call resolveAddr() for sampled events so fd reuse is detected within one sampling interval (was: stale address persisted until restart) - Add isSocket() guard to send_hook/recv_hook; connected UDP sockets (SOCK_DGRAM using send/recv) were emitting NativeSocketEvent - Move MAX_FD_CACHE to public (needed by LRU unit tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rkennke
left a comment
There was a problem hiding this comment.
Thanks for addressing the issues. Looks good now!
|
@rkennke Thanks for the patient review :) |
What does this PR do?
Intercepts libc
send,recv,write,readvia PLT hooking and recordsdatadog.NativeSocketEventJFR events with time-weighted inverse-transformsampling (Poisson process, PID rate control, target ~83 events/s / ~5000/min).
Motivation
Track blocking TCP socket I/O at the libc function level to surface socket
latency and throughput in the Datadog profiler. Netty with Java NIO transport
(the primary use case) goes through libc
send/recv/write/read, makingPLT patching the right interception point. Feature is explicitly opt-in via
the
natsockprofiler argument. Implements PROF-10637.Configuration
natsocknatsock=100usparseUnitsunit:ns,us,ms,s)The interval is the initial parameter of the time-weighted Poisson process;
the PID controller adapts it at runtime to hold the total event rate near
TARGET_EVENTS_PER_SECOND(= 83 events/s) across all four hooks combined.Architecture
flowchart LR A["Java code<br/>OutputStream.write()"] --> B["JDK native<br/>libnio/libnet"] B --> C{"PLT entry<br/>for write()"} C -- patched --> D["NativeSocketSampler::write_hook"] D --> E{"fd is TCP<br/>socket?"} E -- no --> F["orig write()"] E -- yes --> G["orig write()"] G --> H{"shouldSample<br/>(duration, op)"} H -- no --> I["return"] H -- yes --> J["recordEvent"] J --> K["JFR buffer"] F --> ISampling pipeline
flowchart TD A["Hook fires with<br/>duration_ticks"] --> B["PoissonSampler<br/>(thread_local)"] B --> C{"interval budget<br/>consumed?"} C -- no --> D["reject"] C -- yes --> E["compute weight<br/>1 / (1 - exp(-d/interval))"] E --> F["RateLimiter::accept()"] F --> G{"PID budget<br/>for this epoch?"} G -- no --> D G -- yes --> H["record event<br/>operation / fd-addr / bytes / weight"] H --> I["RateLimiter<br/>maybeUpdateInterval<br/>(PID: P=31 I=511 D=3)"] I --> J["next epoch interval"]Two
thread_local PoissonSamplerinstances per thread: outbound (send+write)and inbound (
recv+read). Both share oneRateLimiterso the ~83 events/starget is a single budget across all directions.
Hook installation
flowchart TD A["NativeSocketSampler::start"] --> B["dlsym RTLD_NEXT<br/>(RTLD_DEFAULT fallback on musl)<br/>resolve _orig_send/recv/write/read"] B --> C["patch_socket_functions"] C --> D["walk every loaded CodeCache"] D --> E{"lib imports<br/>send/recv/write/read?"} E -- yes --> F["rewrite PLT entry<br/>to *_hook"] E -- no --> G["skip"] C --> H["also patch dlopen PLT"] H --> I["dlopen_hook wraps<br/>subsequent loads"] I --> J["updateSymbols +<br/>install_socket_hooks"] J --> ELate-loaded libraries (e.g. HotSpot lazily loads
libnet.soon the firstjava.net.Socketuse) are caught by thedlopenwrapper which re-runspatch_socket_functionsafter each new library is registered.JFR event fields (
datadog.NativeSocketEvent)startTimeeventThreadstackTracedurationoperationSENDorRECVremoteAddressip:port(IPv4 or[ipv6]:port); empty if unknownbytesTransferredweight1 / P)spanIdlocalRootSpanIdsum(weight × duration)is an unbiased estimator of total socket I/O time;sum(weight × bytesTransferred)estimates total bytes.Key design notes
send/recv/write/readonly. UDP (sendto/recvfrom)and Netty's native epoll / io_uring transports are explicitly out of scope.
write/readhooks short-circuit on non-socket fds via alock-free
std::atomic<uint8_t> _fd_type_cache[65536]— one relaxed atomicload per call on the non-socket path.
handler), so
mallocandstd::mutexare safe.install_socket_hooks()(called fromdlopen_hookafterupdateSymbols) re-runspatch_socket_functionssolazily-loaded libraries (
libnet.so,libnio.so, native Netty transports)are patched the moment they appear.
stale addresses for reused fds are a known, accepted trade-off to avoid
racing with in-flight recordings.
_orig_send/recv/write/readareassigned once (when
_socket_size == 0) and are intentionally not nulled inunpatch_socket_functionsto avoid a memory-ordering race with in-flighthook invocations that may still be executing.
#if defined(__linux__)— compiled as no-op stubs onmacOS. Runs on both glibc and musl Linux; on musl
RTLD_NEXTreturns NULL(libc precedes the profiler DSO in the link map), so
RTLD_DEFAULTis usedas fallback to locate the real libc symbols. Feature is additionally disabled
in Java tests on J9/Zing via
Platform.isJ9()/Platform.isZing()guards.NativeSocketEventlives innativeSocketSampler.h(not the sharedevent.h) since it is only used byNativeSocketSampler.How to test the change?
C++ unit tests —
NativeSocketSamplerHookTestinddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp— verifies thatsend_hook/recv_hook/write_hook/read_hookeach delegate to theinstalled
_orig_*pointer and return its value.JUnit integration tests in
ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/:NativeSocketEnabledTestNativeSocketDisabledTestNativeSocketEventFieldsTestNativeSocketEventThreadTesteventThreadpopulated with the calling threadNativeSocketStackTraceTestNativeSocketRemoteAddressTestremoteAddressinip:portformatNativeSocketSendRecvSeparateTestNativeSocketBytesAccuracyTestsum(weight × duration)unbiased estimator within toleranceNativeSocketRateLimitTestweight > 1on sampled eventsNativeSocketRestartTestNativeSocketUdpExcludedTestsendto/recvfrom) produces zero eventsNativeSocketMacOsNoOpTestSpec: #487
For Datadog employees
credentials of any kind, I've requested a review from
@DataDog/security-design-and-guidance.