fix(diagnostics): stop a superseded discardedReturn pass instead of r… - #583
Merged
msarson merged 1 commit intoSep 17, 2026
Merged
Conversation
…unning it to completion validateTextDocument runs its async validators 500 ms after the last edit. Its stale-version guard discards the answer when the document version has moved on, but the guard sat after all ten validators, so a superseded pass still did every bit of its work first. The discardedReturn validator resolves the receiver type of every dot-call in the document and enumerates each receiver class cross-file; its memos are keyed by document version, so each pass while typing is cold. On a 770 KB / 14,512-line module a cold pass ran 6.6-9.8 s with five superseded passes overlapping (about 1 s uncontended), all sharing the single thread with interactive requests: a member-completion request measured 24 ms alone and 1.9-3.4 s beside abandoned passes. Fix: validateTextDocument derives `isStale` (a newer document version exists) once, checks it before each validator, and passes it to validateDiscardedReturnValues, which checks it at the top of its per-line loop (at the existing time-slice yield) and returns an empty result as soon as the pass is superseded. Checking only between validators was not enough: discardedReturn is the second of ten and ran uninterrupted, so that check always fired after it had finished. Measured after the change: superseded passes end in 42-526 ms, and the same member- completion requests take 140-152 ms. Tests: ReturnValueDiagnostics.StaleAbort.test.ts (3). Without the fix the two stale-path tests fail (the callback is ignored and all four discarded-call warnings are returned); the control test passes both ways. With the fix the full suite is 3071 passing / 0 failing / 4 pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
msarson
added a commit
that referenced
this pull request
Sep 17, 2026
Co-Authored-By: Claude Opus 5 (1M context) <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.
fix(diagnostics): stop a superseded discardedReturn pass instead of running it to completion
Branch
fix/validation-stale-pass-abort, targetsversion-1.0.4.What happens
In a large module, member completion after a dot (
obj., thenobj.L,obj.Li) becomes slow — 1.9–3.4 s per keystroke on a 770 KB / 14,512-line file — while the same requests take 140–150 ms when nothing else is running. Bare-word completion on the same file stays fast. Instrumenting the server showed the member walk itself was unchanged (11–28 ms); the time went to sharing the single thread with background validation.Root cause
validateTextDocumentruns its async validators 500 ms after the last edit. It has a stale-version guard that discards the pass's answer when the document version has moved on — but the guard ran after all ten validators, so a superseded pass still did every bit of its work first.The
discardedReturnvalidator (ReturnValueDiagnostics.validateDiscardedReturnValues) resolves the receiver type of every dot-call in the document and enumerates each receiver class cross-file. Its memos are keyed by document version, so each pass during typing is cold: on the file above, 67 class enumerations and 98 receiver-type resolutions per pass. A cold pass ran 6.6–9.8 s with up to five superseded passes overlapping (about 1 s when running alone). Everyawaitinside a completion request let one of those passes advance, which is why member completion — many await points — stretched from 24 ms to 2.5 s while bare-word completion, with almost none, did not.Checking staleness only between validators does not help:
discardedReturnis the second of ten and runs uninterrupted, so that check always fired after it had finished.The fix
server/src/server.ts, invalidateTextDocument:server/src/providers/DiagnosticProvider.tsforwards the new optionalisStale?: () => booleanparameter, andserver/src/providers/diagnostics/ReturnValueDiagnostics.tschecks it at the top of its per-line loop, at the time-slice yield that already exists there:The pre-existing guard after the validators is unchanged and still discards the (now empty) result of a superseded pass.
Measured
With the in-loop check, superseded passes end in 42–526 ms instead of 6.6–9.8 s, only the pass for the final version runs all ten validators, and the same member-completion requests take 140–152 ms.
Testing
New
server/src/test/ReturnValueDiagnostics.StaleAbort.test.ts(3 tests) on a fixture with four discardedobj.Method()calls and aMemberLocatorServicewhose receiver-type resolutions are counted:Without the fix the two stale-path tests fail — the callback is ignored and all four warnings come back — while the control test passes either way. With the fix the full suite is 3071 passing, 0 failing, 4 pending.
Scope
Only
discardedReturnreceives the callback; it is the validator whose per-pass cost made superseded passes matter. The other async validators are short enough (tens of ms,undeclaredVarandmissingImpla few hundred) that the between-validator check covers them. Two related costs are deliberately left for their own changes: the version-keyed memos that make every pass cold in the first place (a warm pass of the same validator measures ~0.5 s against ~1 s cold and 6–10 s under contention), and the ~0.55 s of synchronous validators that run at the start of every pass. Sibling change in the same investigation:fix/member-scan-use-loaded-text.