react / react/react-native

Fabric: schedulerDidFinishTransaction picks oldest (not newest) pending transaction as merge target, corrupting mount order

Offen Anfängerfreundlich
#58,175 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Needs: Author Feedback Needs: Repro
Vorherrschende Sprache
C++
Sterne
127k
Forks
25.3k
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
4

Beschreibung

Description

FabricUIManagerBinding::schedulerDidFinishTransaction (Android, ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp) selects which pending mounting transaction an incoming transaction should merge into using a forward search:

auto pendingTransaction = std::find_if(
    pendingTransactions_.begin(),
    pendingTransactions_.end(),
    [&](const auto& transaction) {
      return transaction.getSurfaceId() == mountingTransaction->getSurfaceId();
    });

if (pendingTransaction != pendingTransactions_.end() &&
    pendingTransaction->canMergeWith(*mountingTransaction)) {
  pendingTransaction->mergeWith(std::move(*mountingTransaction));
} else {
  pendingTransactions_.push_back(std::move(*mountingTransaction));
}

std::find_if over begin()/end() returns the oldest queued transaction for a surface. Whenever a surface has more than one pending transaction at once — which can legitimately happen any time MountingTransaction::canMergeWith refuses a merge between two adjacent transactions — this picks the wrong merge target.

Repro scenario

Given pending transactions T1, T2 for the same surface (already queued separately because canMergeWith refused to combine them), and an incoming T3:

  • T3 was diffed by the renderer against shadow-tree state that already includes T2.
  • The forward search finds T1 first and merges T3 into it, producing [T1+T3, T2], which executes as T1 → T3 → T2.
  • But T3 was never diffed against a tree without T2 in it — running it before T2 desyncs the native view tree from the shadow tree that produced the diff.

This manifests as native-tree/shadow-tree divergence at mount-apply time: an insert lands at an index the real parent doesn't have (IndexOutOfBoundsException in addViewAt), or a remove resolves a stale parent tag that's no longer the expected ViewGroup (IllegalStateException in removeViewAt).

Expected behavior

A new transaction should only ever merge into the most recently queued pending transaction for its surface — the only one whose diffed-against tree state actually reflects everything already queued. That means the merge-target lookup should search from the back (rbegin()/rend()) instead of the front (begin()/end()).

Fix

Three-token change:

auto pendingTransaction = std::find_if(
    pendingTransactions_.rbegin(),
    pendingTransactions_.rend(),
    [&](const auto& transaction) {
      return transaction.getSurfaceId() == mountingTransaction->getSurfaceId();
    });

if (pendingTransaction != pendingTransactions_.rend() &&
    pendingTransaction->canMergeWith(*mountingTransaction)) {
  ...

Environment

React Native 0.86.0, Android (Fabric), observed downstream in a fork carrying an additional local patch that makes canMergeWith refuse certain merges (a DeleteCreate tag-pairing guard), which is what surfaces multi-entry pendingTransactions_ queues in practice. The underlying merge-target selection bug is present in schedulerDidFinishTransaction upstream regardless of that local patch — any code path that causes canMergeWith to refuse a merge (including future upstream guards) would trigger the same divergence.

Reproducibility

No deterministic manual repro is provided here — the bug is timing/queue-state dependent. The mechanism is provable deterministically with a unit test against MountingTransaction directly (construct 3 transactions, assert execution order), without needing device timing.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne in ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp und untersuche schedulerDidFinishTransaction zusammen mit MountingTransaction::canMergeWith. Erstelle in einem deterministischen Unit-Test drei Transaktionen, überprüfe die Ausführungsreihenfolge für mehrere ausstehende Transaktionen und bestätige, dass das Merge-Ziel die zuletzt eingereihte Transaktion ist.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
android, cpp, react-native
Bereich
mobile, mobile-dev
Issue-Typ
Bug
Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
78/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.