Icorprofiler

2 posts

datadog3 min readCurated summary

.NET Continuous Profiler: Memory usage

Datadog’s .NET memory profiler helps identify excessive garbage collection, allocation hotspots, and objects that remain in memory after collection. It combines CLR events, operating-system thread metrics, sampled allocation data, stack traces, and weak handles to provide production-friendly memory insights. The approach favors low overhead, though some capabilities depend on the .NET version. ## Measuring Garbage Collector CPU Impact - The profiler uses CLR events to monitor garbage collection phases. - In server GC mode, the CLR creates two high-priority threads per heap/core to process collections in parallel. - Since .NET 5, these threads are named `.NET Server GC` and `.NET BGC`. - At each profile export, the profiler retrieves these threads’ CPU usage from the operating system. - It records the result as a sample with a native stack containing a `Garbage Collector` frame. - This uses a pull model: the exporter periodically requests the CPU measurement because no suitable event or dedicated profiler thread exists. - Before .NET 5, GC thread CPU usage could not reliably be identified because `GCCreateConcurrentThread` did not include thread IDs. ## Sampling Allocations - Per-allocation callbacks such as `ICorProfilerCallback::ObjectAllocated` provide detailed data but significantly slow allocation fast paths. - `GCSampledObjectAllocation` and `ObjectsAllocatedByClass` reduce some costs but do not provide call stacks for individual allocation sites. - Datadog instead listens to `AllocationTick`, emitted for roughly every 100 KB allocated. - Each event includes: - The object’s `ClassID` and type information. - The allocation address. - The object size and total allocation size since the previous tick. - The allocation kind: SOH (`0`), LOH (`1`), or POH (`2`). - Generic type names are reconstructed through the .NET profiling API. - Because allocation events are synchronous, the current thread is responsible for the allocation; the profiler walks that thread’s stack to capture the allocation call site. - This produces sampled allocation data for each heap category without imposing the cost of observing every allocation. ## Tracking Objects That Survive Garbage Collection - An allocation address alone cannot track an object indefinitely because compacting garbage collections can move objects. - Datadog uses weak handles, created through `GCHandle.Alloc`, which move with objects and do not keep them alive. - The profiler added this functionality through the .NET 7 `ICorProfilerInfo13` API and its `LiveObjectsProvider`. - For every sampled allocation, it creates a weak handle and records the object’s creation time. - After each garbage collection: - Handles for unreachable objects are removed and destroyed. - Handles for surviving objects remain and are included in the next profile. - This lets users inspect representative objects that persist after collection and investigate potential memory leaks. ## Practical Recommendation Use allocated-memory profiles to find endpoints and types responsible for excessive allocation, then examine surviving-object samples for retention or leak investigations. GC CPU data is especially useful for diagnosing applications whose high CPU usage is driven by frequent or expensive garbage collections.

Read original(opens in new tab)
datadog3 min readCurated summary

.NET Continuous Profiler: Exception and lock contention

Datadog’s .NET continuous profiler can diagnose performance problems that CPU and wall-time profiling may miss: excessive exceptions and lock contention. Exceptions consume significant CPU and latency, while locks increase request latency through waiting rather than active computation. By collecting exception details and measuring contention duration with low overhead, the profiler helps developers identify the code and runtime conditions responsible. ## Exception Profiling - The CLR notifies the profiler through `ICorProfilerCallback::ExceptionThrown`, providing the exception’s `ObjectID`. - `ExceptionProvider::OnExceptionThrown` extracts details such as: - Exception type - Thread ID - Source location - The profiler maps the exception object to its `ClassID` using `ICorProfilerInfo::GetClassFromObject`. - Type names are resolved and cached by the `FrameStore`. - Exception messages require reading the private `System.Exception._message` field: - The profiler locates `System.Exception` in `mscorlib` or `System.Private.CoreLib`. - `GetModuleMetaData` provides access to assembly metadata. - `FindTypeDefByName` locates the type definition. - `GetClassFromTokenAndTypeArgs` obtains its `ClassID`. - `GetClassLayout` identifies field offsets. - `FindField` locates `_message`. - `GetStringLayout2` provides the string buffer and length needed to read the message. - Collecting exception counts by type, message, and call site makes it possible to replace expensive exception-driven control flow with cheaper checks such as `TryParse`. ## Lock Contention Monitoring - Standard .NET monitoring exposes contention counts, but generally not how long threads waited or where the contention originated. - The CLR emits: - `ContentionStart` when a thread begins waiting - `ContentionStop` when it acquires the lock - On .NET Framework, contention duration is calculated from timestamps recorded for each thread because `ContentionStop` does not include the duration. - Since .NET 8, `ContentionStart` includes the lock’s `ObjectID` and the ID of the thread holding it, allowing the profiler to identify the blocking thread. - .NET Framework exposes counters such as `Contention Rate / Sec` and `Total # of Contentions`; .NET Core provides `monitor-lock-contention-count` through `dotnet-counters`. - These counters alone do not reveal the duration or cause of waits. ## Consuming CLR Events - Since .NET 5, profilers can synchronously receive CLR events through `ICorProfilerCallback10::EventPipeEventDelivered`. - Datadog’s `ClrEventParser` interprets event payloads based on event IDs and keywords. - The parsed duration is passed to `ContentionProvider::OnContention`. - Runtime differences require version-specific handling because event payloads are not identical across .NET Framework and .NET Core. The practical recommendation is to profile both exception frequency and lock-wait duration, rather than relying only on CPU usage or contention counters. This reveals inefficient exception-based logic and identifies locks—and, on newer runtimes, the threads holding them—that materially affect application latency.

Read original(opens in new tab)