Skip to content

Commit eed4592

Browse files
committed
Add metric on lock failure for IsManaged
1 parent 0508822 commit eed4592

6 files changed

Lines changed: 83 additions & 9 deletions

File tree

profiler/src/ProfilerEngine/Datadog.Profiler.Native/CorProfilerCallback.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1567,7 +1567,7 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::Initialize(IUnknown* corProfilerI
15671567
// Use managed code cache
15681568
if (_pConfiguration->UseManagedCodeCache())
15691569
{
1570-
_managedCodeCache = std::make_unique<ManagedCodeCache>(_pCorProfilerInfo);
1570+
_managedCodeCache = std::make_unique<ManagedCodeCache>(_pCorProfilerInfo, _metricsRegistry);
15711571
if (!_managedCodeCache->Initialize())
15721572
{
15731573
Log::Error("Failed to initialize managed code cache. The profiler will not run.");

profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "ManagedCodeCache.h"
55

66
#include "Configuration.h"
7+
#include "CounterMetric.h"
8+
#include "MetricsRegistry.h"
79

810
#include <algorithm>
911
#include <chrono>
@@ -88,8 +90,9 @@ struct IMAGE_NT_HEADERS_GENERIC
8890
WORD Magic;
8991
};
9092

91-
ManagedCodeCache::ManagedCodeCache(ICorProfilerInfo4* pProfilerInfo)
92-
: _profilerInfo(pProfilerInfo)
93+
ManagedCodeCache::ManagedCodeCache(ICorProfilerInfo4* pProfilerInfo, MetricsRegistry& metricsRegistry)
94+
: _profilerInfo(pProfilerInfo),
95+
_lockFailureMetric(metricsRegistry.GetOrRegister<CounterMetric>("dotnet_managed_code_cache_lock_failures"))
9396
{
9497
}
9598

@@ -239,7 +242,14 @@ std::optional<bool> ManagedCodeCache::IsManaged(std::uintptr_t ip) const noexcep
239242
// Best effort to identify an instruction pointer. When called from a signal
240243
// handler, IsManagedImpl uses a time-based lock acquire (bounded wait) and
241244
// returns std::nullopt if a lock cannot be acquired within the timeout.
242-
return IsManagedImpl(ip);
245+
auto result = IsManagedImpl(ip);
246+
if (!result.has_value())
247+
{
248+
// Lock could not be acquired within the timeout: record it. Incr() is an
249+
// atomic increment, so this is safe to call from a signal handler.
250+
_lockFailureMetric->Incr();
251+
}
252+
return result;
243253
}
244254

245255
std::optional<bool> ManagedCodeCache::IsManagedImpl(std::uintptr_t ip) const noexcept

profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include <atomic>
7+
#include <memory>
78
#include <vector>
89
#include <unordered_map>
910
#include <unordered_set>
@@ -17,6 +18,9 @@
1718
#include "cor.h"
1819
#include "corprof.h"
1920

21+
class CounterMetric;
22+
class MetricsRegistry;
23+
2024
// Represents a single contiguous code range
2125
struct CodeRange {
2226
UINT_PTR startAddress;
@@ -73,7 +77,7 @@ class ManagedCodeCache {
7377
public:
7478
static constexpr FunctionID InvalidFunctionId = -1;
7579

76-
explicit ManagedCodeCache(ICorProfilerInfo4* pProfilerInfo);
80+
ManagedCodeCache(ICorProfilerInfo4* pProfilerInfo, MetricsRegistry& metricsRegistry);
7781
~ManagedCodeCache();
7882

7983
// Signal-safe lookup methods (no allocation)
@@ -168,6 +172,10 @@ class ManagedCodeCache {
168172
// Profiler interface (ICorProfilerInfo4 is available in .NET Framework 4.5+)
169173
ICorProfilerInfo4* _profilerInfo;
170174

175+
// Counts how many times IsManaged failed to acquire a lock within the timeout
176+
// (signal-handler read path backing off instead of blocking).
177+
std::shared_ptr<CounterMetric> _lockFailureMetric;
178+
171179
std::optional<FunctionID> GetFunctionIdImpl(std::uintptr_t ip) const noexcept;
172180
std::optional<bool> IsCodeInR2RModule(std::uintptr_t ip, bool signalSafe) const noexcept;
173181
std::optional<FunctionID> GetFunctionFromIP_Original(std::uintptr_t ip) noexcept;

profiler/test/Datadog.Profiler.Native.Tests/FrameStoreTest.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "FrameStore.h"
88
#include "ManagedCodeCache.h"
9+
#include "MetricsRegistry.h"
910
#include "MockProfilerInfo.h"
1011

1112
#include <memory>
@@ -86,7 +87,8 @@ TEST(FrameStoreTest, GetFrame_WithCache_NativeIp_ReturnsNotResolvedAndDropped)
8687

8788
// Empty cache => any IP resolves to InvalidFunctionId (there are no registered
8889
// JIT ranges and no R2R modules), which is exactly the "native IP" case.
89-
auto cache = std::make_unique<ManagedCodeCache>(&mockProfiler);
90+
MetricsRegistry metricsRegistry;
91+
auto cache = std::make_unique<ManagedCodeCache>(&mockProfiler, metricsRegistry);
9092
cache->Initialize();
9193

9294
FrameStore frameStore(
@@ -161,7 +163,8 @@ TEST(FrameStoreTest, GetFrame_WithCache_CachedPathNullopt_ReturnsResolvedPlaceho
161163
{
162164
auto mockProfiler = MockProfilerInfo{};
163165

164-
auto cache = std::make_unique<ManagedCodeCache>(&mockProfiler);
166+
MetricsRegistry metricsRegistry;
167+
auto cache = std::make_unique<ManagedCodeCache>(&mockProfiler, metricsRegistry);
165168
cache->Initialize();
166169

167170
// Register an R2R module range so GetFunctionId falls through to

profiler/test/Datadog.Profiler.Native.Tests/LinuxStackFramesCollectorTest.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test
166166

167167
#ifdef ARM64
168168
// TODO maybe a mock of ICorProfilerInfo to avoid crashing
169-
_pManagedCodeCache = std::make_unique<ManagedCodeCache>(nullptr);
169+
_pManagedCodeCache = std::make_unique<ManagedCodeCache>(nullptr, _metricsRegistry);
170170
_pUnwinder = std::make_unique<HybridUnwinder>(_pManagedCodeCache.get());
171171
#else
172172
_pUnwinder = std::make_unique<Backtrace2Unwinder>();

profiler/test/Datadog.Profiler.Native.Tests/ManagedCodeCacheTest.cpp

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
#include "gtest/gtest.h"
55
#include "gmock/gmock.h"
6+
#include "CounterMetric.h"
67
#include "ManagedCodeCache.h"
8+
#include "MetricsRegistry.h"
79
#include "MockProfilerInfo.h"
810

911
#include <thread>
@@ -15,11 +17,12 @@ using namespace testing;
1517
class ManagedCodeCacheTest : public Test {
1618
protected:
1719
MockProfilerInfo* mockProfiler;
20+
MetricsRegistry metricsRegistry;
1821
std::unique_ptr<ManagedCodeCache> cache;
1922

2023
void SetUp() override {
2124
mockProfiler = new MockProfilerInfo();
22-
cache = std::make_unique<ManagedCodeCache>(mockProfiler);
25+
cache = std::make_unique<ManagedCodeCache>(mockProfiler, metricsRegistry);
2326
cache->Initialize();
2427
}
2528

@@ -44,6 +47,15 @@ class ManagedCodeCacheTest : public Test {
4447
return S_OK;
4548
});
4649
}
50+
51+
// Helper: read (and reset) the IsManaged lock-failure counter.
52+
// GetMetrics() exchanges the underlying atomic to 0, so each call returns the
53+
// number of failures recorded since the previous read.
54+
uint64_t GetLockFailureCount() {
55+
auto metric = metricsRegistry.GetOrRegister<CounterMetric>("dotnet_managed_code_cache_lock_failures");
56+
auto metrics = metric->GetMetrics();
57+
return metrics.empty() ? 0 : static_cast<uint64_t>(metrics.front().second);
58+
}
4759
};
4860

4961
// Test: Single code range
@@ -404,6 +416,47 @@ TEST_F(ManagedCodeCacheTest, IsManaged_WriterHoldsPagesMutex_ReturnsNullopt) {
404416
EXPECT_TRUE(afterRelease.value());
405417
}
406418

419+
// Test: IsManaged increments the lock-failure metric when it cannot acquire the
420+
// pages mutex within the timeout, and leaves it untouched on the success path.
421+
TEST_F(ManagedCodeCacheTest, IsManaged_LockAcquisitionFailure_IncrementsMetric) {
422+
FunctionID testFuncId = 0x4321;
423+
uintptr_t codeStart = 0xD000;
424+
ULONG32 codeSize = 0x100;
425+
426+
SetupMockCodeInfo(testFuncId, codeStart, codeSize);
427+
cache->AddFunction(testFuncId);
428+
429+
// Success path must not increment the failure metric.
430+
ASSERT_TRUE(cache->IsManaged(codeStart + 0x50).value_or(false));
431+
EXPECT_EQ(0u, GetLockFailureCount());
432+
433+
// Hold the pages mutex exclusively so IsManaged times out and returns nullopt.
434+
std::atomic<bool> writerHoldsLock{false};
435+
std::atomic<bool> readerDone{false};
436+
std::thread writer([&]() {
437+
auto lock = cache->LockPagesMutexExclusiveForTest();
438+
writerHoldsLock.store(true);
439+
while (!readerDone.load())
440+
{
441+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
442+
}
443+
});
444+
445+
while (!writerHoldsLock.load())
446+
{
447+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
448+
}
449+
450+
auto contended = cache->IsManaged(codeStart + 0x50);
451+
EXPECT_FALSE(contended.has_value());
452+
453+
readerDone.store(true);
454+
writer.join();
455+
456+
// The failed acquisition must have been recorded exactly once.
457+
EXPECT_EQ(1u, GetLockFailureCount());
458+
}
459+
407460
// Test: IsManaged must also return nullopt when the writer holds the modules
408461
// mutex exclusively and the probed IP is not in any JIT page (i.e. the code path
409462
// falls through to IsCodeInR2RModule(ip, /*signalSafe*/true)).

0 commit comments

Comments
 (0)