Skip to content

Snapshot the collections before diffing them off-thread - #3361

Open
FrayxRulez wants to merge 1 commit into
developfrom
searchcollection-diff-snapshot
Open

Snapshot the collections before diffing them off-thread#3361
FrayxRulez wants to merge 1 commit into
developfrom
searchcollection-diff-snapshot

Conversation

@FrayxRulez

Copy link
Copy Markdown
Collaborator

ArgumentException — "Value does not fall within the expected range." Reported by crash
telemetry on 12.9.1.

   0  System.Array.CopyImpl
   1  System.Array.Copy
   2  System.Collections.Generic.LowLevelList`1.CopyTo
   3  System.Collections.ObjectModel.Collection`1.CopyTo
   4  Rg.DiffUtils.DiffUtil.CalculateDiff<T>
   5  Telegram.Collections.SearchCollection`2.<UpdateImpl>b__0
      Telegram/Collections/SearchCollection.cs:114

Cause

SearchCollection.UpdateImpl computes its diff on the thread pool:

await incremental.LoadMoreItemsAsync(0);
var diff = await Task.Run(() => DiffUtil.CalculateDiff(this, source, DefaultDiffHandler, DefaultOptions));

Both arguments are collections the UI thread owns and is still appending to, and
CalculateDiff begins by copying them — which is exactly the frame that threw. The pool thread
runs Array.Copy over a list whose backing array and count are being replaced underneath it.

The guard meant to prevent this is a plain bool:

  • UpdateImpl sets _loading = true before awaiting, and LoadMoreItemsAsync returns early
    while it is set.
  • But LoadMoreItemsAsync also clears _loading when it finishes. One that was already in
    flight when UpdateImpl set the flag hands the door back while the diff is still running, and
    the 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.OnViewChanging firing every few hundred milliseconds while the profile's media tab
paged in.

The fix

Snapshot both collections on the UI thread and diff the snapshots. CalculateDiff only ever
needed the sequences; the indices it returns stay valid because items are appended rather than
inserted, so ReplaceDiff and UpdateItems are 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

_loading remains a plain bool written by two different async methods, so it is still an
unreliable 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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant