From 719bddcccff9b4f4b04f8e871e67fbf8654776b6 Mon Sep 17 00:00:00 2001 From: Mateusz Lach Date: Thu, 23 Oct 2025 10:12:30 +0200 Subject: [PATCH 1/5] trace-based api --- .../continuous_profiler.cpp | 124 ++++++++++++------ .../continuous_profiler.h | 72 +++++++--- .../NativeMethods.cs | 34 +++-- .../IntegrationTests/SelectiveSamplerTests.cs | 9 +- .../thread_span_context_test.cpp | 48 +++++-- .../Plugins/FrequentSamplingProcessor.cs | 23 ++-- .../Program.cs | 9 +- .../Properties/launchSettings.json | 3 +- 8 files changed, 222 insertions(+), 100 deletions(-) diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp index 2042848777..4a2167c800 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp +++ b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp @@ -23,7 +23,7 @@ constexpr auto kMaxCodesPerBuffer = 10 * 1000; constexpr auto kSamplesBufferMaximumSize = 200 * 1024; constexpr auto kSamplesBufferDefaultSize = 20 * 1024; -constexpr auto kSelectiveSamplingMaxSpans = 50; +constexpr auto kSelectiveSamplingMaxTraces = 50; constexpr auto kSelectiveSamplingMaxAgeMinutes = 15; // If you change these, change ThreadSampler.cs too @@ -90,9 +90,9 @@ static std::mutex thread_span_context_lock; static continuous_profiler::ThreadSpanContextMap thread_span_context_map; // Keep timestamps as values to handle stalled spans -static std::mutex selective_sampling_lock; -static std::unordered_map selective_sampling_trace_map; -static std::unordered_set selective_sampling_thread_buffer; +static std::mutex selective_sampling_lock; +static std::unordered_map selective_sampling_trace_map; +static std::unordered_set selective_sampling_thread_buffer; static std::mutex name_cache_lock = std::mutex(); @@ -172,9 +172,14 @@ static void AppendToSelectedThreadsSampleBuffer(int32_t appendLen, unsigned char selective_sampling_buffer.insert(selective_sampling_buffer.end(), appendBuf, &appendBuf[appendLen]); } +bool continuous_profiler::trace_context::IsDefault() const +{ + return trace_id_high_ == 0 && trace_id_low_ == 0; +} + bool continuous_profiler::thread_span_context::IsDefault() const { - return trace_id_high_ == 0 && trace_id_low_ == 0 && span_id_ == 0; + return trace_context_.IsDefault() && span_id_ == 0; } void AllocationSamplingAppendToBuffer(int32_t appendLen, unsigned char* appendBuf) @@ -318,6 +323,13 @@ void ThreadSamplesBuffer::EndSelectedThreadsBatch() const WriteByte(kSelectedThreadsEndBatch); } +void ThreadSamplesBuffer::WriteSpanContext(const thread_span_context& span_context) const +{ + WriteUInt64(span_context.trace_context_.trace_id_high_); + WriteUInt64(span_context.trace_context_.trace_id_low_); + WriteUInt64(span_context.span_id_); +} + void ThreadSamplesBuffer::StartSample(ThreadID id, const ThreadState* state, const thread_span_context& span_context) const @@ -325,9 +337,7 @@ void ThreadSamplesBuffer::StartSample(ThreadID id, CHECK_SAMPLES_BUFFER_LENGTH() WriteByte(kThreadSamplesStartSample); WriteString(state->thread_name_); - WriteUInt64(span_context.trace_id_high_); - WriteUInt64(span_context.trace_id_low_); - WriteUInt64(span_context.span_id_); + WriteSpanContext(span_context); // Feature possibilities: (managed/native) thread priority, cpu/wait times, etc. } @@ -338,9 +348,7 @@ void ThreadSamplesBuffer::StartSampleForSelectedThread(const ThreadState* WriteByte(kSelectedThreadSample); WriteCurrentTimeMillis(); WriteString(state->thread_name_); - WriteUInt64(span_context.trace_id_high_); - WriteUInt64(span_context.trace_id_low_); - WriteUInt64(span_context.span_id_); + WriteSpanContext(span_context); } void ThreadSamplesBuffer::MarkSelectedForFrequentSampling(bool value) const @@ -363,9 +371,7 @@ void ThreadSamplesBuffer::AllocationSample(uint64_t allocSize, WriteUInt64(allocSize); WriteString(allocType, allocTypeCharLen); WriteString(state->thread_name_); - WriteUInt64(span_context.trace_id_high_); - WriteUInt64(span_context.trace_id_low_); - WriteUInt64(span_context.span_id_); + WriteSpanContext(span_context); } void ThreadSamplesBuffer::RecordFrame(const FunctionIdentifier& fid, const trace::WSTRING& frame) @@ -476,6 +482,18 @@ void ThreadSpanContextMap::Remove(const thread_span_context& spanContext) thread_span_context_map.erase(threadId); } span_context_thread_map.erase(spanContext); + + const auto foundByTraceContext = trace_active_span_map.find(spanContext.trace_context_); + if (foundByTraceContext == trace_active_span_map.end()) + { + return; // nothing to remove + } + auto& traceActiveSpans = foundByTraceContext->second; + traceActiveSpans.erase(spanContext); + if (traceActiveSpans.empty()) + { + trace_active_span_map.erase(spanContext.trace_context_); + } } void ThreadSpanContextMap::Remove(ThreadID threadId) @@ -485,6 +503,7 @@ void ThreadSpanContextMap::Remove(ThreadID threadId) { return; // nothing to remove } + const auto spanContext = foundByThreadId->second; const auto foundBySpanContext = span_context_thread_map.find(spanContext); if (foundBySpanContext != span_context_thread_map.end()) @@ -492,6 +511,7 @@ void ThreadSpanContextMap::Remove(ThreadID threadId) auto& threadIds = foundBySpanContext->second; threadIds.erase(threadId); } + thread_span_context_map.erase(threadId); } @@ -506,24 +526,43 @@ void ThreadSpanContextMap::Put(ThreadID threadId, const thread_span_context& cur span_context_thread_map[previousContext].erase(threadId); } } + thread_span_context_map[threadId] = currentSpanContext; - if (!currentSpanContext.IsDefault()) + if (currentSpanContext.IsDefault()) { - span_context_thread_map[currentSpanContext].insert(threadId); + return; } + + span_context_thread_map[currentSpanContext].insert(threadId); + + // Also note of possibly new activity, store it in trace map. + // This is a noop if already present. + trace_active_span_map[currentSpanContext.trace_context_].insert(currentSpanContext); } -const std::unordered_set* ThreadSpanContextMap::Get(const thread_span_context& spanContext) +void ThreadSpanContextMap::GetAllThreads(const trace_context traceContext, std::unordered_set& buffer) { - const auto iterator = span_context_thread_map.find(spanContext); - if (iterator == span_context_thread_map.end()) + const auto traceSpans = trace_active_span_map.find(traceContext); + if (traceSpans == trace_active_span_map.end()) + { + return; + } + + const auto& spanContexts = traceSpans->second; + for (const auto& spanContext : spanContexts) { - return nullptr; + const auto foundBySpanContext = span_context_thread_map.find(spanContext); + if (foundBySpanContext == span_context_thread_map.end()) + { + continue; + } + + const auto& threadIds = foundBySpanContext->second; + buffer.insert(threadIds.begin(), threadIds.end()); } - return &iterator->second; } -std::optional ThreadSpanContextMap::Get(ThreadID threadId) +std::optional ThreadSpanContextMap::GetContext(ThreadID threadId) { const auto iterator = thread_span_context_map.find(threadId); if (iterator == thread_span_context_map.end()) @@ -858,7 +897,7 @@ static ThreadState* GetThreadState(const std::unordered_mapselectedThreadsSamplingInterval.has_value()) { const auto threadSelectedForFrequentSampling = - selective_sampling_trace_map.find(spanContext) != selective_sampling_trace_map.end(); + selective_sampling_trace_map.find(spanContext.trace_context_) != selective_sampling_trace_map.end(); prof->cur_cpu_writer_->MarkSelectedForFrequentSampling(threadSelectedForFrequentSampling); } @@ -924,7 +963,7 @@ static void ResolveSymbolsAndPublishBufferForSelectedThreads( AppendToSelectedThreadsSampleBuffer(static_cast(localBytes.size()), localBytes.data()); } -static void RemoveOutdatedEntries(std::unordered_map& selectiveSamplingTraceSet) +static void RemoveOutdatedEntries(std::unordered_map& selectiveSamplingTraceSet) { const auto now = std::chrono::steady_clock::now(); @@ -936,9 +975,9 @@ static void RemoveOutdatedEntries(std::unordered_map deadline) { - trace::Logger::Warn("SelectiveSampling: removing outdated entry for span {", + trace::Logger::Warn("SelectiveSampling: removing outdated entry for trace {", "traceIdHigh: ", it->first.trace_id_high_, ", traceIdLow: ", it->first.trace_id_low_, - ", spanId: ", it->first.span_id_, "} because it was enqueued more than ", + "} because it was enqueued more than ", kSelectiveSamplingMaxAgeMinutes, " minutes ago"); it = selectiveSamplingTraceSet.erase(it); } @@ -983,13 +1022,9 @@ static void PauseClrAndCaptureSamples(ContinuousProfiler* } selective_sampling_thread_buffer.clear(); - for (const auto& spanContext : selective_sampling_trace_map) + for (const auto& [traceContext, _] : selective_sampling_trace_map) { - const auto threadsForSpanContext = thread_span_context_map.Get(spanContext.first); - if (threadsForSpanContext != nullptr) - { - selective_sampling_thread_buffer.insert(threadsForSpanContext->begin(), threadsForSpanContext->end()); - } + thread_span_context_map.GetAllThreads(traceContext, selective_sampling_thread_buffer); } if (selective_sampling_thread_buffer.empty()) { @@ -1192,7 +1227,7 @@ void ContinuousProfiler::SetGlobalInfo12(ICorProfilerInfo12* cor_profiler_info12 void ContinuousProfiler::InitSelectiveSamplingBuffer() { selective_sampling_buffer.reserve(kSamplesBufferDefaultSize); - selective_sampling_thread_buffer.reserve(kSelectiveSamplingMaxSpans); + selective_sampling_thread_buffer.reserve(kSelectiveSamplingMaxTraces); } void ContinuousProfiler::StartThreadSampling() @@ -1544,6 +1579,9 @@ extern "C" } EXPORTTHIS void ContinuousProfilerSetNativeContext(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId) { + // This method is called anytime thread-span association changes, e.g. when activity is started/stopped or + // when suspension/resumption occurs. + ThreadID threadId; const HRESULT hr = profiler_info->GetCurrentThreadID(&threadId); if (FAILED(hr)) @@ -1554,28 +1592,28 @@ extern "C" } std::lock_guard guard(thread_span_context_lock); - continuous_profiler::thread_span_context newSpanContext = {traceIdHigh, traceIdLow, spanId}; + const continuous_profiler::thread_span_context newSpanContext = {traceIdHigh, traceIdLow, spanId}; thread_span_context_map.Put(threadId, newSpanContext); } - EXPORTTHIS void SelectiveSamplingStart(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId) + EXPORTTHIS void SelectiveSamplingStart(uint64_t traceIdHigh, uint64_t traceIdLow) { if (profiler_info == nullptr) { return; } - continuous_profiler::thread_span_context context = {traceIdHigh, traceIdLow, spanId}; + const continuous_profiler::trace_context context = {traceIdHigh, traceIdLow}; if (context.IsDefault()) { return; } std::lock_guard guard(selective_sampling_lock); // Don't allow for too many samples at once - if (selective_sampling_trace_map.size() >= kSelectiveSamplingMaxSpans) + if (selective_sampling_trace_map.size() >= kSelectiveSamplingMaxTraces) { - trace::Logger::Warn("SelectiveSamplingStart: ignoring request to start sampling for span {", - "traceIdHigh: ", traceIdHigh, ", traceIdLow: ", traceIdLow, ", spanId: ", spanId, - "} because maximum number of spans is already being sampled."); + trace::Logger::Warn("SelectiveSamplingStart: ignoring request to start sampling for trace {", + "traceIdHigh: ", traceIdHigh, ", traceIdLow: ", traceIdLow, + "} because maximum number of traces is already being sampled."); return; } const auto now = @@ -1584,14 +1622,14 @@ extern "C" selective_sampling_trace_map[context] = now; } - EXPORTTHIS void SelectiveSamplingStop(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId) + EXPORTTHIS void SelectiveSamplingStop(uint64_t traceIdHigh, uint64_t traceIdLow) { if (profiler_info == nullptr) { return; } - continuous_profiler::thread_span_context context = {traceIdHigh, traceIdLow, spanId}; + const continuous_profiler::trace_context context = {traceIdHigh, traceIdLow}; if (context.IsDefault()) { return; diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.h b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.h index 26af8f60a0..ff78001261 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.h +++ b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.h @@ -32,8 +32,8 @@ extern "C" EXPORTTHIS int32_t SelectiveSamplerReadThreadSamples(int32_t len, unsigned char* buf); // ReSharper disable CppInconsistentNaming EXPORTTHIS void ContinuousProfilerSetNativeContext(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId); - EXPORTTHIS void SelectiveSamplingStart(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId); - EXPORTTHIS void SelectiveSamplingStop(uint64_t traceIdHigh, uint64_t traceIdLow, uint64_t spanId); + EXPORTTHIS void SelectiveSamplingStart(uint64_t traceIdHigh, uint64_t traceIdLow); + EXPORTTHIS void SelectiveSamplingStop(uint64_t traceIdHigh, uint64_t traceIdLow); // ReSharper restore CppInconsistentNaming } @@ -56,33 +56,56 @@ struct FunctionIdentifierResolveArgs FunctionID function_id; COR_PRF_FRAME_INFO frame_info; + FunctionIdentifierResolveArgs() = delete; + FunctionIdentifierResolveArgs(const FunctionID func_id, const COR_PRF_FRAME_INFO frame_info) + : function_id(func_id) + , frame_info(frame_info) + { + } bool operator==(const FunctionIdentifierResolveArgs& p) const { return function_id == p.function_id && frame_info == p.frame_info; } }; -class thread_span_context +struct trace_context { -public: uint64_t trace_id_high_; uint64_t trace_id_low_; - uint64_t span_id_; - thread_span_context() : trace_id_high_(0), trace_id_low_(0), span_id_(0) + trace_context(): trace_id_high_(0), trace_id_low_(0) { } - thread_span_context(uint64_t _traceIdHigh, uint64_t _traceIdLow, uint64_t _spanId) : - trace_id_high_(_traceIdHigh), trace_id_low_(_traceIdLow), span_id_(_spanId) + trace_context(const uint64_t trace_id_high, const uint64_t trace_id_low) + : trace_id_high_(trace_id_high) + , trace_id_low_(trace_id_low) { } - thread_span_context(thread_span_context const& other) : - trace_id_high_(other.trace_id_high_), trace_id_low_(other.trace_id_low_), span_id_(other.span_id_) + + bool operator==(const trace_context& p) const { + return trace_id_low_ == p.trace_id_low_ && trace_id_high_ == p.trace_id_high_; } + [[nodiscard]] bool IsDefault() const; +}; + +class thread_span_context +{ +public: + trace_context trace_context_; + uint64_t span_id_; + + thread_span_context() : span_id_(0) + { + } + thread_span_context(uint64_t _traceIdHigh, uint64_t _traceIdLow, uint64_t _spanId) : + trace_context_(_traceIdHigh, _traceIdLow), span_id_(_spanId) + { + } + bool operator==(const thread_span_context& other) const { - return trace_id_high_ == other.trace_id_high_ && trace_id_low_ == other.trace_id_low_ && span_id_ == other.span_id_; + return trace_context_ == other.trace_context_ && span_id_ == other.span_id_; } bool operator!=(const thread_span_context& other) const { @@ -119,12 +142,21 @@ struct std::hash } }; +template <> +struct std::hash +{ + std::size_t operator()(const continuous_profiler::trace_context& k) const noexcept + { + return hash_combine(k.trace_id_high_, k.trace_id_low_); + } +}; + template <> struct std::hash { std::size_t operator()(const continuous_profiler::thread_span_context& k) const noexcept { - return hash_combine(k.trace_id_high_, k.trace_id_low_, k.span_id_); + return hash_combine(k.trace_context_, k.span_id_); } }; @@ -171,6 +203,7 @@ class ThreadSamplesBuffer void StartBatch() const; void StartSelectedThreadsBatch() const; void EndSelectedThreadsBatch() const; + void WriteSpanContext(const thread_span_context& span_context) const; void StartSample(ThreadID id, const ThreadState* state, const thread_span_context& span_context) const; void StartSampleForSelectedThread(const ThreadState* state, const thread_span_context& span_context) const; void MarkSelectedForFrequentSampling(bool value) const; @@ -198,15 +231,16 @@ namespace continuous_profiler class ThreadSpanContextMap { public: - void Put(ThreadID threadId, const thread_span_context& currentSpanContext); - std::optional Get(ThreadID threadId); - const std::unordered_set* Get(const thread_span_context& spanContext); - void Remove(const thread_span_context& spanContext); - void Remove(ThreadID threadId); + void Put(ThreadID threadId, const thread_span_context& currentSpanContext); + std::optional GetContext(ThreadID threadId); + void GetAllThreads(trace_context traceContext, std::unordered_set& buffer); + void Remove(const thread_span_context& spanContext); + void Remove(ThreadID threadId); private: - std::unordered_map thread_span_context_map; - std::unordered_map> span_context_thread_map; + std::unordered_map thread_span_context_map; + std::unordered_map> span_context_thread_map; + std::unordered_map> trace_active_span_map; }; template class NameCache diff --git a/src/OpenTelemetry.AutoInstrumentation/NativeMethods.cs b/src/OpenTelemetry.AutoInstrumentation/NativeMethods.cs index 1bb8807717..27013b16cf 100644 --- a/src/OpenTelemetry.AutoInstrumentation/NativeMethods.cs +++ b/src/OpenTelemetry.AutoInstrumentation/NativeMethods.cs @@ -119,40 +119,50 @@ public static void ContinuousProfilerNotifySpanStopped(Activity activity) } } - public static void SelectiveSamplingStart(Activity activity) + public static void SelectiveSamplingStart(ActivityTraceId traceId) { - if (!TryParseSpanContext(activity, out var traceIdHigh, out var traceIdLow, out var spanId)) + if (!TryParseTraceContext(traceId, out var traceIdHigh, out var traceIdLow)) { return; } if (IsWindows) { - Windows.SelectiveSamplingStart(traceIdHigh, traceIdLow, spanId); + Windows.SelectiveSamplingStart(traceIdHigh, traceIdLow); } else { - NonWindows.SelectiveSamplingStart(traceIdHigh, traceIdLow, spanId); + NonWindows.SelectiveSamplingStart(traceIdHigh, traceIdLow); } } - public static void SelectiveSamplingStop(Activity activity) + public static void SelectiveSamplingStop(ActivityTraceId traceId) { - if (!TryParseSpanContext(activity, out var traceIdHigh, out var traceIdLow, out var spanId)) + if (!TryParseTraceContext(traceId, out var traceIdHigh, out var traceIdLow)) { return; } if (IsWindows) { - Windows.SelectiveSamplingStop(traceIdHigh, traceIdLow, spanId); + Windows.SelectiveSamplingStop(traceIdHigh, traceIdLow); } else { - NonWindows.SelectiveSamplingStop(traceIdHigh, traceIdLow, spanId); + NonWindows.SelectiveSamplingStop(traceIdHigh, traceIdLow); } } + private static bool TryParseTraceContext(ActivityTraceId currentActivityTraceId, out ulong traceIdHigh, out ulong traceIdLow) + { + traceIdLow = 0; + traceIdHigh = 0; + var hexTraceId = currentActivityTraceId.ToHexString(); + + return ulong.TryParse(hexTraceId.AsSpan(0, 16), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out traceIdHigh) && + ulong.TryParse(hexTraceId.AsSpan(16), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out traceIdLow); + } + private static bool TryParseSpanContext(Activity currentActivity, out ulong traceIdHigh, out ulong traceIdLow, out ulong spanId) { traceIdLow = 0; @@ -197,10 +207,10 @@ private static class Windows public static extern void ContinuousProfilerNotifySpanStopped(ulong traceIdHigh, ulong traceIdLow, ulong spanId); [DllImport("OpenTelemetry.AutoInstrumentation.Native.dll")] - public static extern void SelectiveSamplingStart(ulong traceIdHigh, ulong traceIdLow, ulong spanId); + public static extern void SelectiveSamplingStart(ulong traceIdHigh, ulong traceIdLow); [DllImport("OpenTelemetry.AutoInstrumentation.Native.dll")] - public static extern void SelectiveSamplingStop(ulong traceIdHigh, ulong traceIdLow, ulong spanId); + public static extern void SelectiveSamplingStop(ulong traceIdHigh, ulong traceIdLow); #endif } @@ -234,10 +244,10 @@ private static class NonWindows public static extern void ContinuousProfilerNotifySpanStopped(ulong traceIdHigh, ulong traceIdLow, ulong spanId); [DllImport("OpenTelemetry.AutoInstrumentation.Native")] - public static extern void SelectiveSamplingStart(ulong traceIdHigh, ulong traceIdLow, ulong spanId); + public static extern void SelectiveSamplingStart(ulong traceIdHigh, ulong traceIdLow); [DllImport("OpenTelemetry.AutoInstrumentation.Native")] - public static extern void SelectiveSamplingStop(ulong traceIdHigh, ulong traceIdLow, ulong spanId); + public static extern void SelectiveSamplingStop(ulong traceIdHigh, ulong traceIdLow); #endif } } diff --git a/test/IntegrationTests/SelectiveSamplerTests.cs b/test/IntegrationTests/SelectiveSamplerTests.cs index 50bbfa8464..c16912ad39 100644 --- a/test/IntegrationTests/SelectiveSamplerTests.cs +++ b/test/IntegrationTests/SelectiveSamplerTests.cs @@ -45,10 +45,17 @@ public void ExportThreadSamples() // Test app sleeps for 0.5s, sampling interval set to 0.05s Output.WriteLine($"Count: {threadSamples.Count}"); - Assert.InRange(threadSamples.Count, 7, 12); + Assert.InRange(threadSamples.Count, 7, 14); var threadNames = threadSamples.Select(sample => sample.ThreadName).Distinct(StringComparer.InvariantCultureIgnoreCase); + var uniqueSpanContextCount = threadSamples + .Select(ts => (ts.SpanId, ts.TraceIdHigh, ts.TraceIdLow)) + .Distinct() + .Count(); + + Assert.Equal(2, uniqueSpanContextCount); + Assert.Equal(2, threadNames.Count()); } diff --git a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp index b72da71623..a41fcf0342 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp +++ b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp @@ -7,7 +7,7 @@ TEST(ThreadSpanContextMapTest, BasicGet) const continuous_profiler::thread_span_context context = {1, 1, 1}; threadSpanContextMap.Put(1, context); - ASSERT_EQ(threadSpanContextMap.Get(1), context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), context); } TEST(ThreadSpanContextMapTest, BasicUpdate) @@ -16,10 +16,10 @@ TEST(ThreadSpanContextMapTest, BasicUpdate) const continuous_profiler::thread_span_context context = {1, 1, 1}; const continuous_profiler::thread_span_context other_context = {2, 2, 2}; threadSpanContextMap.Put(1, context); - ASSERT_EQ(threadSpanContextMap.Get(1), context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), context); threadSpanContextMap.Put(1, other_context); - ASSERT_EQ(threadSpanContextMap.Get(1), other_context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), other_context); } TEST(ThreadSpanContextMapTest, ConsistentUpdate) @@ -29,16 +29,36 @@ TEST(ThreadSpanContextMapTest, ConsistentUpdate) const continuous_profiler::thread_span_context other_context = {2, 2, 2}; threadSpanContextMap.Put(1, context); threadSpanContextMap.Put(2, context); - ASSERT_EQ(threadSpanContextMap.Get(1), context); - ASSERT_EQ(threadSpanContextMap.Get(2), context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), context); + ASSERT_EQ(threadSpanContextMap.GetContext(2), context); threadSpanContextMap.Put(1, other_context); - ASSERT_EQ(threadSpanContextMap.Get(1), other_context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), other_context); threadSpanContextMap.Remove(context); - ASSERT_EQ(threadSpanContextMap.Get(1), other_context); - ASSERT_FALSE(threadSpanContextMap.Get(2).has_value()); + ASSERT_EQ(threadSpanContextMap.GetContext(1), other_context); + ASSERT_FALSE(threadSpanContextMap.GetContext(2).has_value()); +} + +TEST(ThreadSpanContextMapTest, GetThreadsForTrace) +{ + continuous_profiler::ThreadSpanContextMap threadSpanContextMap; + const continuous_profiler::thread_span_context context = {1, 1, 1}; + const continuous_profiler::thread_span_context other_context = {1, 1, 2}; + threadSpanContextMap.Put(1, context); + threadSpanContextMap.Put(2, other_context); + + std::unordered_set buffer; + continuous_profiler::trace_context trace_context = {1 ,1}; + threadSpanContextMap.GetAllThreads(trace_context, buffer); + + ASSERT_EQ(buffer.size(), 2); + + threadSpanContextMap.Remove(other_context); + buffer.clear(); + threadSpanContextMap.GetAllThreads(trace_context, buffer); + ASSERT_EQ(buffer.size(), 1); } TEST(ThreadSpanContextMapTest, RemoveByThreadId) @@ -46,11 +66,11 @@ TEST(ThreadSpanContextMapTest, RemoveByThreadId) continuous_profiler::ThreadSpanContextMap threadSpanContextMap; const continuous_profiler::thread_span_context context = {1, 1, 1}; threadSpanContextMap.Put(1, context); - ASSERT_EQ(threadSpanContextMap.Get(1), context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), context); threadSpanContextMap.Remove(1); - ASSERT_FALSE(threadSpanContextMap.Get(1).has_value()); + ASSERT_FALSE(threadSpanContextMap.GetContext(1).has_value()); } TEST(ThreadSpanContextMapTest, RemoveBySpanContext) @@ -59,11 +79,11 @@ TEST(ThreadSpanContextMapTest, RemoveBySpanContext) const continuous_profiler::thread_span_context context = {1, 1, 1}; threadSpanContextMap.Put(1, context); threadSpanContextMap.Put(2, context); - ASSERT_EQ(threadSpanContextMap.Get(1), context); - ASSERT_EQ(threadSpanContextMap.Get(2), context); + ASSERT_EQ(threadSpanContextMap.GetContext(1), context); + ASSERT_EQ(threadSpanContextMap.GetContext(2), context); threadSpanContextMap.Remove(context); - ASSERT_FALSE(threadSpanContextMap.Get(1).has_value()); - ASSERT_FALSE(threadSpanContextMap.Get(2).has_value()); + ASSERT_FALSE(threadSpanContextMap.GetContext(1).has_value()); + ASSERT_FALSE(threadSpanContextMap.GetContext(2).has_value()); } \ No newline at end of file diff --git a/test/test-applications/integrations/TestApplication.SelectiveSampler/Plugins/FrequentSamplingProcessor.cs b/test/test-applications/integrations/TestApplication.SelectiveSampler/Plugins/FrequentSamplingProcessor.cs index 586d807f54..453394e23d 100644 --- a/test/test-applications/integrations/TestApplication.SelectiveSampler/Plugins/FrequentSamplingProcessor.cs +++ b/test/test-applications/integrations/TestApplication.SelectiveSampler/Plugins/FrequentSamplingProcessor.cs @@ -11,8 +11,8 @@ namespace TestApplication.SelectiveSampler.Plugins; // Custom processor that selects all spans for frequent sampling. public class FrequentSamplingProcessor : BaseProcessor { - private static Action? _startSamplingDelegate; - private static Action? _stopSamplingDelegate; + private static Action? _startSamplingDelegate; + private static Action? _stopSamplingDelegate; public FrequentSamplingProcessor() { @@ -22,20 +22,27 @@ public FrequentSamplingProcessor() throw new Exception("OpenTelemetry.AutoInstrumentation.NativeMethods could not be found."); } - var startMethod = nativeMethodsType.GetMethod("SelectiveSamplingStart", BindingFlags.Static | BindingFlags.Public, null, [typeof(Activity)], null); - var stopMethod = nativeMethodsType!.GetMethod("SelectiveSamplingStop", BindingFlags.Static | BindingFlags.Public, null, [typeof(Activity)], null); + var startMethod = nativeMethodsType.GetMethod("SelectiveSamplingStart", BindingFlags.Static | BindingFlags.Public, null, [typeof(ActivityTraceId)], null); + var stopMethod = nativeMethodsType!.GetMethod("SelectiveSamplingStop", BindingFlags.Static | BindingFlags.Public, null, [typeof(ActivityTraceId)], null); - _startSamplingDelegate = (Action)Delegate.CreateDelegate(typeof(Action), startMethod!); - _stopSamplingDelegate = (Action)Delegate.CreateDelegate(typeof(Action), stopMethod!); + _startSamplingDelegate = (Action)Delegate.CreateDelegate(typeof(Action), startMethod!); + _stopSamplingDelegate = (Action)Delegate.CreateDelegate(typeof(Action), stopMethod!); } public override void OnStart(Activity data) { - _startSamplingDelegate?.Invoke(data); + // Native side API is trace-based, notifying only of root span is sufficient. + if (data.Parent == null) + { + _startSamplingDelegate?.Invoke(data.TraceId); + } } public override void OnEnd(Activity data) { - _stopSamplingDelegate?.Invoke(data); + if (data.Parent == null) + { + _stopSamplingDelegate?.Invoke(data.TraceId); + } } } diff --git a/test/test-applications/integrations/TestApplication.SelectiveSampler/Program.cs b/test/test-applications/integrations/TestApplication.SelectiveSampler/Program.cs index 388a32ca10..70a22e1942 100644 --- a/test/test-applications/integrations/TestApplication.SelectiveSampler/Program.cs +++ b/test/test-applications/integrations/TestApplication.SelectiveSampler/Program.cs @@ -14,9 +14,14 @@ public static async Task Main(string[] args) { Thread.CurrentThread.Name = "Main"; - using (ActivitySource.StartActivity()) + // Trace with nested activities + using (ActivitySource.StartActivity("outer")) { - await SimpleAsyncCase(); + Thread.Sleep(100); + using (ActivitySource.StartActivity("inner")) + { + await SimpleAsyncCase(); + } } } diff --git a/test/test-applications/integrations/TestApplication.SelectiveSampler/Properties/launchSettings.json b/test/test-applications/integrations/TestApplication.SelectiveSampler/Properties/launchSettings.json index 7d4d3a67eb..d09ed8ba5e 100644 --- a/test/test-applications/integrations/TestApplication.SelectiveSampler/Properties/launchSettings.json +++ b/test/test-applications/integrations/TestApplication.SelectiveSampler/Properties/launchSettings.json @@ -13,7 +13,8 @@ "OTEL_DOTNET_AUTO_HOME": "$(SolutionDir)bin\\tracer-home", "OTEL_DOTNET_AUTO_PLUGINS": "TestApplication.SelectiveSampler.Plugins.SelectiveSamplerPlugin, TestApplication.SelectiveSampler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES": "TestApplication.SelectiveSampler", - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318" + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + "OTEL_TRACES_EXPORTER": "otlp,console" }, "dotnetRunMessages": true, "nativeDebugging": true From 0e0732b2485353d043e5496355c2469f519f7ac0 Mon Sep 17 00:00:00 2001 From: Mateusz Lach Date: Thu, 23 Oct 2025 10:24:13 +0200 Subject: [PATCH 2/5] native formatting fixes --- .../continuous_profiler.cpp | 4 ++-- .../thread_span_context_test.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp index 4a2167c800..2c72b4a514 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp +++ b/src/OpenTelemetry.AutoInstrumentation.Native/continuous_profiler.cpp @@ -977,8 +977,8 @@ static void RemoveOutdatedEntries(std::unordered_map& { trace::Logger::Warn("SelectiveSampling: removing outdated entry for trace {", "traceIdHigh: ", it->first.trace_id_high_, ", traceIdLow: ", it->first.trace_id_low_, - "} because it was enqueued more than ", - kSelectiveSamplingMaxAgeMinutes, " minutes ago"); + "} because it was enqueued more than ", kSelectiveSamplingMaxAgeMinutes, + " minutes ago"); it = selectiveSamplingTraceSet.erase(it); } else diff --git a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp index a41fcf0342..7176bfda32 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp +++ b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp @@ -49,8 +49,8 @@ TEST(ThreadSpanContextMapTest, GetThreadsForTrace) threadSpanContextMap.Put(1, context); threadSpanContextMap.Put(2, other_context); - std::unordered_set buffer; - continuous_profiler::trace_context trace_context = {1 ,1}; + std::unordered_set buffer; + continuous_profiler::trace_context trace_context = {1 ,1}; threadSpanContextMap.GetAllThreads(trace_context, buffer); ASSERT_EQ(buffer.size(), 2); From fb350c195842ac10444a5c1055ec59df0e42a683 Mon Sep 17 00:00:00 2001 From: Mateusz Lach Date: Thu, 23 Oct 2025 10:25:40 +0200 Subject: [PATCH 3/5] more formatting --- .../thread_span_context_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp index 7176bfda32..db77379294 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp +++ b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp @@ -50,7 +50,7 @@ TEST(ThreadSpanContextMapTest, GetThreadsForTrace) threadSpanContextMap.Put(2, other_context); std::unordered_set buffer; - continuous_profiler::trace_context trace_context = {1 ,1}; + continuous_profiler::trace_context trace_context = {1, 1}; threadSpanContextMap.GetAllThreads(trace_context, buffer); ASSERT_EQ(buffer.size(), 2); From 8501b9511a0df071e0b1a4b1c0c597241b04ffa1 Mon Sep 17 00:00:00 2001 From: Mateusz Lach Date: Thu, 23 Oct 2025 10:55:33 +0200 Subject: [PATCH 4/5] native tests fix --- .../thread_span_context_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp index db77379294..89d154b56f 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp +++ b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp @@ -49,7 +49,7 @@ TEST(ThreadSpanContextMapTest, GetThreadsForTrace) threadSpanContextMap.Put(1, context); threadSpanContextMap.Put(2, other_context); - std::unordered_set buffer; + std::unordered_set buffer; continuous_profiler::trace_context trace_context = {1, 1}; threadSpanContextMap.GetAllThreads(trace_context, buffer); From 44b5912cfc00f4202361a88ddad99be7902e046b Mon Sep 17 00:00:00 2001 From: Mateusz Lach Date: Thu, 23 Oct 2025 10:57:06 +0200 Subject: [PATCH 5/5] format fix --- .../thread_span_context_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp index 89d154b56f..965801eaba 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp +++ b/test/OpenTelemetry.AutoInstrumentation.Native.Tests/thread_span_context_test.cpp @@ -49,8 +49,8 @@ TEST(ThreadSpanContextMapTest, GetThreadsForTrace) threadSpanContextMap.Put(1, context); threadSpanContextMap.Put(2, other_context); - std::unordered_set buffer; - continuous_profiler::trace_context trace_context = {1, 1}; + std::unordered_set buffer; + continuous_profiler::trace_context trace_context = {1, 1}; threadSpanContextMap.GetAllThreads(trace_context, buffer); ASSERT_EQ(buffer.size(), 2);