Skip to content

fix(monitoring): make Counter and Meter increments O(1) amortized - #53

Merged
CypherPotato merged 1 commit into
mainfrom
fix/monitoring-counter-meter-cost
Oct 16, 2026
Merged

CypherPotato merged 1 commit into
mainfrom
fix/monitoring-counter-meter-cost

Conversation

@CypherPotato

Copy link
Copy Markdown
Member

What

Counter.Increment and Meter.Increment had a cost that grew with the amount of retained data, so a busy counter or a long Meter window made every increment slower.

  • Counter kept one List entry per increment and ran RemoveAll over the whole list on every call (O(n) per increment, quadratic overall). Entries now live in a PriorityQueue ordered by expiry, and only the expired head is dequeued. A priority queue is used instead of a plain queue because Increment(by, duration) accepts a different duration per call, so insertion order is not expiry order.
  • Meter rebuilt an array of all bucket keys with LINQ on every call (about 193 B allocated per increment, with up to 86,400 buckets for the default 24 h window). It now prunes at most once per bucket interval, without LINQ or per-call allocations.

Public API, TTL/window semantics and thread safety are unchanged. Counter.Current and the state export still return the same values.

Measurements

Release, net9.0, loopback, same probe against main and this branch:

Scenario Before After
Counter, 200k live entries 91 µs/inc 0.13 µs/inc
Meter, full 24 h window 164 µs/inc 0.26 µs/inc
Meter, full 24 h window, 8 threads ~6k inc/s ~5.8M inc/s

Tests

New CounterMeterTests in Sisk.Monitoring.Tests: sum and expiry correctness, expiry out of insertion order, Meter window expiry, and a cost test (100k increments must finish within 1 s). With the old implementation the cost test fails (52 s); with this change Sisk.Monitoring.Tests passes 8/8.

Notes

  • The cost test uses a wide margin (about 50x between old and new) to avoid being flaky on slow CI machines.
  • Tests use short real sleeps (up to ~3 s for the Meter window) because the types read DateTime.Now directly and there is no injectable clock.

Counter kept one list entry per increment and scanned the whole list on
every call. It now keeps entries in a priority queue ordered by expiry
and only dequeues the expired head. Meter rebuilt an array of all bucket
keys on every call; it now prunes at most once per bucket interval,
without LINQ or per-call allocations.

Measured in Release (net9.0): Counter with 200k live entries went from
91 us to 0.13 us per increment, a full 24 h Meter from 164 us to 0.26 us,
and 8 threads on a full Meter from ~6k to ~5.8M increments per second.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-14T17:46:58.912614Z faa0ac4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@CypherPotato
CypherPotato merged commit d699c5a into main Oct 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant