| name | performance-requirements |
| description | ClrProfiler-specific performance and memory requirements for CLR EventListener callbacks, GC event correlation, bounded channels, timer sampling, statistics values, callback dispatch, and Datadog or logger metric projection. Use when changing event ingestion, listener or timer hot paths, statistics models, tracker concurrency, or metric formatting and tag caching. |
Performance Requirements
Treat overhead added to the profiled process as part of ClrProfiler's correctness. Apply these requirements to changes in src/ClrProfiler and src/ClrProfiler.DatadogTracing that run per CLR event, per timer tick, or per emitted metric.
Protect the producer path
- Keep
EventListener.OnEventWritten, EventCreatedHandler, and ProcessEvent bounded and non-blocking. Never wait synchronously for user callbacks, logging, network I/O, or channel capacity.
- Parse only the payload fields needed by the matched event. Prefer typed payload values and invariant conversion; avoid
ToString plus Parse on the normal path when the runtime already supplies a numeric value.
- Avoid LINQ, closures, temporary collections, interpolated diagnostic strings, and per-event lookup construction in listener callbacks.
- Keep exception handling at the event boundary. Route malformed payload and callback failures to the configured error callback without terminating the reader loop.
- Do not silently change delivery semantics. Channel capacity, full mode, reader/writer assumptions, event ordering, and loss behavior are observable design decisions and require explicit tests.
Preserve event correlation
- Correlate paired events using runtime identity, not arrival adjacency. In particular, background and foreground GCs can overlap, so match
GCStart and GCEnd by GC index.
- Bound correlation state and avoid per-event allocation. If a fixed-size structure is used, test collisions, missing starts, stale entries, and overlapping collections before changing its capacity or indexing.
- Use
DateTime.Ticks or the existing raw numeric representation through correlation, then calculate durations once when producing the statistics value.
- Treat unexpected event names, versions, missing payloads, and numeric representations as input-boundary cases. Do not let them corrupt state for subsequent events.
Keep data and dispatch inexpensive
- Prefer compact
readonly struct statistics for immutable per-sample values. Preserve value equality and hash-code behavior when adding fields.
- Pass large statistics by
in where the existing metric projection API does so. Do not box statistics or enums on hot paths without measuring the cost.
- Cache reusable metric tags and mappings outside per-event methods. Keep caches bounded by a small runtime-defined key space; do not cache arbitrary user-controlled strings indefinitely.
- Keep the core
ClrProfiler project dependency-free. Backend-specific formatting and delivery belong in ClrProfiler.DatadogTracing or another adapter.
- Await user callbacks in the single reader path so callback order remains deterministic. Continue reading after reporting a callback exception.
Concurrency and lifecycle
- Keep listener, timer, and tracker instances independent. Do not introduce mutable static lifecycle state.
- Make
Start, Stop, Restart, Cancel, Reset, and Dispose transitions safe and idempotent where the public API already promises that behavior.
- Keep a reader alive across
Stop and Restart; cancellation owns reader termination. Disposal must release EventListener, Timer, channel-related, and cancellation resources without resurrecting them.
- Never hold a lifecycle or correlation lock while invoking user code, awaiting a task, emitting metrics, or performing I/O.
- Use
RunContinuationsAsynchronously for completion sources in concurrent tests to avoid running test continuations inside production critical sections.
Verify performance changes
Do not claim a numeric performance improvement from stopwatch timing or from the functional test suite. Use the committed benchmark project at src/ClrProfiler.Benchmarks.
E2EBenchmarks measures representative workloads with every listener disabled and enabled, driven by a TrackingEnabled parameter, so each benchmark reports the overhead the profiler adds:
AllocationAndGc — allocate 1 MiB and force a Gen0 collection
Contention — contended monitor across parallel operations
ThreadPoolDispatch — parallel ThreadPool dispatch
It is configured with MemoryDiagnoser and Job.ShortRun, and uses BenchmarkSwitcher, so BenchmarkDotNet's own --filter argument selects benchmarks. The project multi-targets net8.0, net9.0, and net10.0, so a target framework must be given explicitly:
dotnet run -c Release --project src/ClrProfiler.Benchmarks -f net9.0 -- --list flat
dotnet run -c Release --project src/ClrProfiler.Benchmarks -f net9.0 -- --filter "*Contention*"
For a meaningful hot-path change:
- Run the relevant correctness and data-integrity tests first.
- Measure the old and new implementations with the same Release build, runtime, event payload, event count, warmup, and invocation method.
- Record throughput and allocated bytes. Also inspect Gen0 collections when allocation behavior changes.
- Extend
src/ClrProfiler.Benchmarks when an existing benchmark does not cover the changed path. Add a separate BenchmarkDotNet project only when a task needs isolated performance gating, and keep benchmarks out of the sample applications.
- Reject unexplained event loss, ordering changes, unbounded state, or allocation regressions even when mean time improves.
An allocation assertion in the test suite is a complement to benchmarking, not a substitute, and it has its own trap: a measured loop left in tier-0 code is replaced mid-flight by an on-stack replacement transition that itself allocates on the executing thread. Put the measured loop in its own method marked [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] and run the same method for warmup, as tests/CleProfiler.DatadogTracing.UnitTest/MetricTagAllocationTest.cs does. Otherwise the result depends on test execution order.
Always run the repository's Release build and tests after a performance-sensitive change:
dotnet build -c Release
dotnet test -c Release --no-build