Skip to content
2 changes: 2 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@
X(JVMTI_STACKS_INIT_OK, "jvmti_stacks_init_ok") \
X(JVMTI_STACKS_INIT_FAILED, "jvmti_stacks_init_failed") \
X(JVMTI_STACKS_REQUESTED, "jvmti_stacks_requested") \
X(NATIVE_TRACE_HOOK_PREFIX_NOT_FOUND, "native_trace_hook_prefix_not_found") \
X(NATIVE_HOOK_MARK_RESOLVE_FAILED, "native_hook_mark_resolve_failed") \
X(JVMTI_STACKS_FAILED_WRONG_PHASE, "jvmti_stacks_failed_wrong_phase") \
X(JVMTI_STACKS_FAILED_OTHER, "jvmti_stacks_failed_other") \
/* Delegated stacks dropped at slot-lock. Rec-lock drops from all recording \
Expand Down
1 change: 1 addition & 0 deletions ddprof-lib/src/main/cpp/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum EventType {
EXECUTION_SAMPLE,
WALL_CLOCK_SAMPLE,
MALLOC_SAMPLE,
SOCKET_SAMPLE,
INSTRUMENTED_METHOD,
METHOD_TRACE,
ALLOC_SAMPLE,
Expand Down
37 changes: 21 additions & 16 deletions ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ inline EventType eventTypeFromBCI(jint bci_type) {
return PARK_SAMPLE;
case BCI_NATIVE_MALLOC:
return MALLOC_SAMPLE;
case BCI_NATIVE_SOCKET:
return SOCKET_SAMPLE;
default:
// For unknown or invalid BCI types, default to EXECUTION_SAMPLE
// This maintains backward compatibility and prevents undefined behavior
Expand Down Expand Up @@ -305,7 +307,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
bool anchor_recovery_used = false;

// Show extended frame types and stub frames for execution-type events
bool details = event_type <= MALLOC_SAMPLE || features.mixed;
bool details = event_type <= SOCKET_SAMPLE || features.mixed;

if (details && vm_thread != NULL && VMThread::isJavaThread(vm_thread)) {
anchor = vm_thread->anchor();
Expand Down Expand Up @@ -678,18 +680,15 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
} else {
// Resolve native frame (may use remote symbolication if enabled)
Profiler::NativeFrameResolution resolution = profiler->resolveNativeFrameForWalkVM((uintptr_t)pc, lock_index);
if (resolution.is_marked) {
// This is a marked C++ interpreter frame, terminate scan
break;
}
const char* method_name = resolution.method_name;
int frame_bci = resolution.bci;
char mark;
if (frame_bci != BCI_NATIVE_FRAME_REMOTE && method_name != NULL && (mark = NativeFunc::read_mark(method_name)) != 0) {
if (mark == MARK_ASYNC_PROFILER && event_type == MALLOC_SAMPLE) {
// Skip all internal frames above malloc_hook functions, leave the hook itself
if (resolution.is_marked()) {
if (resolution.mark == MARK_ASYNC_PROFILER &&
isHookPrefixedSample(event_type)) {
// Discard frames captured above the malloc/socket hook boundary,
// excluding the hook's own frame, and resume from the real
// caller above it — mirrors the FP/DWARF skip-prefix logic in
// Profiler::convertNativeTrace.
depth = 0;
} else if (mark == MARK_COMPILER_ENTRY && features.comp_task && vm_thread != NULL) {
} else if (resolution.mark == MARK_COMPILER_ENTRY && features.comp_task && vm_thread != NULL) {
// Insert current compile task as a pseudo Java frame
VMMethod* method = vm_thread->compiledMethod();
if (method != nullptr) {
Expand All @@ -698,13 +697,19 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
fillFrame(frames[depth++], FRAME_JIT_COMPILED, 0, method_id, method);
}
}
} else if (mark == MARK_THREAD_ENTRY) {
} else if (resolution.mark == MARK_THREAD_ENTRY) {
// Thread entry point detected via pre-computed mark - this is the root frame
// No need for expensive symbol resolution, just stop unwinding
Counters::increment(THREAD_ENTRY_MARK_DETECTIONS);
break;
} else {
// Other marks (VM runtime / interpreter) terminate the scan.
break;
}
} else if (method_name == NULL && details && !anchor_recovery_used
goto dwarf_unwind;
}
const char* method_name = resolution.method_name;
int frame_bci = resolution.bci;
if (method_name == NULL && details && !anchor_recovery_used
&& profiler->findLibraryByAddress(pc) == NULL) {
// Try anchor recovery — prefer live anchor, fall back to saved data
anchor_recovery_used = true;
Expand Down Expand Up @@ -1194,7 +1199,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) {
int java_frames = 0;
if (features.mixed) {
java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated);
} else if (request.event_type == BCI_NATIVE_MALLOC || request.event_type == BCI_NATIVE_SOCKET) {
} else if (isHookPrefixedSample(request.event_type)) {
if (cstack >= CSTACK_VM) {
java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated);
} else {
Expand Down
4 changes: 2 additions & 2 deletions ddprof-lib/src/main/cpp/jfrMetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ void JfrMetadata::initialize(
<< field("name", T_STRING, "Name")
<< field("count", T_LONG, "Count"))

<< (type("profiler.Malloc", T_MALLOC, "malloc")
<< category("Java Virtual Machine", "Native Memory")
<< (type("datadog.NativeMemoryAllocation", T_MALLOC, "Native Memory Allocation")
<< category("Datadog", "Profiling")
<< field("startTime", T_LONG, "Start Time", F_TIME_TICKS)
<< field("eventThread", T_THREAD, "Event Thread", F_CPOOL)
<< field("stackTrace", T_STACK_TRACE, "Stack Trace", F_CPOOL)
Expand Down
39 changes: 39 additions & 0 deletions ddprof-lib/src/main/cpp/nativeSocketSampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

#if defined(__linux__)

#include "codeCache.h"
#include "common.h"
#include "counters.h"
#include "flightRecorder.h"
#include "libraries.h"
#include "libraryPatcher.h"
#include "log.h"
#include "os.h"
Expand All @@ -27,6 +30,30 @@
static thread_local PoissonSampler _send_sampler;
static thread_local PoissonSampler _recv_sampler;

// Marks the hook wrapper's own symbol as MARK_ASYNC_PROFILER so native call-stack
// unwinding (Profiler::convertNativeTrace) can recognize the boundary between
// profiler-internal frames and the real caller, mirroring MallocHooker::initialize().
// Resolved by address (not by symbol-name predicate) because these are mangled
// C++ static member functions, unlike malloc_hook's extern "C" free functions.
// Returns false if the symbol could not be resolved/marked, in which case the
// hook boundary is never recognized and every socket sample's native stack
// comes back empty (see NATIVE_TRACE_HOOK_PREFIX_NOT_FOUND).
static bool markAsyncProfilerHook(void* fn_addr) {
CodeCache* lib = Libraries::instance()->findLibraryByAddress(fn_addr);
if (lib == nullptr) {
Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED);
return false;
}
const char* name = nullptr;
lib->binarySearch(fn_addr, &name);
if (name == nullptr) {
Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED);
return false;
}
NativeFunc::set_mark(name, MARK_ASYNC_PROFILER);
return true;
}

// Debug-only hook-fire counters, paired with TEST_LOG (common.h). Gated at
// compile time to keep release hot paths free of cross-thread atomic writes.
#ifdef DEBUG
Expand Down Expand Up @@ -386,6 +413,18 @@ Error NativeSocketSampler::start(Arguments &args) {
TEST_LOG("NativeSocketSampler::start interval_ticks=%ld tsc_freq=%llu",
init_interval, (unsigned long long)TSC::frequency());
#endif
bool hooks_marked = markAsyncProfilerHook((void*)&NativeSocketSampler::send_hook);
hooks_marked &= markAsyncProfilerHook((void*)&NativeSocketSampler::recv_hook);
hooks_marked &= markAsyncProfilerHook((void*)&NativeSocketSampler::write_hook);
hooks_marked &= markAsyncProfilerHook((void*)&NativeSocketSampler::read_hook);
if (!hooks_marked) {
// Not fatal: hooks are still installed and sampling still works, but
// native stacks for socket samples will come back empty because the
// hook boundary frame can't be recognized during unwinding.
Log::warn("NativeSocketSampler: failed to mark one or more hook symbols; "
"native call stacks for socket samples may be empty");
}

if (!LibraryPatcher::patch_socket_functions()) {
return Error("failed to install native socket hooks (dlsym returned NULL)");
}
Expand Down
55 changes: 44 additions & 11 deletions ddprof-lib/src/main/cpp/profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ int Profiler::getNativeTrace(void *ucontext, ASGCT_CallFrame *frames,
if (_cstack == CSTACK_NO ||
(event_type == BCI_ALLOC || event_type == BCI_ALLOC_OUTSIDE_TLAB) ||
(event_type != BCI_CPU && event_type != BCI_WALL &&
!isHookPrefixedSample(event_type) &&
_cstack == CSTACK_DEFAULT)) {
return 0;
}
Expand All @@ -318,7 +319,9 @@ int Profiler::getNativeTrace(void *ucontext, ASGCT_CallFrame *frames,
java_ctx, truncated);
}

return convertNativeTrace(native_frames, callchain, frames, lock_index);
bool skip_hook_prefix = isHookPrefixedSample(event_type);
return convertNativeTrace(native_frames, callchain, frames, lock_index,
skip_hook_prefix);
}

/**
Expand Down Expand Up @@ -375,24 +378,27 @@ Profiler::NativeFrameResolution Profiler::resolveNativeFrameForWalkVM(uintptr_t
char mark = (method_name != nullptr) ? NativeFunc::read_mark(method_name) : 0;

if (mark != 0) {
return {nullptr, BCI_NATIVE_FRAME, true}; // Marked - stop processing
return NativeFrameResolution(nullptr, BCI_NATIVE_FRAME, mark); // Marked - caller dispatches on mark
}

// Pack remote symbolication data using utility struct
uintptr_t pc_offset = pc - (uintptr_t)lib->imageBase();
uint32_t lib_index = (uint32_t)lib->libIndex();
unsigned long packed = RemoteFramePacker::pack(pc_offset, mark, lib_index);

return NativeFrameResolution(packed, BCI_NATIVE_FRAME_REMOTE, false);
return NativeFrameResolution(packed, BCI_NATIVE_FRAME_REMOTE);
}

// Traditional symbol resolution
const char *method_name = nullptr;
if (lib != nullptr) {
lib->binarySearch((void*)pc, &method_name);
}
if (method_name != nullptr && NativeFunc::is_marked(method_name)) {
return NativeFrameResolution(nullptr, BCI_NATIVE_FRAME, true);
if (method_name != nullptr) {
char mark = NativeFunc::read_mark(method_name);
if (mark != 0) {
return NativeFrameResolution(nullptr, BCI_NATIVE_FRAME, mark);
}
}

// No symbol but known library: pack for library-relative identification.
Expand All @@ -402,10 +408,10 @@ Profiler::NativeFrameResolution Profiler::resolveNativeFrameForWalkVM(uintptr_t
uintptr_t pc_offset = pc - (uintptr_t)lib->imageBase();
uint32_t lib_index = (uint32_t)lib->libIndex();
unsigned long packed = RemoteFramePacker::pack(pc_offset, 0, lib_index);
return NativeFrameResolution(packed, BCI_NATIVE_FRAME_REMOTE, false);
return NativeFrameResolution(packed, BCI_NATIVE_FRAME_REMOTE);
}

return NativeFrameResolution(method_name, BCI_NATIVE_FRAME, false);
return NativeFrameResolution(method_name, BCI_NATIVE_FRAME);
}

/**
Expand All @@ -418,9 +424,16 @@ Profiler::NativeFrameResolution Profiler::resolveNativeFrameForWalkVM(uintptr_t
* marked frames (JVM internals) that should terminate the stack walk.
*/
int Profiler::convertNativeTrace(int native_frames, const void **callchain,
ASGCT_CallFrame *frames, int lock_index) {
ASGCT_CallFrame *frames, int lock_index,
bool skip_hook_prefix) {
int depth = 0;
void* prev_identifier = NULL; // Can be jmethodID or frame pointer for remote
// skip_hook_prefix: the walk started inside profiler-internal code (e.g. the
// malloc/socket hook call chain), not at an interrupted user PC. Discard frames
// until the hook wrapper's own MARK_ASYNC_PROFILER-marked frame is reached, then
// resume normally from the real caller. Other mark kinds still terminate the
// scan immediately, same as the non-skipping case.
bool skipping = skip_hook_prefix;

for (int i = 0; i < native_frames; i++) {
uintptr_t pc = (uintptr_t)callchain[i];
Expand All @@ -436,9 +449,15 @@ int Profiler::convertNativeTrace(int native_frames, const void **callchain,
char mark = (method_name != nullptr) ? NativeFunc::read_mark(method_name) : 0;

if (mark != 0) {
if (skip_hook_prefix && mark == MARK_ASYNC_PROFILER) {
depth = 0;
skipping = false;
continue;
}
// Terminate scan at marked frame
return depth;
}
if (skipping) continue;

// Populate remote frame inline - no allocation needed!
// Pass the mark we already retrieved to avoid duplicate binarySearch
Expand All @@ -456,10 +475,19 @@ int Profiler::convertNativeTrace(int native_frames, const void **callchain,

// Fallback: Traditional symbol resolution
const char *method_name = findNativeMethod((void*)pc);
if (method_name != nullptr && NativeFunc::is_marked(method_name)) {
// Terminate scan at marked frame
return depth;
if (method_name != nullptr) {
char mark = NativeFunc::read_mark(method_name);
if (mark != 0) {
if (skip_hook_prefix && mark == MARK_ASYNC_PROFILER) {
depth = 0;
skipping = false;
continue;
}
// Terminate scan at marked frame
return depth;
}
}
if (skipping) continue;

// Store standard frame
jmethodID current_method = (jmethodID)method_name;
Expand All @@ -473,6 +501,11 @@ int Profiler::convertNativeTrace(int native_frames, const void **callchain,
}
}

if (skipping) {
// The hook-boundary (MARK_ASYNC_PROFILER) frame was never found in the
// callchain; every frame was discarded and the sample has no native stack.
Counters::increment(NATIVE_TRACE_HOOK_PREFIX_NOT_FOUND);
}
return depth;
}

Expand Down
43 changes: 31 additions & 12 deletions ddprof-lib/src/main/cpp/profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ __asm__(".symver exp,exp@GLIBC_2.17");
#endif
#endif

// A "hook-prefixed" sample (malloc, socket I/O, ...) is one whose native
// callchain starts inside the profiler's own PLT/libc hook and must be
// trimmed up to the MARK_ASYNC_PROFILER boundary frame before symbolication.
// Single source of truth for that predicate across both BCI_* (FP/DWARF path)
// and EventType (walkVM path) representations.
inline bool isHookPrefixedSample(jint bci) {
return bci == BCI_NATIVE_MALLOC || bci == BCI_NATIVE_SOCKET;
}

inline bool isHookPrefixedSample(EventType event_type) {
return event_type == MALLOC_SAMPLE || event_type == SOCKET_SAMPLE;
}

#ifdef DEBUG
#include <signal.h>
static const char* force_stackwalk_crash_env = getenv("DDPROF_FORCE_STACKWALK_CRASH");
Expand Down Expand Up @@ -330,12 +343,13 @@ class alignas(alignof(SpinLock)) Profiler {
* Bits 47-61: lib_index (15 bits, 32K libraries)
*. Bits 62-63: reserved
*
* Mark values indicate JVM internal frames that should terminate stack walks:
* Mark values identify JVM-internal frames the caller must dispatch on;
* they don't all mean "terminate the walk":
* 0 = no mark (regular native frame)
* MARK_VM_RUNTIME = 1
* MARK_INTERPRETER = 2
* MARK_COMPILER_ENTRY = 3
* MARK_ASYNC_PROFILER = 4
* MARK_VM_RUNTIME = 1 -- terminates the scan
* MARK_INTERPRETER = 2 -- terminates the scan
* MARK_COMPILER_ENTRY = 3 -- inserts a pseudo JIT-compile-task frame, then resumes unwinding
* MARK_ASYNC_PROFILER = 4 -- resets depth and resumes unwinding above the hook boundary
*
* During stack walking, we perform symbol resolution (binarySearch) to check
* marks and pack the mark value for later use. The performance is O(log n) for
Expand Down Expand Up @@ -389,20 +403,25 @@ class alignas(alignof(SpinLock)) Profiler {
struct NativeFrameResolution {
union {
unsigned long packed_remote_frame; // Packed remote frame data (pc_offset|mark|lib_index)
const char* method_name; // Resolved method name
const char* method_name; // Resolved method name
};
int bci; // BCI_NATIVE_FRAME_REMOTE or BCI_NATIVE_FRAME
bool is_marked; // true if this is a marked C++ interpreter frame (stop processing)
NativeFrameResolution(const char* name, int bci_type, bool marked)
: method_name(name), bci(bci_type), is_marked(marked) {}
NativeFrameResolution(unsigned long packed, int bci_type, bool marked)
: packed_remote_frame(packed), bci(bci_type), is_marked(marked) {}
char mark; // the Mark value for this frame, 0 if unmarked
// True if this frame carries a JVM-internal mark; caller must inspect
// `mark` to decide whether to terminate the walk or dispatch/resume
// (see the Mark values table above resolveNativeFrameForWalkVM).
bool is_marked() const { return mark != 0; }
NativeFrameResolution(const char* name, int bci_type, char mark_value = 0)
: method_name(name), bci(bci_type), mark(mark_value) {}
NativeFrameResolution(unsigned long packed, int bci_type, char mark_value = 0)
: packed_remote_frame(packed), bci(bci_type), mark(mark_value) {}
};

void populateRemoteFrame(ASGCT_CallFrame* frame, uintptr_t pc, CodeCache* lib, char mark);
NativeFrameResolution resolveNativeFrameForWalkVM(uintptr_t pc, int lock_index);
int convertNativeTrace(int native_frames, const void **callchain,
ASGCT_CallFrame *frames, int lock_index);
ASGCT_CallFrame *frames, int lock_index,
bool skip_hook_prefix);
bool recordSample(void *ucontext, u64 weight, int tid, jint event_type,
u64 call_trace_id, Event *event,
u64 *recorded_call_trace_id = nullptr);
Expand Down
Loading
Loading