Snapshot the collections before diffing them off-thread - #3361
Open
FrayxRulez wants to merge 1 commit into
Open
Conversation
SearchCollection computes its diff on the thread pool:
await incremental.LoadMoreItemsAsync(0);
var diff = await Task.Run(() => DiffUtil.CalculateDiff(this, source, ...));
Both arguments are collections the UI thread owns and is still appending to,
and CalculateDiff starts by copying them. So the pool thread runs Array.Copy
over a list whose backing array and count are being replaced underneath it, and
the reported ArgumentException comes straight out of Array.CopyImpl.
The guard that was meant to prevent this is a plain bool. UpdateImpl sets
_loading before awaiting, and LoadMoreItemsAsync returns early while it is set
- but LoadMoreItemsAsync also clears it when it finishes, so one that was
already in flight when UpdateImpl set it hands the door back while the diff is
still running. The next scroll then appends to the very collection being
copied. That interleaving needs a load to be in flight at the moment an update
starts, which is why it is rare and why the report shows half a minute of
continuous scrolling behind it.
Snapshot both on the UI thread and diff the snapshots. The diff only ever
needed the sequences, the indices it produces stay valid because items are
appended rather than inserted, and nothing else has to be synchronised.
The same lines were copied into ChatTextBox's autocomplete, so all four sites
are changed; only the first is evidenced by a crash.
Reported by crash telemetry on 12.9.1.
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.
ArgumentException— "Value does not fall within the expected range." Reported by crashtelemetry on 12.9.1.
Cause
SearchCollection.UpdateImplcomputes its diff on the thread pool:Both arguments are collections the UI thread owns and is still appending to, and
CalculateDiffbegins by copying them — which is exactly the frame that threw. The pool threadruns
Array.Copyover a list whose backing array and count are being replaced underneath it.The guard meant to prevent this is a plain
bool:UpdateImplsets_loading = truebefore awaiting, andLoadMoreItemsAsyncreturns earlywhile it is set.
LoadMoreItemsAsyncalso clears_loadingwhen it finishes. One that was already inflight when
UpdateImplset the flag hands the door back while the diff is still running, andthe next scroll appends to the very collection being copied.
That interleaving needs a load to be in flight at the moment an update starts, which is why it is
rare — and the report has about half a minute of continuous scrolling behind it, with
ProfilePage.OnViewChangingfiring every few hundred milliseconds while the profile's media tabpaged in.
The fix
Snapshot both collections on the UI thread and diff the snapshots.
CalculateDiffonly everneeded the sequences; the indices it returns stay valid because items are appended rather than
inserted, so
ReplaceDiffandUpdateItemsare unaffected. Nothing else has to be synchronised,and the cross-thread read is gone by construction rather than guarded.
Two arrays per update, on a path that runs when the query or sender changes — not a hot path.
The same lines were copied into
ChatTextBox's autocomplete, so all four sites are changed.Only the first is evidenced by a crash; the others are the same code with a smaller, shorter
lived collection behind it.
Not fixed here
_loadingremains a plainboolwritten by two different async methods, so it is still anunreliable guard for anything else that depends on it. It no longer has memory-safety
consequences, which is why this change stops where it does.
Build
Not built — a UWP/.NET Native build isn't available here. Both files pass
CSharpSyntaxTree.ParseText(...).GetDiagnostics(), which confirms they parse and nothing more.🤖 Generated with Claude Code