Skip to content

perf(cli): count a cluster's members once, not once per member (#175) - #201

Merged
xping-admin merged 2 commits into
mainfrom
perf/175-cluster-assembly
Sep 7, 2026
Merged

perf(cli): count a cluster's members once, not once per member (#175)#201
xping-admin merged 2 commits into
mainfrom
perf/175-cluster-assembly

Conversation

@xping-admin

Copy link
Copy Markdown
Collaborator

Closes #175.

SharedFailure counted 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. --since reads up to 1000.

The counts move into the index

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 deliberately. references and members are appended together only when the fingerprint resolves, and FindingSubject.Group is built from the first while the evidence is built from the second; hoisting the whole loop to pre-group would break that pairing.

Spread was the other half

Same 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:

index build cluster analysis
before 1842 ms 22 960 ms
after 1843 ms 75 ms

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.Build signing 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.ExecutionsOf is a dictionary probe (TestIndex.cs:85), so the Count(...) was the only quadratic term in that loop. The issue left it open.
  • There are no golden files in this repo. The acceptance bullet about byte-identical golden output had no artefact behind it; ReportEnvelopeTests compares 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.MaskedExemplars and DurationProvider.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.Failures at 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

#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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Comment thread src/Xping.Cli/Report/Providers/FailureModeProvider.cs
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/Xping.Cli/Report/Indexes/SignatureIndex.cs 97.65% <100.00%> (+0.07%) ⬆️
.../Xping.Cli/Report/Providers/FailureModeProvider.cs 96.16% <100.00%> (+0.41%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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
@xping-admin
xping-admin merged commit 60236ba into main Sep 7, 2026
2 checks passed
@xping-admin
xping-admin deleted the perf/175-cluster-assembly branch September 7, 2026 10:27
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.

perf(cli): SharedFailure cluster assembly is O(members × failures) and --since reads up to 1000 sessions

2 participants