perf(cli): count a cluster's members once, not once per member (#175) - #201
Merged
Conversation
#175 reported that `SharedFailure` counts each cluster member's failures with a linear scan over the whole cluster, inside the loop over the cluster's members — `O(|Fingerprints| x |Failures|)` with an ordinal string comparison in the inner loop, once per cluster. The scaling factor is the payload the kind exists to report. A broken fixture takes out every test in an assembly, so the members and the failures grow together and the pathological input is precisely the successful case. The default window reads 20 sessions, which bounds it; `--since` reads up to 1000. `SignatureGroup` gains `FailuresByFingerprint`, counted in the same walk of the same list that already produces `Fingerprints`, `MaxTestsInOneSession` and `SessionCount` — so the counts are free there, and there is no longer a second place that could decide what "this member's failures" means. `SharedFailure` reads them. The loop keeps its shape: `references` and `members` are appended together only when the fingerprint resolves, and the subject is built from the first while the evidence is built from the second. `Spread` was the other half, on the same list and hit once per cluster. It ordered every failure under a four-key comparator — materialising `ExecutionId.ToString("N")` for each one — to read three entries off the front. It now keeps the earliest failure per key in one pass and orders those, which is the same answer: the entries of the ordered list that introduce a new key are exactly the per-key minima. The top-up branch, reached only below three distinct keys, selects into a bounded shortlist rather than sorting. The last comparison key stays the hex form rather than `Guid.CompareTo`, which orders differently; it is now formatted only for two attempts of one test in one run. Measured on 500 runs x 1000 tests all failing on one signature: cluster analysis 22,960ms to 75ms, with the emitted evidence byte-identical. At the issue's headline 1000 x 2000 it is 339ms. What now bounds that store is `SignatureIndex.Build` signing two million failed executions at 6.7s — linear, and a separate question from this one. Two new tests. Nothing in the suite asserted `ClusterMember.Failures` at all, and nothing pinned which exemplars a cluster picks rather than how many. Both pass against the pre-change provider, which is what makes them evidence that the selection is unchanged rather than merely self-consistent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The repeated-key exemplar top-up path lacks an exact output-order regression test.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Optimizes shared-failure analysis while preserving evidence ordering.
Changes:
- Precomputes failure counts per fingerprint.
- Replaces full exemplar sorting with bounded selection.
- Adds count and ordering regression tests.
File summaries
| File | Description |
|---|---|
SignatureIndex.cs |
Indexes per-fingerprint failure counts. |
FailureModeProvider.cs |
Uses indexed counts and optimized exemplar selection. |
FailureModeProviderTests.cs |
Tests member counts and distinct-key ordering. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Review of #201. `TopUp` was reached only by tests asserting the exemplar count, and the cluster-order test added with the refactor has six distinct keys, so it never enters that branch. A change promising byte-identical evidence needs the repeated-key case held to the identities it published before, not to its length. Four tests, each naming a rule the ordering makes and none of them reachable through the count alone: One mode, five failures: the three exemplars are the three most recent, so they describe what the test does now. Two modes, four failures: each mode is shown once before either is shown twice, so the third exemplar is the newest of what is left rather than the third newest failure. That distinction is the reason the spread exists and nothing asserted it. A run that recorded its seventh retry attempt first: the exemplars are still its earliest attempts. Nothing promises an adapter writes attempts in order, and the bounded shortlist must not depend on it — this is also the only fixture in which a better failure is offered after the shortlist is full. Two executions of one test in one run, from an adapter tracking no retry metadata: they agree on everything the order reads except their execution ids, and the tie-break `TestSessionFactory` already derives its ids to keep testable is what separates them. All four pass against the pre-change provider, which is what makes them evidence about the selection rather than about the new implementation agreeing with itself. Patch coverage of the branch goes to 100%: what was uncovered was the shortlist's insertion and eviction, the attempt comparison against a failure carrying no retry metadata, and the execution-id fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
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.
Closes #175.
SharedFailurecounted each cluster member's failures with a linear scan over the whole cluster, inside the loop over the cluster's members —O(|Fingerprints| × |Failures|)with an ordinal string comparison in the inner loop, once per cluster.The scaling factor is the payload the kind exists to report. A broken fixture takes out every test in an assembly, so the members and the failures grow together: the pathological input is precisely the successful case. The default window reads 20 sessions, which bounds it.
--sincereads up to 1000.The counts move into the index
SignatureGroupgainsFailuresByFingerprint, counted in the same walk of the same list that already producesFingerprints,MaxTestsInOneSessionandSessionCount— so the counts are free there, and there is no longer a second place that could decide what "this member's failures" means.SharedFailurereads them.The loop keeps its shape deliberately.
referencesandmembersare appended together only when the fingerprint resolves, andFindingSubject.Groupis built from the first while the evidence is built from the second; hoisting the whole loop to pre-group would break that pairing.Spreadwas the other halfSame list, also once per cluster. It ordered every failure under a four-key comparator — materialising
ExecutionId.ToString("N")for each one — to read three entries off the front.It now keeps the earliest failure per key in one pass and orders those, which is the same answer: the entries of the ordered list that introduce a new key are exactly the per-key minima, and no two minima can tie (a tie needs the same run, attempt, test and execution id, which is one execution and so one key). The top-up branch, reached only below three distinct keys, selects into a bounded shortlist instead of sorting.
The last comparison key stays the hex form rather than
Guid.CompareTo, which orders differently. It is now formatted only for two attempts of one test in one run.Measured
500 runs × 1000 tests, all failing on one signature:
Emitted evidence byte-identical, at that size and at 200 × 500. At the issue's headline 1000 × 2000 the analysis is 339 ms.
Worth a reviewer's attention: what now bounds that store is
SignatureIndex.Buildsigning two million failed executions — a regex pipeline plus SHA-256 each — at 6.7 s. That is linear rather than a blow-up, so it is a separate question from this one, but it means the issue's "report in seconds at 1000 × 2000" is not reached by this change alone.Two corrections to the issue
context.Tests.ExecutionsOfis a dictionary probe (TestIndex.cs:85), so theCount(...)was the only quadratic term in that loop. The issue left it open.ReportEnvelopeTestscompares two runs against each other, which catches nondeterminism but not a changed value. The byte-comparison above was run against the pre-change provider instead.The issue's two "noted only so they are not fixed unnecessarily" items —
RetryProvider.MaskedExemplarsandDurationProvider.SessionMedians— were both re-checked and both left alone.Verification
dotnet test tests/Xping.Cli.Tests— 910 pass, 2 new.Nothing in the suite asserted
ClusterMember.Failuresat all, and nothing pinned which exemplars a cluster picks rather than how many. Both new tests pass against the pre-change provider, which is what makes them evidence that the selection is unchanged rather than merely self-consistent.🤖 Generated with Claude Code
https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc