Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bfdd675
NetFX-Stack-Capture Adding support for capturing NetFx call stacks
eftiquar Nov 7, 2025
6acc2ff
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 7, 2025
54b0721
NetFX-Stack-Capture - use logger machinery; remove excessive debug lo…
eftiquar Nov 18, 2025
6e99887
Merge branch 'NetFX-Stack-Capture' of github.com:eftiquar/opentelemet…
eftiquar Nov 18, 2025
18eb604
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 18, 2025
058e872
NetFX-Stack-Capture merge main
eftiquar Nov 18, 2025
ee32a92
NetFX-Stack-Capture removed unused variable, annotated the stack seed…
eftiquar Nov 19, 2025
f58ea70
NetFX-Stack-Capture add logs for critical failures; removed unused f…
eftiquar Nov 19, 2025
80e89d0
NetFX-Stack-Capture - format native code as per the workflow requirem…
eftiquar Nov 19, 2025
af7c764
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 19, 2025
5a3467f
Merge branch 'main' into NetFX-Stack-Capture
Kielek Nov 20, 2025
9171ea2
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 20, 2025
c57c5c2
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 21, 2025
c7cd58b
NetFX-Stack-Capture - enable netfx sampling tests
eftiquar Nov 21, 2025
b7ee8a8
NetFX-Stack-Capture adding test to verify empty allocation samples ar…
eftiquar Nov 21, 2025
0b0b446
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Nov 21, 2025
9c0a658
Merge branch 'main' into NetFX-Stack-Capture
eftiquar Dec 3, 2025
eb8d0e1
NetFX-Stack-Capture - enable fondational stack capture tests for net fx
eftiquar Dec 4, 2025
6b418d1
NetFX-Stack-Capture fix compilation error on Linux
eftiquar Dec 4, 2025
c92448a
NetFX-Stack-Capture add app.config
eftiquar Dec 4, 2025
49643db
Merge branch 'main' into NetFX-Stack-Capture
Kielek Dec 4, 2025
efe4084
Fix endpoint for mock collector profiles
Kielek Dec 4, 2025
62434db
Avoid inlining, simple methods in .NET Fx
Kielek Dec 4, 2025
2325c01
Fix DefaultDllImportSearchPaths
Kielek Dec 4, 2025
bf8fc66
restore comment
Kielek Dec 4, 2025
a8403a1
add missing conditional compilation
Kielek Dec 4, 2025
39836ad
Build SelectiveSampler tests .NET Fx4.6.2
Kielek Dec 4, 2025
ec02c21
Execute SelectiveSamplerTests.ExportThreadSamples on .NET Fx
Kielek Dec 4, 2025
e07e96c
Merge branch 'main' into NetFX-Stack-Capture
Kielek Dec 5, 2025
8b6f2e3
revert debugging changes
Kielek Dec 5, 2025
0777a58
Fix selective sampler for .NET Framework
Kielek Dec 5, 2025
ac55229
typo fix
Kielek Dec 5, 2025
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 @@ -160,6 +160,7 @@ add_library("OpenTelemetry.AutoInstrumentation.Native.static" STATIC
member_resolver.cpp
metadata_builder.cpp
miniutf.cpp
stack_capture_strategy_factory.cpp
regex_utils.cpp
string_utils.cpp
util.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
<ClInclude Include="continuous_profiler.h" />
<ClInclude Include="cor_profiler.h" />
<ClInclude Include="cor_profiler_base.h" />
<ClInclude Include="dot_net_stack_capture_strategy.h" />
<ClInclude Include="environment_variables.h" />
<ClInclude Include="environment_variables_parser.h" />
<ClInclude Include="environment_variables_util.h" />
Expand All @@ -196,13 +197,17 @@
<ClInclude Include="miniutfdata.h" />
<ClInclude Include="module_metadata.h" />
<ClInclude Include="netfx_assembly_redirection.h" />
<ClInclude Include="netfx_stack_capture_strategy.h" />
<ClInclude Include="otel_profiler_constants.h" />
<ClInclude Include="pal.h" />
<ClInclude Include="regex_utils.h" />
<ClInclude Include="profiler_stack_capture.h" />
<ClInclude Include="rejit_handler.h" />
<ClInclude Include="rejit_preprocessor.h" />
<ClInclude Include="rejit_work_offloader.h" />
<ClInclude Include="signature_builder.h" />
<ClInclude Include="stack_capture_strategy.h" />
<ClInclude Include="stack_capture_strategy_factory.h" />
<ClInclude Include="startup_hook.h" />
<ClInclude Include="stats.h" />
<ClInclude Include="string_utils.h" />
Expand All @@ -228,9 +233,11 @@
<ClCompile Include="method_rewriter.cpp" />
<ClCompile Include="miniutf.cpp" />
<ClCompile Include="regex_utils.cpp" />
<ClCompile Include="profiler_stack_capture.cpp" />
<ClCompile Include="rejit_handler.cpp" />
<ClCompile Include="rejit_preprocessor.cpp" />
<ClCompile Include="rejit_work_offloader.cpp" />
<ClCompile Include="stack_capture_strategy_factory.cpp" />
<ClCompile Include="string_utils.cpp" />
<ClCompile Include="stub_generator.cpp" />
<ClCompile Include="tracer_tokens.cpp" />
Expand Down
152 changes: 91 additions & 61 deletions src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ static std::mutex name_cache_lock = std::mutex();

static std::shared_mutex profiling_lock = std::shared_mutex();

static ICorProfilerInfo12* profiler_info; // After feature sets settle down, perhaps this should be refactored and have
// a single static instance of ThreadSampler
static ICorProfilerInfo7* profiler_info; // After feature sets settle down, perhaps this should be refactored and have
// a single static instance of ThreadSampler

// Dirt-simple back pressure system to save overhead if managed code is not reading fast enough
bool ThreadSamplingShouldProduceThreadSample()
Expand Down Expand Up @@ -336,7 +336,14 @@ void ThreadSamplesBuffer::StartSample(ThreadID id,
{
CHECK_SAMPLES_BUFFER_LENGTH()
WriteByte(kThreadSamplesStartSample);
WriteString(state->thread_name_);
if (state->thread_name_.empty())
{
WriteString(trace::ToWSTRING(std::to_string(id)));
}
else
{
WriteString(state->thread_name_);
}
Comment on lines +339 to +346
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? ThreadId != ThreadName. If it is not available, it should be just empty.
Was it for debugging purpises?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on .Net thread-pool threads have names on net fx that is not guaranteed. We need to produce unique list of threads that have context. Thead ID is guaranteed to be always present.

WriteSpanContext(span_context);
// Feature possibilities: (managed/native) thread priority, cpu/wait times, etc.
}
Expand Down Expand Up @@ -553,7 +560,7 @@ void NamingHelper::ClearFunctionIdentifierCache()
mdToken function_token = 0;
// theoretically there is a possibility to use GetFunctionInfo method, but it does not support generic methods
const HRESULT hr =
info12_->GetFunctionInfo2(func_id, frame_info, nullptr, &module_id, &function_token, 0, nullptr, nullptr);
info7_->GetFunctionInfo2(func_id, frame_info, nullptr, &module_id, &function_token, 0, nullptr, nullptr);
if (FAILED(hr))
{
trace::Logger::Debug("GetFunctionInfo2 failed. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex, hr);
Expand Down Expand Up @@ -583,8 +590,8 @@ void NamingHelper::GetFunctionName(FunctionIdentifier function_identifier, trace
}

ComPtr<IMetaDataImport2> metadata_import;
HRESULT hr = info12_->GetModuleMetaData(function_identifier.module_id, ofRead, IID_IMetaDataImport2,
reinterpret_cast<IUnknown**>(&metadata_import));
HRESULT hr = info7_->GetModuleMetaData(function_identifier.module_id, ofRead, IID_IMetaDataImport2,
reinterpret_cast<IUnknown**>(&metadata_import));
if (FAILED(hr))
{
trace::Logger::Debug("GetModuleMetaData failed. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex, hr);
Expand Down Expand Up @@ -730,6 +737,16 @@ struct DoStackSnapshotParams
DoStackSnapshotParams(ContinuousProfiler* p, std::vector<FunctionIdentifier>* b) : prof(p), buffer(b) {}
};

struct DoStackSnapshotParamsEx : DoStackSnapshotParams
{
using ThreadStacks = std::unordered_map<ThreadID, std::vector<FunctionIdentifier>>;
ThreadStacks* threadStacksBuffer;
DoStackSnapshotParamsEx(ContinuousProfiler* p, std::vector<FunctionIdentifier>* b, ThreadStacks* t)
: DoStackSnapshotParams(p, b), threadStacksBuffer(t)
{
}
};

static HRESULT __stdcall FrameCallback(_In_ FunctionID func_id,
_In_ UINT_PTR ip,
_In_ COR_PRF_FRAME_INFO frame_info,
Expand Down Expand Up @@ -783,30 +800,36 @@ static HRESULT __stdcall FrameCallback(_In_ FunctionID func_id,

static void CaptureFunctionIdentifiersForThreads(
ContinuousProfiler* prof,
ICorProfilerInfo12* info12,
ICorProfilerInfo7* info7,
const std::unordered_set<ThreadID>& selectedThreads,
std::unordered_map<ThreadID, std::vector<FunctionIdentifier>>& threadStacksBuffer)
{
prof->helper.ClearFunctionIdentifierCache();
for (auto threadId : selectedThreads)

if (auto stackCaptureStrategy = prof->GetStackCaptureStrategy(); stackCaptureStrategy != nullptr)
{
DoStackSnapshotParams doStackSnapshotParams(prof, &threadStacksBuffer[threadId]);
HRESULT snapshotHr = info12->DoStackSnapshot(threadId, &FrameCallback, COR_PRF_SNAPSHOT_DEFAULT,
&doStackSnapshotParams, nullptr, 0);
if (FAILED(snapshotHr))
auto callBackRaw = [](FunctionID func_id, UINT_PTR ip, COR_PRF_FRAME_INFO frame_info, ULONG32 context_size,
BYTE context[], void* client_data) -> HRESULT
{
trace::Logger::Debug("DoStackSnapshot failed. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex,
snapshotHr);
}
auto params = static_cast<continuous_profiler::StackSnapshotCallbackParams*>(client_data);
auto thread = params->threadId;
auto doStackSnapshotParams = static_cast<DoStackSnapshotParamsEx*>(params->clientData);
doStackSnapshotParams->buffer = &((*doStackSnapshotParams->threadStacksBuffer)[thread]);
FrameCallback(func_id, ip, frame_info, context_size, context, doStackSnapshotParams);
return S_OK;
};
DoStackSnapshotParamsEx doStackSnapshotParamsEx(prof, nullptr, &threadStacksBuffer);
StackSnapshotCallbackParams params{callBackRaw, &doStackSnapshotParamsEx};
stackCaptureStrategy->CaptureStacks(selectedThreads, &params);
}
}

static std::unordered_set<ThreadID> EnumerateThreads(ICorProfilerInfo12* info12)
static std::unordered_set<ThreadID> EnumerateThreads(ICorProfilerInfo7* info7)
{
std::unordered_set<ThreadID> threads;

ICorProfilerThreadEnum* thread_enum = nullptr;
HRESULT hr = info12->EnumThreads(&thread_enum);
HRESULT hr = info7->EnumThreads(&thread_enum);
if (FAILED(hr))
{
trace::Logger::Debug("Could not EnumThreads. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex, hr);
Expand All @@ -826,7 +849,7 @@ static void ResolveFrames(ContinuousProfiler* prof,
const std::vector<FunctionIdentifier>& threadStack,
ThreadSamplesBuffer& buffer)
{
for (auto functionIdentifier : threadStack)
for (const auto& functionIdentifier : threadStack)
{
const trace::WSTRING* name = prof->helper.Lookup(functionIdentifier, prof->stats_);
// This is where line numbers could be calculated
Expand Down Expand Up @@ -949,7 +972,7 @@ static void RemoveOutdatedEntries(std::unordered_map<trace_context, long long>&
}

static void PauseClrAndCaptureSamples(ContinuousProfiler* prof,
ICorProfilerInfo12* info12,
ICorProfilerInfo7* info7,
const SamplingType samplingType,
std::unordered_map<ThreadID, std::vector<FunctionIdentifier>>& threadStacksBuffer)
{
Expand Down Expand Up @@ -1010,52 +1033,33 @@ static void PauseClrAndCaptureSamples(ContinuousProfiler*

const auto start = std::chrono::steady_clock::now();

HRESULT hr = info12->SuspendRuntime();

if (FAILED(hr))
{
trace::Logger::Warn("Could not suspend runtime to sample threads. HRESULT=0x", std::setfill('0'), std::setw(8),
std::hex, hr);
}
else
try
{
try
{

if (samplingType == SamplingType::Continuous)
{
auto allThreads = EnumerateThreads(info12);
CaptureFunctionIdentifiersForThreads(prof, info12, allThreads, threadStacksBuffer);
}
else if (samplingType == SamplingType::SelectedThreads)
{
CaptureFunctionIdentifiersForThreads(prof, info12, selective_sampling_thread_buffer,
threadStacksBuffer);
}
}
catch (const std::exception& e)
if (samplingType == SamplingType::Continuous)
{
trace::Logger::Warn("Could not capture thread samples: ", e.what());
auto allThreads = EnumerateThreads(info7);
CaptureFunctionIdentifiersForThreads(prof, info7, allThreads, threadStacksBuffer);
}
catch (...)
else if (samplingType == SamplingType::SelectedThreads)
{
trace::Logger::Warn("Could not capture thread sample for unknown reasons");
CaptureFunctionIdentifiersForThreads(prof, info7, selective_sampling_thread_buffer, threadStacksBuffer);
}
}
// I don't have any proof but I sure hope that if suspending fails then it's still ok to ask to resume, with no
// ill effects
hr = info12->ResumeRuntime();
catch (const std::exception& e)
{
trace::Logger::Warn("Could not capture thread samples: ", e.what());
}
catch (...)
{
trace::Logger::Warn("Could not capture thread sample for unknown reasons");
}

const auto end = std::chrono::steady_clock::now();
const auto elapsed_micros = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

prof->stats_.micros_suspended = static_cast<int>(elapsed_micros);

if (FAILED(hr))
{
trace::Logger::Error("Could not resume runtime? HRESULT=0x", std::setfill('0'), std::setw(8), std::hex, hr);
}

const size_t nonEmptyCount = std::count_if(threadStacksBuffer.begin(), threadStacksBuffer.end(),
[](const std::pair<const ThreadID, std::vector<FunctionIdentifier>>& v)
{ return !v.second.empty(); });
Expand Down Expand Up @@ -1118,9 +1122,9 @@ static bool ShouldTrackIterations(const ContinuousProfiler* const prof)

static void SamplingThreadMain(ContinuousProfiler* prof)
{
ICorProfilerInfo12* info12 = prof->info12;
ICorProfilerInfo7* info7 = prof->info7;

info12->InitializeCurrentThread();
info7->InitializeCurrentThread();

std::unordered_map<ThreadID, std::vector<FunctionIdentifier>> threadStacksBuffer;
unsigned int iteration = 0;
Expand Down Expand Up @@ -1159,7 +1163,7 @@ static void SamplingThreadMain(ContinuousProfiler* prof)
iteration = 0;
}

PauseClrAndCaptureSamples(prof, info12, samplingType, threadStacksBuffer);
PauseClrAndCaptureSamples(prof, info7, samplingType, threadStacksBuffer);

if (prof->IsShutdownRequested())
{
Expand All @@ -1185,11 +1189,28 @@ static void SamplingThreadMain(ContinuousProfiler* prof)
}
}

void ContinuousProfiler::SetGlobalInfo7(ICorProfilerInfo7* cor_profiler_info7)
{
info7 = cor_profiler_info7;
this->helper.info7_ = cor_profiler_info7;
profiler_info = cor_profiler_info7;
}

void ContinuousProfiler::SetGlobalInfo12(ICorProfilerInfo12* cor_profiler_info12)
{
profiler_info = cor_profiler_info12;
this->info12 = cor_profiler_info12;
this->helper.info12_ = cor_profiler_info12;
// ICorProfilerInfo12 derives from ICorProfilerInfo7, so we can use it as ICorProfilerInfo7
SetGlobalInfo7(cor_profiler_info12);
info12 = cor_profiler_info12;
}

void ContinuousProfiler::SetStackCaptureStrategy(IStackCaptureStrategy* stack_capture_strategy)
{
stack_capture_strategy_ = stack_capture_strategy;
}

IStackCaptureStrategy* ContinuousProfiler::GetStackCaptureStrategy() const
{
return stack_capture_strategy_;
}

void ContinuousProfiler::InitSelectiveSamplingBuffer()
Expand Down Expand Up @@ -1263,8 +1284,8 @@ constexpr auto AllocationTickV4SizeWithoutTypeName = 4 + 4 + 2 + 8 + EtwPoint
static void CaptureAllocationStack(ContinuousProfiler* prof, std::vector<FunctionIdentifier>& threadStack)
{
DoStackSnapshotParams doStackSnapshotParams(prof, &threadStack);
HRESULT hr = prof->info12->DoStackSnapshot((ThreadID)NULL, &FrameCallback, COR_PRF_SNAPSHOT_DEFAULT,
&doStackSnapshotParams, nullptr, 0);
HRESULT hr = prof->info7->DoStackSnapshot((ThreadID)NULL, &FrameCallback, COR_PRF_SNAPSHOT_DEFAULT,
&doStackSnapshotParams, nullptr, 0);
if (FAILED(hr))
{
trace::Logger::Debug("DoStackSnapshot failed. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex, hr);
Expand Down Expand Up @@ -1362,7 +1383,7 @@ void ContinuousProfiler::AllocationTick(ULONG dataLen, LPCBYTE data)
size_t typeNameCharLen = (dataLen - AllocationTickV4SizeWithoutTypeName) / 2 - 1;

ThreadID threadId;
const HRESULT hr = info12->GetCurrentThreadID(&threadId);
const HRESULT hr = info7->GetCurrentThreadID(&threadId);
if (FAILED(hr))
{
trace::Logger::Debug("GetCurrentThreadId failed, ", hr);
Expand Down Expand Up @@ -1405,6 +1426,11 @@ void ContinuousProfiler::AllocationTick(ULONG dataLen, LPCBYTE data)

void ContinuousProfiler::StartAllocationSampling(const unsigned int maxMemorySamplesPerMinute)
{
if (!info12) // no info12 - we are on .Net Fx - ignore allocation sampling request
{
trace::Logger::Warn("Ignore Allocation Sampling request, it is not supported for .Net Framework applications");
return;
}
this->allocationSubSampler = std::make_unique<AllocationSubSampler>(maxMemorySamplesPerMinute, 60);

COR_PRF_EVENTPIPE_PROVIDER_CONFIG sessionConfig[] = {{WStr("Microsoft-Windows-DotNETRuntime"),
Expand All @@ -1422,6 +1448,10 @@ void ContinuousProfiler::StartAllocationSampling(const unsigned int maxMemorySam

void ContinuousProfiler::StopAllocationSampling()
{
if (!info12) // no info12 - we are on .Net Fx - ignore allocation sampling stop request
{
return;
}
if (session_ == 0)
{
return;
Expand Down
Loading
Loading