Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 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
4431f5f
NetFX-Stack-Capture Added dedicated canary thread; made cont. profile…
eftiquar Dec 10, 2025
7cfdce3
Merge latest chnages from main
eftiquar Dec 10, 2025
7023703
NetFX-Stack-Capture - fix compile time error missing method in contex…
eftiquar Dec 10, 2025
68b8c4c
NetFX-Stack-Capture exclude windows specific stack capture macihnery …
eftiquar Dec 10, 2025
2128207
NetFX-Stack-Capture guard windoes specific code behind win32 macro
eftiquar Dec 11, 2025
0e9cc95
NetFX-Stack-Capture - _M_AMD64 leaks into some linux biulds leading t…
eftiquar Dec 11, 2025
cd82beb
Merge remote-tracking branch 'upstream/main' into NetFX-Stack-Capture
eftiquar Dec 12, 2025
70d5b44
Merge branch 'main' into NetFX-Stack-Capture
Kielek Dec 12, 2025
c910961
build fix after merge with main
Kielek Dec 12, 2025
730adff
Merge branch 'main' into NetFX-Stack-Capture
Kielek Dec 12, 2025
62c8e06
Do not push managed thread id as a thread name
Kielek Dec 12, 2025
e924187
remove unused parameter
Kielek Dec 12, 2025
8c20fbe
Uncomment log
Kielek Dec 12, 2025
d343ed3
Merge branch 'main' into NetFX-Stack-Capture
Kielek Dec 15, 2025
4af041b
Fix CA1840
Kielek Dec 15, 2025
546923a
Fix CA2263
Kielek Dec 15, 2025
32dc3bf
Update src/OpenTelemetry.AutoInstrumentation.Native/OpenTelemetry.Aut…
eftiquar Dec 15, 2025
54ce58d
NetFX-Stack-Capture- make cont profiler singleton, multi appdomain s…
eftiquar Dec 16, 2025
0ff2e6c
NetFX-Stack-Capture handl multi app domain by creating canary thread …
eftiquar Dec 17, 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_x64.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
138 changes: 74 additions & 64 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 @@ -330,9 +330,7 @@ void ThreadSamplesBuffer::WriteSpanContext(const thread_span_context& span_conte
WriteUInt64(span_context.span_id_);
}

void ThreadSamplesBuffer::StartSample(ThreadID id,
const ThreadState* state,
const thread_span_context& span_context) const
void ThreadSamplesBuffer::StartSample(const ThreadState* state, const thread_span_context& span_context) const
{
CHECK_SAMPLES_BUFFER_LENGTH()
WriteByte(kThreadSamplesStartSample);
Expand Down Expand Up @@ -553,7 +551,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 +581,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 @@ -783,30 +781,35 @@ 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 frameProcessor = [&threadStacksBuffer, prof](StackSnapshotCallbackContext* snapshot_context) -> HRESULT
{
trace::Logger::Debug("DoStackSnapshot failed. HRESULT=0x", std::setfill('0'), std::setw(8), std::hex,
snapshotHr);
}
auto thread = snapshot_context->threadId;
DoStackSnapshotParams doStackSnapshotParams{prof, &threadStacksBuffer[thread]};
FrameCallback(snapshot_context->functionId, snapshot_context->instructionPointer,
snapshot_context->frameInfo, snapshot_context->contextSize, snapshot_context->context,
&doStackSnapshotParams);
return S_OK;
};

StackSnapshotCallbackContext context{frameProcessor};
stackCaptureStrategy->CaptureStacks(selectedThreads, &context);
}
}

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 +829,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 @@ -867,7 +870,7 @@ static void ResolveSymbolsAndPublishBufferForAllThreads(
thread_span_context spanContext = GetContext(threadId);
const auto threadState = GetThreadState(prof->managed_tid_to_state_, threadId);

prof->cur_cpu_writer_->StartSample(threadId, threadState, spanContext);
prof->cur_cpu_writer_->StartSample(threadState, spanContext);

if (prof->selectedThreadsSamplingInterval.has_value())
{
Expand Down Expand Up @@ -949,7 +952,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 +1013,33 @@ static void PauseClrAndCaptureSamples(ContinuousProfiler*

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

HRESULT hr = info12->SuspendRuntime();

if (FAILED(hr))
try
{
trace::Logger::Warn("Could not suspend runtime to sample threads. HRESULT=0x", std::setfill('0'), std::setw(8),
std::hex, hr);
}
else
{
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 +1102,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 +1143,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 +1169,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 +1264,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 +1363,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 +1406,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 +1428,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