perf: remove unnecessary lock in ObjectTracker.TrackObjects - #5217
Conversation
TrackedObjects is per-TestContext and TrackObjects is called from a single thread per test context. The lock(kvp.Value) on each HashSet caused Monitor.Enter_Slowpath contention (~1.25% exclusive CPU) during parallel test execution of 1,013+ tests, with no actual concurrent access to protect against.
There was a problem hiding this comment.
Code Review
Overall: Clean and correct performance improvement. The analysis is sound and the change is safe.
Summary of Changes
- **Removes in ** — the lock was guarding iteration of a that comes from .
- Removes a stale comment in referencing a lock that no longer existed.
Analysis: Was the lock actually necessary?
The PR's reasoning holds up under scrutiny:
- delegates to , which constructs a fresh on every call — the
kvp.ValueHashSets are not shared references. TrackedObjectsonTestContextis per-context, not shared across tests.- The
ObjectGraphDiscovererclass itself is documented as thread-safe (staticConcurrentBag, cached reflection), but the returned dictionary is local to each call.
So yes, the lock was guarding a locally-constructed, call-owned collection — providing no real safety benefit while introducing Monitor.Enter_Slowpath overhead under parallel test execution.
Minor Consideration
The new comment says:
// No lock needed: TrackedObjects is per-TestContext and TrackObjects is called from a single thread per test.
The "called from a single thread per test" part is an architectural contract that isn't enforced at compile time. If this invariant ever breaks (e.g., a future parallel data-provider feature), it would be a silent regression. Consider whether this guarantee should be documented more formally at the call site in the engine, or whether a Debug.Assert or thread-affinity check could serve as a guardrail. That said, this is a pre-existing condition — not introduced by this PR — and is low risk given the current architecture.
Stale Comment Removal
Correct. The removed comment ("Dispose outside the lock to avoid blocking other untrack operations") referenced a lock in UntrackObject that no longer exists. The cleanup is appropriate.
No blocking issues. The change correctly identifies and removes unnecessary synchronization overhead.
There was a problem hiding this comment.
Code Review
Overall: Clean and correct performance improvement. The analysis is sound and the change is safe.
Summary of Changes
- Removes
lock (kvp.Value)inTrackObjects— the lock was guarding iteration of aHashSet<object>that comes fromtrackableObjectGraphProvider.GetTrackableObjects(testContext). - Removes a stale comment in
UntrackObjectreferencing a lock that no longer existed.
Analysis: Was the lock actually necessary?
The PR's reasoning holds up under scrutiny:
GetTrackableObjectsdelegates toObjectGraphDiscoverer.DiscoverAndTrackObjects, which constructs a freshDictionary<int, HashSet<object>>on every call — thekvp.ValueHashSets are not shared references.TrackedObjectsonTestContextis per-context, not shared across tests.- The
ObjectGraphDiscovererclass itself is documented as thread-safe (staticConcurrentBag, cached reflection), but the returned dictionary is local to each call.
So yes, the lock was guarding a locally-constructed, call-owned collection — providing no real safety benefit while introducing Monitor.Enter_Slowpath overhead under parallel test execution.
Minor Consideration
The new comment says:
// No lock needed: TrackedObjects is per-TestContext and TrackObjects is called from a single thread per test.
The "called from a single thread per test" part is an architectural contract that isn't enforced at compile time. If this invariant ever breaks (e.g., a future parallel data-provider feature), it would be a silent regression. Consider whether this guarantee should be documented more formally at the call site in the engine, or whether a Debug.Assert or thread-affinity check could serve as a guardrail. That said, this is a pre-existing condition — not introduced by this PR — and is low risk given the current architecture.
Stale Comment Removal
Correct. The removed comment ("Dispose outside the lock to avoid blocking other untrack operations") referenced a lock in UntrackObject that no longer exists. The cleanup is appropriate.
No blocking issues. The change correctly identifies and removes unnecessary synchronization overhead.
Uh oh!
There was an error while loading. Please reload this page.
Summary
lock (kvp.Value)inObjectTracker.TrackObjects()that causedMonitor.Enter_Slowpathcontention (~1.25% exclusive CPU during parallel execution)TrackedObjectsis per-TestContextandTrackObjectsis called from a single thread per tests_trackedObjectsConcurrentDictionary) is already thread-safe viaGetOrAddUntrackObjectthat referenced a non-existent lockTest plan