fix: create the HzMemoryCache ActivitySource once instead of per call - #28
Merged
Merged
Conversation
HzActivities.Source was an expression-bodied property, so every cache operation and every cleanup-timer tick constructed a new ActivitySource. Each instance registers itself in the runtime's global source list and is never disposed, so the list grows for the lifetime of the process. Since .NET 9 that list is a copy-on-write array: each registration allocates a new array and copies all previous entries. In admin-backend this showed up as allocation rate and gen2 GC rate climbing linearly with uptime while idle (0.3 -> 7 MB/s over 2.5 days at ~2 creations/s from the 1 s cleanup timer alone), with memory and CPU following until the next deploy. A single static instance is the documented usage pattern for ActivitySource and removes the growth entirely. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Asserts that HzActivities.Source is a single instance and that a burst of cache operations plus cleanup-timer ticks registers no new ActivitySource, observed through an ActivityListener's ShouldListenTo callback. Both tests fail against the previous expression-bodied property. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
patrik-j-nilsson
approved these changes
Sep 7, 2026
There was a problem hiding this comment.
🟡 Changes recommended
It introduces a binary-breaking public API change (Source property → field) and the new test has a cross-thread data race on the armed flag that can yield false negatives.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR addresses a runtime/performance issue where HzActivities.Source was constructing a new ActivitySource on every access, causing steadily increasing allocation and CPU cost over process uptime due to repeated global registration.
Changes:
- Replace per-access
ActivitySourceconstruction with a single shared instance inHzActivities. - Add unit tests to assert
HzActivities.Sourceis stable and that cache operations do not create additionalActivitySourceinstances.
File summaries
| File | Description |
|---|---|
| UnitTests/DiagnosticsTests.cs | Adds regression tests to detect per-call ActivitySource creation. |
| HzMemoryCache/Diagnostics/HzActivities.cs | Switches HzActivities.Source to a single shared instance to avoid repeated registrations/allocations. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Expose the single instance through a property backed by a private static field, so consumers compiled against get_Source() keep working. The test now captures the expected instance before registering the listener and counts any other source with the same name, which removes the cross-thread flag and the dependence on which test touches Source first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
HzActivities.Sourceis an expression-bodied property:Every access constructs a new
ActivitySource. It is accessed from 27 call sites (HzMemoryCache,HzCacheMemoryLocker,RedisBackedHzCache), and the cleanup timer alone (cleanupJobIntervaldefault 1000 ms) accesses it twice per tick viaProcessExpiredEvictionandEvictExpired, so a process creates at least 2 new sources per second even when the cache is idle.Every
ActivitySourceconstructor registers the instance in the runtime's statics_activeSourceslist and onlyDispose()removes it, which never happens here. Since .NET 9 that list is a copy-on-write array: each registration allocates an array of n+1 references and copies the previous n. So the cost of each creation grows with process uptime, the arrays land on the LOH, and gen2 collections follow.Observed effect in admin-backend (Admin.API, .NET 10)
RC pod at idle (about one request per hour, zero SQL operations):
At 2 creations/s the copy-on-write model predicts 6.9 MB/s after 2.5 days and about 55 MiB of retained sources; observed 7.1 MB/s and 55 MiB. Busier environments (stage, prod) climb faster because request-path cache operations add more creations. Memory and CPU grow until the next deploy resets the process.
Fix
A single static instance, which is the documented usage pattern for
ActivitySource:No call-site changes needed. The type is no longer nullable, which only removes a nullable warning at the call sites.
Verification
dotnet build hzcache.sln -c Release: 0 errorsdotnet test --filter "TestCategory!=Integration"(same filter as CI): 36 passed, 0 failedFollow-up
release/0.0.19and bumpRedisBackedHzCachein commerce-admin (src/Infrastructure, currently 0.0.18) and Storm.Admin (0.0.16).Norce.Data.Sharding.Common.Instrumentation.Sourceindata-shardinghas the same pattern at a much lower call rate; same one-line fix.🤖 Generated with Claude Code