Make the correctness evidence reproducible in CI, and pin the shareability guarantee - #44
Merged
Conversation
…ivalence transcript The correctness evidence behind the recent performance changes -- a 4,007-resolution equivalence transcript, production differentials, race repros -- was run ad hoc on a laptop. None of it ran in CI. go-python-packaging had already shipped a race-safety fix whose regression test was INERT in its own CI, because that CI ran plain `go test`; a revert would have passed green. CI now runs `go test -race ./...`, with an explicit -timeout on every invocation that reaches the transcript test so a per-case hang guard can never collide with `go test`'s default package budget. index/shared_version_test.go shares ONE parsed version.Version across eight goroutines and covers both paths gpp fixes separately -- the packed integer key, and the padParts fallback that about a quarter of distinct real versions take -- plus PackageMetadata.SupportsPython with a shared target, which is the exported API whose documented concurrency contract was relaxed on the strength of prose. Fixture strings are chosen for the shape the hazard needs (trailing zero on the shared operand, shared operand strictly shorter) and a guard asserts that shape, including an allocation probe proving which of the two paths each entry takes.⚠️ Three corrections to claims this repository already made. Pinned back to go-python-packaging v0.5.0 with the padding race live, the ENTIRE existing suite passed under -race: nothing in it shared a parsed Version between goroutines. pep440set/verpos_race_test.go named resolver/concurrency_test.go as the test that reached the hazard and it never did; MockIndex.Versions and RSFIndex.Versions both re-parse from stored keys, so no index could hand two goroutines an aliased value; and concurrentIndex's doc claimed its Requires-Python was there "so Specifiers.Check runs" when the provider converts it to a version set and never calls Check. TestConcurrentResolutionsShareOneParsedInterpreter is added and LABELLED as non-discriminating, because it measures zero races at v0.5.0. TestResolutionTranscriptMatchesGolden converts the two-build differential into a build-against-golden diff on the committed excerpt: 125 cases per pull request, compared byte for byte, failing rather than skipping if the fixture is missing, with a CI step that fails if it skips anyway and matches the run's MODE so a nightly that lost its env var cannot pass by re-running the fast set. The full-snapshot differential genuinely cannot run in CI and the file says so rather than pretending otherwise. Minimums are on DISCRIMINATING cases -- dependency walks, activated extras, derived conflicts -- not on cases compared, which is what an earlier differential counted while covering none of its own subject. Failures whose whole report is "this package is absent" are counted as vacuous and credited to nothing. Verified by mutation: an inverted ranking policy and a report-only rendering change are both caught. Twenty excerpt packages that are essentially the whole sweep cost are held out of the per-PR run and covered by a new nightly job against a second golden; `hypothesis` is excluded from both, having failed to finish under bounds of 20 s, 60 s, 3 min and 10 min. The sweep halts on the first timeout so a hang costs one deadline rather than N, and coverage minimums are suppressed on a halted sweep so one hang does not produce four confident errors naming the wrong cause. The golden writer refuses to run when the sweep failed, and when GPR_TRANSCRIPT_REPORTS=full would inline every report -- a path reachable by following the file's own investigation instructions. CI cost on the runner: build job 22-31 s (six runs on main) to 143-186 s (five runs on this branch), of which -race is 101-120 s.
…e figures with their load Rebased onto 11da678, which brings go-python-packaging v0.7.0 and a rewritten pep440set comparison ladder.⚠️ RE-VERIFIED RATHER THAN ASSUMED, because the fail-without-fix evidence had been gathered against a different implementation than the one this now merges onto. On the rebased branch, pinned back to v0.5.0, all seven subtests still report WARNING: DATA RACE individually (4 x 7 for Compare, 3 x 2 for SupportsPython), and the packable/unpackable allocation guards still hold at v0.7.0 -- so the packed-key and padParts paths are both still covered. The goldens also still match, which says the pep440set rewrite did not change any rendered version-set text. The index package does not import pep440set, so the rewritten verPos ladder is not on the path these tests reach; that is now checked rather than asserted.⚠️ A self-inconsistency of mine that survived five review rounds: concurrency_test.go listed TestConcurrentResolutionsShareOneParsedInterpreter among 'the tests that DO cover it' while that test's own doc says at length that it measures zero races at v0.5.0 and does not. Corrected. Figures are now quoted WITH the machine load they were taken at, because the full sweep measured 231 s during this rebase against the 95-110 s in the comment -- load average 63 on the same machine. Both the absolute times and the fast/full ratio move with load (16-19x idle, 10.7x loaded), so the split decision now rests on the order of magnitude, which is stable, rather than on a multiplier that is not. The nightly job is noted as the only direct measurement of that sweep on a runner. index/types.go's 'this module requires v0.6.0' is updated, and the paragraph now says why it is no longer only a paragraph.
jonyoder
force-pushed
the
ci/reproducible-evidence
branch
from
August 14, 2026 18:22
7ed6b6b to
3216009
Compare
jonyoder
added a commit
that referenced
this pull request
Aug 14, 2026
RSFIndex.Versions memoized the sorted order of a package's version keys and re-parsed every one of them on every call. It held strings because it had to: until go-python-packaging v0.6.0 a version.Version could not be shared between goroutines, since Version.Compare padded the shorter operand's release segment with append into spare capacity a by-value copy shares. v0.6.0 fixed that -- packable versions never touch part.Parts, and the fallback pads by copying -- and this takes the memo the fix unblocks. Warm resolution is 1.13x to 3.23x faster and allocates 41% to 94% fewer objects against the production snapshot, measured at base 11da678 over nine interleaved rounds. Cold does not improve, as expected: the first call already parsed the keys in order to sort them. candvers, metadata and the pin set are identical on every corpus entry, and 4,007 resolutions produce byte-identical transcripts with zero one-sided timeout exclusions.⚠️ These figures are larger than the same change measured against 6c13230 (1.24x and 2.27x). #46 shrank the denominator rather than the parse: parseKeys went from 47.4% of wide-versions' Resolve to 69.1%. Neither figure is quotable against the other's base. The returned slice is a COPY of the memo's, because cmd/pyresolve's `versions` subcommand sorts the result in place. Measured at -0.3% to +9.7% warm against a variant that shares -- higher than the "under 2%" this was scoped with, for the same denominator reason. It is also redundant on this module's own resolution path, since provider hands the result straight to candidate.Rank, which copies again; it protects the exported contract, and an internal accessor that skips it is a follow-up worth taking at that price. The cost is retained heap rather than churn, measured rather than estimated: a warmed index keeps roughly three times as much alive after a resolve (app-set 0.35 -> 0.99 MB, wide-versions 1.31 -> 4.41 MB), identical at both bases. The memo is left unbounded to match the blob cache's rationale, and the code now says plainly that a bound is a prerequisite for embedding this in a long-lived server. index/shared_memo_test.go shares one warmed memo across eight goroutines over fixtures chosen for the padding hazard's actual shape, covering both of v0.6.0's safety arguments and also driving version.ReleaseKey, which #46 made a second reader of a shared parsed version. Every fixture reports WARNING: DATA RACE under a v0.5.0 pin, 20 fresh processes each.⚠️ That guard is inert until the -race step in #44 lands: gpr CI runs plain `go test`, under which the equivalent pre-existing check catches a deleted defensive copy 0 times in 20. Sweeps the rationales that asserted the memo was impossible, in index/rsfindex.go, index/mock.go, provider/provider.go and resolver/bench_test.go. The CHANGELOG diff is pure insertion; no dated section is touched.
jonyoder
added a commit
that referenced
this pull request
Aug 14, 2026
RSFIndex.Versions memoized the sorted order of a package's version keys and re-parsed every one of them on every call. It held strings because it had to: until go-python-packaging v0.6.0 a version.Version could not be shared between goroutines, since Version.Compare padded the shorter operand's release segment with append into spare capacity a by-value copy shares. v0.6.0 fixed that -- packable versions never touch part.Parts, and the fallback pads by copying -- and this takes the memo the fix unblocks. Warm resolution is 1.13x to 3.23x faster and allocates 41% to 94% fewer objects against the production snapshot, measured at base 11da678 over nine interleaved rounds. Cold does not improve, as expected: the first call already parsed the keys in order to sort them. candvers, metadata and the pin set are identical on every corpus entry, and 4,007 resolutions produce byte-identical transcripts with zero one-sided timeout exclusions.⚠️ These figures are larger than the same change measured against 6c13230 (1.24x and 2.27x). #46 shrank the denominator rather than the parse: parseKeys went from 47.4% of wide-versions' Resolve to 69.1%. Neither figure is quotable against the other's base. The returned slice is a COPY of the memo's, because cmd/pyresolve's `versions` subcommand sorts the result in place. Measured at -0.3% to +9.7% warm against a variant that shares -- higher than the "under 2%" this was scoped with, for the same denominator reason. It is also redundant on this module's own resolution path, since provider hands the result straight to candidate.Rank, which copies again; it protects the exported contract, and an internal accessor that skips it is a follow-up worth taking at that price. The cost is retained heap rather than churn, measured rather than estimated: a warmed index keeps roughly three times as much alive after a resolve (app-set 0.35 -> 0.99 MB, wide-versions 1.31 -> 4.41 MB), identical at both bases. The memo is left unbounded to match the blob cache's rationale, and the code now says plainly that a bound is a prerequisite for embedding this in a long-lived server. index/shared_memo_test.go shares one warmed memo across eight goroutines over fixtures chosen for the padding hazard's actual shape, covering both of v0.6.0's safety arguments and also driving version.ReleaseKey, which #46 made a second reader of a shared parsed version. Every fixture reports WARNING: DATA RACE under a v0.5.0 pin, 20 fresh processes each. That guard runs in CI on the -race step #44 added, which this is rebased onto. Without it these tests are a green no-op: measured on a tree with the defensive copy deleted, the pre-existing concurrency check catches it 8 times in 20 under -race and 0 times in 20 without. Sweeps the rationales that asserted the memo was impossible, in index/rsfindex.go, index/mock.go, provider/provider.go and resolver/bench_test.go. The CHANGELOG diff is pure insertion; no dated section is touched.
jonyoder
added a commit
that referenced
this pull request
Aug 14, 2026
RSFIndex.Versions memoized the sorted order of a package's version keys and re-parsed every one of them on every call. It held strings because it had to: until go-python-packaging v0.6.0 a version.Version could not be shared between goroutines, since Version.Compare padded the shorter operand's release segment with append into spare capacity a by-value copy shares. v0.6.0 fixed that -- packable versions never touch part.Parts, and the fallback pads by copying -- and this takes the memo the fix unblocks. Warm resolution is 1.13x to 3.23x faster and allocates 41% to 94% fewer objects against the production snapshot, measured at base 11da678 over nine interleaved rounds. Cold does not improve, as expected: the first call already parsed the keys in order to sort them. candvers, metadata and the pin set are identical on every corpus entry, and 4,007 resolutions produce byte-identical transcripts with zero one-sided timeout exclusions.⚠️ These figures are larger than the same change measured against 6c13230 (1.24x and 2.27x). #46 shrank the denominator rather than the parse: parseKeys went from 47.4% of wide-versions' Resolve to 69.1%. Neither figure is quotable against the other's base. The returned slice is a COPY of the memo's, because cmd/pyresolve's `versions` subcommand sorts the result in place. Measured at -0.3% to +9.7% warm against a variant that shares -- higher than the "under 2%" this was scoped with, for the same denominator reason. It is also redundant on this module's own resolution path, since provider hands the result straight to candidate.Rank, which copies again; it protects the exported contract, and an internal accessor that skips it is a follow-up worth taking at that price. The cost is retained heap rather than churn, measured rather than estimated: a warmed index keeps roughly three times as much alive after a resolve (app-set 0.35 -> 0.99 MB, wide-versions 1.31 -> 4.41 MB), identical at both bases. The memo is left unbounded to match the blob cache's rationale, and the code now says plainly that a bound is a prerequisite for embedding this in a long-lived server. index/shared_memo_test.go shares one warmed memo across eight goroutines over fixtures chosen for the padding hazard's actual shape, covering both of v0.6.0's safety arguments and also driving version.ReleaseKey, which #46 made a second reader of a shared parsed version. Every fixture reports WARNING: DATA RACE under a v0.5.0 pin, 20 fresh processes each. That guard runs in CI on the -race step #44 added, which this is rebased onto. Without it these tests are a green no-op: measured on a tree with the defensive copy deleted, the pre-existing concurrency check catches it 8 times in 20 under -race and 0 times in 20 without. Sweeps the rationales that asserted the memo was impossible, in index/rsfindex.go, index/mock.go, provider/provider.go and resolver/bench_test.go. The CHANGELOG diff is pure insertion; no dated section is touched.
jonyoder
added a commit
that referenced
this pull request
Aug 14, 2026
RSFIndex.Versions memoized the sorted order of a package's version keys and re-parsed every one of them on every call. It held strings because it had to: until go-python-packaging v0.6.0 a version.Version could not be shared between goroutines, since Version.Compare padded the shorter operand's release segment with append into spare capacity a by-value copy shares. v0.6.0 fixed that -- packable versions never touch part.Parts, and the fallback pads by copying -- and this takes the memo the fix unblocks. Warm resolution is 1.13x to 3.23x faster and allocates 41% to 94% fewer objects against the production snapshot, measured at base 11da678 over nine interleaved rounds. Cold does not improve, as expected: the first call already parsed the keys in order to sort them. candvers, metadata and the pin set are identical on every corpus entry, and 4,007 resolutions produce byte-identical transcripts with zero one-sided timeout exclusions.⚠️ These figures are larger than the same change measured against 6c13230 (1.24x and 2.27x). #46 shrank the denominator rather than the parse: parseKeys went from 47.4% of wide-versions' Resolve to 69.1%. Neither figure is quotable against the other's base. The returned slice is a COPY of the memo's, because cmd/pyresolve's `versions` subcommand sorts the result in place. Measured at -0.3% to +9.7% warm against a variant that shares -- higher than the "under 2%" this was scoped with, for the same denominator reason. It is also redundant on this module's own resolution path, since provider hands the result straight to candidate.Rank, which copies again; it protects the exported contract, and an internal accessor that skips it is a follow-up worth taking at that price. The cost is retained heap rather than churn, measured rather than estimated: a warmed index keeps roughly three times as much alive after a resolve (app-set 0.35 -> 0.99 MB, wide-versions 1.31 -> 4.41 MB), identical at both bases. The memo is left unbounded to match the blob cache's rationale, and the code now says plainly that a bound is a prerequisite for embedding this in a long-lived server. index/shared_memo_test.go shares one warmed memo across eight goroutines over fixtures chosen for the padding hazard's actual shape, covering both of v0.6.0's safety arguments and also driving version.ReleaseKey, which #46 made a second reader of a shared parsed version. Every fixture reports WARNING: DATA RACE under a v0.5.0 pin, 20 fresh processes each. That guard runs in CI on the -race step #44 added, which this is rebased onto. Without it these tests are a green no-op: measured on a tree with the defensive copy deleted, the pre-existing concurrency check catches it 8 times in 20 under -race and 0 times in 20 without. Sweeps the rationales that asserted the memo was impossible, in index/rsfindex.go, index/mock.go, provider/provider.go and resolver/bench_test.go. The CHANGELOG diff is pure insertion; no dated section is touched.
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.
Test and CI only. No behaviour change, no tag.
The correctness evidence behind the last few performance changes — a 4,007-resolution equivalence transcript, production differentials, race repros — was run ad hoc on a laptop. None of it ran in CI. There is already a concrete instance of that failure mode:
go-python-packagingshipped a race-safety fix whose regression test was inert in its own CI, because CI ran plaingo test; a revert would have passed green.1.
go test -race ./...in CI2. The shareability guarantee is pinned
PackageMetadata.SupportsPython's doc was relaxed in 0.6.0 to tell callers a parsedversion.Versionis safe to share. That was true and it was backed by prose.index/shared_version_test.gonow shares one parsed value across eight goroutines and covers both of the paths 0.6.0 fixes separately:part.Parts(the packed integer key);padPartsfallback, which about a quarter of distinct real versions take under an entirely separate safety argument.Fixture strings are chosen for the shape the hazard needs — trailing zero on the shared operand, shared operand strictly shorter — and
TestSharedVersionFixturesReachBothPathsasserts that shape rather than trusting the comment, including an allocation probe that proves which of the two paths each entry actually took (packed allocates nothing;padPartscopies).Discrimination, verified
Each subtest was run against a scratch copy with
go-python-packagingpinned back to v0.5.0. All seven reportWARNING: DATA RACEthere and are clean at v0.6.0.TestSharedParsedVersionIsRaceFree/packable.../unpackable-local.../unpackable-epoch.../unpackable-long-releaseTestSupportsPythonSharedTargetIsRaceFree/packable-less-than.../packable-greater-than.../unpackable-epochPinned to v0.5.0 with the padding race live, the entire existing suite passed under
-race. Nothing in it shared a parsedversion.Versionbetween goroutines:resolver/concurrency_test.gowas named inpep440set/verpos_race_test.goas "the test that reaches it, through cross-group Compare in ranking". It does not, and it says so now. It deliberately shares nothing parsed, andMockIndex.Versionsre-parses from stored string keys on every call, so no fixture edit could have changed that.RSFIndex.Versionsdoes the same. No index in this module can hand two goroutines an aliased value.concurrentIndex's doc said itsRequires-Pythonwas there "soSpecifiers.Checkruns". It is not: the provider convertsRequires-Pythonto a version set ininterpreterDependencyand never callsCheck, andSpecifiers.Checkhas no non-test caller outsideSupportsPython— which is an API for external callers, not a step of a resolve.TestConcurrentResolutionsShareOneParsedInterpreteris added and labelled as non-discriminating: it shares the one thing a real caller shares, and measured against v0.5.0 it reports zero races, for the three reasons above. It is kept for the caller-visible shape and as a tripwire if any of them stops holding — not counted as coverage.3. The equivalence transcript runs in CI
TestResolutionTranscriptMatchesGoldenconverts the two-build diff into a build-against-golden diff on the committed excerpt: 125 cases per PR, compared byte for byte againstresolver/testdata/excerpt-transcript.txt. It fails rather than skips when the fixture is missing, and CI fails if it skips anyway — the same pattern as the producer-output step, added for the same reason. The CI guard matches the run's mode, not just the summary's shape, so a nightly that lostGPR_TRANSCRIPT_FULLfails instead of silently re-running the PR subset.writeTranscriptis shared withTestDumpResolutions, so the CI check and the env-gated full-snapshot tool cannot drift into rendering the same resolution differently.4. Anti-vacuity
The minimums are on discriminating cases, not cases compared: resolutions that pinned a dependency (
Deep), that activated an extra, and failures that derived a conflict. Failures whose whole report is "this package is absent" are counted asVacuousand credited to nothing — that class is exactly what an earlier differential counted as coverage.Current excerpt, per-PR set:
transcript[fast]: 125 cases, 45 discriminating (31 deep, 14 derivations); 72 shallow, 2 extras, 8 vacuous, 0 unparseable, 0 timeouts.Verified by mutation, not by argument:
Newest.Lessinverted (oldest-first ranking)flask 3.1.3→flask 0.11, andextrasfell to 1, tripping the minimummarkerWithPrerendering changed (report text only)sha256mismatch onpandas numpy<1.265. CI runtime, measured on the runner
Test(go test ./...)Race(go test -race ./...)Equivalence transcriptThe
-racestep is 80% of the increase. To hold the equivalence half to 14 s, 20 excerpt packages that account for nearly all of the sweep's cost are held out of the per-PR run (95 s full vs 6 s subset, locally) and covered by a new nightly job against a second golden file.Second commit: fixes from review
TIMEOUTline forhypothesis— a committed assertion about wall clock, i.e. exactly the fragility the by-name exclusion exists to avoid, reintroduced through the nightly, and it would have made that job red on a runner.hypothesisturns out not to finish at all on this excerpt: it exceeds a ten-minute per-case bound. It moves to a newunboundedExcerptPackageslist excluded from both modes, because no deadline makes its entry a fact about the resolver rather than about the machine.transcriptStatsnow counts timeouts and asserts there are none — the check that would have caught this.Also corrected: the format header was written only by the golden test, leaving
TestDumpResolutions— the tool that produced the v1 transcripts the header exists to distinguish — the one unmarked artifact;minCaseswas described as "roughly half" when it is 72%;firstDifferencereported a differing case header as its own context.Notes
lines=N bytes=N sha256=…plus the conclusion line. Full text would make the golden file 1.15 MB — larger than the RSF fixture it derives from — of which 1.0 MB is nine reports enumerating whole version sets. The hash keeps every byte of sensitivity;GPR_TRANSCRIPT_REPORTS=fullis there for investigating a mismatch.# gpr-transcript v2header prevents a new transcript being silently diffed against the v1 files behind the published 0.6.0 figures.Third commit: second round of review findings
Two were defects in the checks themselves:
caseTimeoutwas 10 minutes — exactlygo test's default-timeout— and none of the threeci.ymlsteps that run this test passes-timeout. A genuinely hung case would trip the package timeout and panic the binary beforewriteTranscriptcould record aTIMEOUTand beforestats.Timeoutswas evaluated. The assertion added in the second commit, credited there as "the check that would have caughthypothesis", could not fire in the PR job at all; only the nightly (-timeout 60m) was unaffected. Now 2 minutes — ~6× above the worst measured case, ~5× below the package default.ERRORclass was counted nowhere and asserted nowhere — the exact defectstats.Timeoutshad just been added to fix. It is reachable (resolver.go:127,:205,:212) and incremented onlyCases. WithminDeepResolutionsat 15 against an actual 31, half the deep cases could have degraded intoERRORand every minimum would still have passed. Now counted, asserted zero, and the classes are asserted to sum toCasesso this cannot recur a third time.One overclaim, measured false:
sharedInterpreter/sharedInterpreterConstraintclaimed their spellings were "the entire test" and that>=3.8would have made it green at v0.5.0. It is green either way —Specifiers.Checkis never reached from a resolve. Same overclaim this branch corrected inconcurrentIndexone commit earlier, two files over.Figures corrected against fresh measurement: the full-report golden is 1,465,675 bytes, not 1.15 MB, and its largest report is
ipykernelat 293 KB, notprompt-toolkitat 227 KB;ipykernel/ipythonare 19.9 s and 18.7 s ("38 s each" was the pair); two stale21s; "roughly fifteen" used the transcript's ratio, not the repository's; the CHANGELOG quoted the first commit's build job (178 s) rather than the head's (168 s).Three rationales were wrong despite correct conclusions: the
ci.ymlsummary grep catches a run that never executed, not one that compared nothing; the nightly'sconcurrencyblock serialises rather than deduplicates; andfixtureDepth = 4is a dependency-depth criterion, so "none of that criteria mentions dependency depth" was false.Also documented: the race detector collapses repeated stacks, so running the whole
indexpackage against a broken pin shows one failing subtest, not four. That is deduplication, not three uncovered paths — each was confirmed individually.Fourth commit: third round of review findings
caseTimeoutmargin was a laptop margin. Two minutes was justified as "about six times above the worst measured case" — 120 s over a 21.5 s figure measured on an M4 Max, weighed against a bound that has to hold on a runner. Scaled by the observed ratio (go test ./...is 7.1 s locally, 23 s on the runner), the worst case is nearer 70 s and the real margin was 1.7×, on a check whose failure mode is exactly the machine-dependentTIMEOUTthe previous commit removed.Fixed from both ends rather than by trading one margin against the other:
caseTimeoutis 5 minutes, ~4.3× the runner-adjusted worst case;ci.ymlpasses an explicit-timeout 20mon all three invocations that reach this test, so the package budget no longer depends ongo test's default and can no longer collide withcaseTimeoutby coincidence. Five minutes is still under that default, so a bare localgo test ./...is armed too.Newly documented because it is load-bearing and was not: the PR job survives this bound only because
slowExcerptPackagesholds the expensive cases out of the fast set — it runs under-race, which costs a further ~4×.The cost table is re-measured whole, under one methodology (cold single-package resolve, fresh index each — an upper bound on marginal sweep cost), rather than patched two entries at a time. The previous note called the figures "lower bounds" and said they "do not sum to the 95 s total"; both were backwards. Cold consistently exceeds in-sweep, and the twenty sum to 106 s against a 95–100 s sweep. All twenty figures moved.
hypothesishas now been measured against 20 s, 60 s, 3 min and 10 min bounds and exceeded all four, against 21.5 s for the slowest package that finishes.Three claims corrected for overreach rather than error: the
Errors/Timeoutsassertions were justified as catching cases that would "vanish", but the golden comparison catches them too — what the counters add is a message naming the class, plus cover for a golden regenerated while cases were timing out (which is how one got committed). And the sum assertion was credited with catching the uncountedERRORclass; it is a runtime check that would have sat green through both commits whereERRORwas uncounted and empty. It catches the co-occurrence, not the omission.Smaller:
Errorscannot be a cancelled context (caseCtx.Err()is tested first, routing those toTimeouts) — the reachable case is the solver giving up; the sum failure message now explains a negative count rather than printing one;String()documents thatextrasoverlapsdeepby design; three stale "order of magnitude" phrases; and the CHANGELOG no longer implies its per-step figures reconcile to the second.Fifth commit: fourth round of review findings
Two behavioural defects, both verified fixed by running them:
writeTranscriptcontinued past a timeout, so N hung cases cost N ×caseTimeoutand the guard's reach depended on how many things hung. At 5 minutes exactly one hang fits inside a barego test's 10-minute package budget; at the 2 minutes it replaced, four did — so raising the constant made the local path strictly worse, and the compensating-timeouthad been added only toci.yml. Hangs also arrive in classes, not singly (14 of the 125 fast cases already sit above a 200 ms bound), so a regime shift would hang many at once and panic the binary rather than fail with a message. The sweep now halts on the first timeout. Verified with a 50 ms bound: stops at case 6 of 125, sum still balances.assertTranscriptIsDiscriminatingusest.Errorf, and theGPR_UPDATE_TRANSCRIPTbranch ran unconditionally afterwards — so a regeneration whose sweep timed out wrote theTIMEOUTline straight into the golden, signalled only by exit status. That is how one got committed two commits ago. It now refuses to write when the run has already failed. Verified: golden checksum unchanged after a failing regeneration attempt.Figures corrected, all of them mine:
-racecostThe
-raceunderstatement inverted its own paragraph's conclusion, and the "four or five times" error is the exact laptop-vs-runner mistake thecaseTimeoutnote calls out by name, made a hundred lines above it.String()now renders the partition as an explicit sum with the overlay outside it — its previous doc claimed extras sat "after the semicolon with the other non-class counts", of which there were none. CI greps updated to match.A note on the evidence itself
While this PR was being written, concurrent work on the same machine destroyed three of five baseline rounds belonging to a different measurement running alongside it. The surviving files still existed, still parsed, and would still have produced a plausible-looking median. Nothing in the output would have said it was wrong.
That is the sharper version of this PR's thesis: an ad hoc measurement is not merely unrepeatable, it is undetectably corruptible. Every figure quoted here was re-derived afterwards from freshly created, uniquely-prefixed directories, and the CI timings come from the GitHub API rather than from any local file. A CI check has neither problem — it reruns on demand and derives its inputs from the committed tree. That reasoning is now recorded at the top of
equivalence_test.go.Sixth commit: fifth round of review findings
t.Failed()write guard only caught poisonings a counter notices.GPR_TRANSCRIPT_REPORTS=fullinlines every failure report and moves no counter at all — so the run passes and writes a golden 20× larger (240 KB fast; byrenderFailure's own figure 1.47 MB in full mode, larger than the RSF fixture, in a public repo).It is reachable by following the instructions, not by misusing them:
renderFailure's doc and the golden-mismatch message both tell the reader to re-run withGPR_TRANSCRIPT_REPORTS=fullto investigate, and doing so withGPR_UPDATE_TRANSCRIPTstill exported from the previous command silently rewrites the baseline from a green run. Now refused explicitly — verified blocked, golden checksum unchanged. The previous commit's claim that the counters "PREVENT a poisoned golden" is corrected to what they actually do: they prevent the ones a counter can see.go test ./..., which on the runner compiles every package; the marginal cost of twenty more resolutions contains no compile at all. That is the same species of error as the draft it replaced.The like-for-like number was available all along — the equivalence step runs with
-v, so CI logs the test body:Recomputed: the extra sweep is 169–260 s and the repository's test time goes to roughly 190–280 s — an order of magnitude, stated as such rather than to two figures. The straddle band becomes 184–414 s against the 300 s guard, so the conclusion survives — and that is the point. It is now the conclusion that doesn't depend on where in the band you land, rather than one produced by picking a convenient multiplier, which both previous drafts did, in opposite directions.
Halting turned one hang into five errors, four naming the wrong cause. A hang at case 6 of 125 reported "the excerpt or the corpus has shrunk" plus three collapsed minimums — none of which happened. Coverage minimums are now skipped when the sweep halted, and the timeout error says so. Verified: one diagnosis, not five.
Also:
writeTranscript's doc comment had been left attached to thetranscriptRunstruct inserted inside it, so godoc read the function's documentation as the type's — a comment describing something other than what it's attached to, in a commit series about exactly that. And the halt rationale claimed it "costs nothing real" while the paragraph above it argued the class size is the diagnostic signal; halting does lose that, and now says so.CI range widened to 143–186 s over five runs; per-step ranges re-checked across all five and unchanged.
Rebased onto
11da678(gpp v0.7.0) — and the discrimination re-verifiedmainmoved under this PR: #46 rewrotepep440set's comparison ladder and bumped go-python-packaging to v0.7.0, and #47 dated the release. Branch collapsed to a single commit and rebased; conflicts inCHANGELOG.mdandpep440set/verpos_race_test.goresolved by keeping both sides rather than picking one — main's updated mechanism note plus this branch's correction, and my entries under[Unreleased]with main's dated[0.7.0]section preserved below them.TestSharedParsedVersionIsRaceFreeTestSupportsPythonSharedTargetIsRaceFreeAlso re-checked at v0.7.0: the packable/unpackable allocation guards still hold, so both the packed-key path and the
padPartsfallback are still genuinely covered; and the goldens still match, which independently says thepep440setrewrite changed no rendered version-set text.The
indexpackage does not importpep440set, so the rewrittenverPosladder is not on the path these tests reach — now checked rather than asserted.concurrency_test.golistedTestConcurrentResolutionsShareOneParsedInterpreteramong "the tests that DO cover it", while that test's own doc says at length that it measures zero races at v0.5.0 and does not cover the hazard. Corrected.Figures now carry their load
The full sweep measured 231 s during this rebase against the 95–110 s in the comment — same machine, load average 63.
Both the absolute times and the ratio move with load, so the split decision now rests on the order of magnitude — stable across both columns — rather than on a multiplier that isn't. The nightly job is noted as the only direct measurement of that sweep on a runner; everything in the test file is local or extrapolated.
index/types.go's "this module requires v0.6.0" is updated, and that paragraph now says why it is no longer only a paragraph.