datadog3 min read

Curated summary

.NET Continuous Profiler: Exception and lock contention

Read original(opens in new tab)

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.

Continue with another curated summary.