Skip to content

perf: reuse counter snapshot buffer in CounterGroup.OnTimer#127886

Open
unsafePtr wants to merge 2 commits intodotnet:mainfrom
unsafePtr:perf/countergroup-buffer-reuse
Open

perf: reuse counter snapshot buffer in CounterGroup.OnTimer#127886
unsafePtr wants to merge 2 commits intodotnet:mainfrom
unsafePtr:perf/countergroup-buffer-reuse

Conversation

@unsafePtr
Copy link
Copy Markdown
Contributor

CounterGroup.OnTimer allocates a fresh DiagnosticCounter[] snapshot every poll to copy the counter list. In practice the counter set is fixed at EventSource construction and rarely changes. Reuse a per-instance buffer instead, grow on demand, and clear trailing slots for disposed counters.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label May 6, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @dotnet/area-system-diagnostics-tracing
See info in area-owners.md if you want to be subscribed.

_counters.CopyTo(counters);

counterCount = _counters.Count;
if (_onTimerCounters.Length < counterCount)
Copy link
Copy Markdown
Member

@tarekgh tarekgh May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

race condition can be raised? The _onTimerCounters is allocated/resized and then filled using _counters.CopyTo inside the s_counterGroupLock but later it is used outside the lock. This means theoretically _onTimerCounters or its contents can be changed before executing the next foreach loop.

I would suggest instead of doing that, we can use ArrayPool?

Copy link
Copy Markdown
Contributor Author

@unsafePtr unsafePtr May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only a single thread executing PollForValues

s_pollingThread = new Thread(PollForValues)
{
IsBackground = true,
Name = ".NET Counter Poller"
};

OnTimer itself is called sequentially in a foreach within PollForValues

foreach (CounterGroup onTimer in onTimers)
{
onTimer.OnTimer();
}

There is no parrallel accessed pattern, and if _counters are modified, we are protected under s_counterGroupLock lock. _onTimerCounters is only ever read or written inside OnTimer.

In practice the counter set is fixed at first Enable and never mutated afterward. ArrayPool would pay Rent/Return overhead on every tick for a buffer that, in practice, never needs to be returned.

Copy link
Copy Markdown
Contributor Author

@unsafePtr unsafePtr May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it would be possible to do stackalloc DiagnosticCounter[_counters.Count] it would be an easy change here. Actually now thinking about it again, we can do [InlineArray] for small counts (maybe below 32?). As it seems the max count of RuntimeEventSource is 27 counters. And use ValueListBuilder which switches to ArrayPool when there is no enough space.

i.e.

[InlineArray(32)]
private struct CounterScratch
{
    private DiagnosticCounter? _element0;
}

CounterScratch scratch = default; 
var builder = new ValueListBuilder<DiagnosticCounter>(scratch);

// instead of  _onTimerCounters = new DiagnosticCounter[counterCount];
counterCount = _counters.Count;
Span<DiagnosticCounter> dst = builder.AppendSpan(counterCount);
CollectionsMarshal.AsSpan(_counters).CopyTo(dst);

Seems to be an elegant solution, and this way we avoid holding an array. Will wait for your judgment before doing the change.

Match the GC behavior of the previous local-snapshot version: drop
counter references the moment the foreach exits, instead of holding
them until the next tick.
counter.WritePayload((float)elapsed.TotalSeconds, (int)pollingInterval.TotalMilliseconds);
}

Array.Clear(_onTimerCounters);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaning so we don't hold any references, and GC can collect them in case if _counters is updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Diagnostics.Tracing community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants