Summary:
# What is this?
For a very long time, we've discussed the possibility of detecting Node Reparenting in the Fabric Differ. Practically, from the developer perspective, ReactJS and React Native do not allow reparenting: nodes cannot be reparented, only deleted and then recreated with entirely new tags.
However, Fabric introduced the idea of View Flattening where views deemed unnecessary would be removed from the View hierarchy entirely. This is great and improves memory usage, except for one issue: if a View becomes unflattened, or becomes flattened, the entire tree underneath it must be rebuilt.
In a past diff we introduced a mechanism to detect sibling reordering cleverly, and produce a minimal instruction set. This diff is very similar: we know the invariants around flattening and unflattening of views and we take advantage of them to produce an optimal set of instructions efficiently.
# What's different from previous attempts?
No global maps! Those are slow!
This seems to work and (hopefully) might even improve performance, since way less work is being done on the UI thread in cases when views are (un)flattened.
This *only* does extra work when flattening/unflattening happens, which gives product engineers a little more control over perf.
# So, how's it work?
This algorithm is intuitively simple (I think) but tricky to pull off, because there are lots of edge-cases.
In short: In the past, that information was hidden from the Differ: the differ didn't know if views were being reparented, it would see them
as entirely new views or as views being deleted if a View was flattened or unflattened. We very subtly change the information given to the differ:
all nodes are visible to the differ, but marked as Flattened or Unflattened. Thus, when the differ compares two nodes in the "old" and "new" tree,
it can tell not just if there are updates to the node but if it has been unflattened or flattened as well.
For example, take this tree, where * indicates that a View is flattened:
```
A
+
+----+---+
B* X
+ +
| |
+---+--+ +
E F Y
```
When the Differ asks for the children of A, in the past it would get a list `[E, F, X]`. That is, B* and X are both its children, but since B is flattened, it is omitted entirely from the list and
its children are substituted.
Now, when the Differ asks for the children of A, we give it this list instead: `[B*, E, F, X]`. That is: we give it a list which includes B, but B is marked as flattened.
Another wrinkle: A node `X` could have its children flattened, but still be a concrete view: so flattening/unflattening is a different operation from making a view "concrete" or "unconcrete", which can change independently of flattening.
There is one additional wrinkle: because of zIndex/stacking order, the children of `B` might not actually appear after `B` in the list. Depending on zIndex, a tree that looks like this:
```
A
+
+------+------+
B* C*
+ +
| |
+--+--+ +--+--+
D E F G
```
Could actually be linearized as: `[D G B* F C* E]` (as an extreme example; but basically all permutations as possible).
This is the reason, and the *only* reason that the inner Flattener/Unflattener
## The cases we need to handle
There are 7 cases/edge-cases of flattening and unflattening that we need to handle. Practically, all cases of reordering + flattening/unflattening, and taking recursive cases into account:
1. View A and A' (A in the old tree, A' in the new tree) are matched in the differ, and A* has been flattened or unflattened. These two cases are the easiest to handle.
2. View A' has been reordered with its siblings, and has been flattened or unflattened. These cases are slightly trickier to handle.
3. While flattening or unflattening, we encounter a child that has also been unflattened or flattened. So we need to handle four cases here in total: Flatten-Flatten, Flatten-Unflatten, Unflatten-Flatten, and Unflatten-Unflatten.
Other things to think about, also covered above:
1. Ordering. Views can be reordered and flattened/unflattened at the same time.
2. zIndex ordering: children in a certain order from the ShadowNode perspective may be stacked differently from a View perspective. We use the zIndex ordering for everything in the differ, and this prevents us from performing certain optimizations (see above: we cannot assume that children come after their parent in a list; they may come before, may be interwoven with children from other parents, etc).
# Perf Implications?
Practically, there should be very little negative overhead. There is some overhead in actually performing a flattening/unflattening operation, but... not much more than before. We don't use global maps, so the cost of flattening/unflattening is basically `O(number of nodes reparented)` - note that that's direct nodes reparented, *not* descendants.
tl;dr the perf hit should be similar to reordering, which is non-zero, but close to zero, and zero-cost for any diff operations on parts of the tree that don't involve flattening/unflattening. AFAICT this is very close to an ideal solution for that reason (but I wish it was simpler overall).
# In Summary?
I hope this works out and I think it could improve a number of things downstream: perf, LayoutAnimations, Bindings, certain crashes because of platform assumptions about mutations, etc.
Is it worth it? This new implementation is substantially harder to reason about, harder to read, and harder to understand. This is an important consideration. All I can say there is that I trust the test suite I've been using, but
the decreased readability is a big negative. Hopefully we can improve this in the future.
The rest is fiddly implementation details that I sincerely hope can be improved and simplified in the future.
# Followups?
The part that makes this algorithm the most expensive is that because of zIndex ordering, we cannot assume that children are linearized after their parents and so we rely more heavily on maps for the flattening/unflattening. Our TinyMap implementation should make these `find` operations fast enough unless trees' children are constantly being reordered, but it's still worth thinking of ways to make this even faster.
Changelog: [Internal]
Reviewed By: shergin, mdvacca
Differential Revision: D23259341
fbshipit-source-id: 35d9b90caf262d601a31996ea2cb37e329c61ffc
Summary:
1. When testing major changes to the differ, it can be useful to have more verbose logging.
2. On Android, since asserts don't fire yet, I log which asserts are failing.
Should have no impact on any builds unless you manually set the macro here, and it will have no impact on production builds regardless.
Changelog: [Internal]
Reviewed By: shergin
Differential Revision: D23257859
fbshipit-source-id: 94a8e74ece8023064de0f2203db6074975f8f1f0
Summary:
Simplify the TextInput measurement mechanism.
Now, data only flows from JS->C++->Java and from Java->JS. C++ passes along AttributedStrings from JS if JS updates, and otherwise Java maintains the only source of truth.
Previously we tried to keep all three in sync. This was complicated, slow, and even lead to some crashes.
This feels a bit hacky but I believe it's the simplest way to achieve this short-term. Ideally, we would use something like `AttributedStringBox` and pass that to State from Java,
but currently everything passed through the State system from Java must be serializable as `folly::dynamic`. So, instead, we just cache one Spannable per TextInput component and
use ReactTag as the cache identifier for lookup.
An interesting side-effect is that `measure` could race with TextInput updates, but the race condition favors measuring the latest text, not outdated values.
Followups:
- Can we do this without copying the EditText Spannable on every keystroke? Maybe this approach is too aggressive, but I don't want a background thread measuring a Spannable as it's being mutated.
- Do we need to support measuring Attachments?
- How can we clean up this API? It should work for now, but feels a little hacky.
Changelog: [Internal]
Reviewed By: mdvacca
Differential Revision: D23290230
fbshipit-source-id: 832d2f397d30dfb17b77958af970d9c52a37e88b
Summary:
Changelog: [internal]
# What is Teller?
Teller is a bank's employee who deals with the customer on behalf of the bank. In Fabric's scenario it is a class that on behalf of the view deals with State.
# Why do we need it?
Dealing with `ConcreteState` can be complicated and patterns are often repeated among different component views. `ConcreteStateTeller` aims to resolve these issues. Examples:
- You can call teller's methods without checking for nullptr (we have had crashes because of this before).
- Methods are save to be called on any thread.
- Mechanism to retry state update if it fails is built in.
It is designed to be used from ComponentView so views don't have to talk directly to `ConcreteState`.
Reviewed By: JoshuaGross, shergin
Differential Revision: D23216865
fbshipit-source-id: 90a50702e036eac084f89743ebab687a67182dc0
Summary:
Changelog: [Internal]
If `ConcreteShadowNode::setStateData` is called and the node is cloned before it is mounted, the cloned node will have old state before `setStateData` was called.
To solve this, simply call `setMostRecentState` on the family inside `ConcreteShadowNode::setStateData`.
Reviewed By: JoshuaGross
Differential Revision: D23283560
fbshipit-source-id: f9822fb69e4234f776d512fc02fe13ea7de64897
Summary:
This diff moves AndroidSwitch C++ files to make them compatible with RN OSS Build
Changelog: [Internal] internal
Reviewed By: PeteTheHeat
Differential Revision: D23227861
fbshipit-source-id: 8f23c2eb266a47cb9af82f4159f64b987c14141b
Summary:
This diff moves Android Slider C++ files to make them compatible with RN Tester OSS build
changelog: [internal] internal
Reviewed By: JoshuaGross
Differential Revision: D23227862
fbshipit-source-id: 7a5ed1bdc03cbe715467eddd4aad9af82761d4f0
Summary:
This diff moves AndroidDialogPicker C++ files to make it compatible with RN OSS build system.
changelog: [internal]
Reviewed By: fkgozali
Differential Revision: D23198857
fbshipit-source-id: 8fd4b7081f5f573946a6b9cd3fdc408488f91e13
Summary:
This diff updates the directory hierarchy of AndroidTextInput C++ files to be compatible with Android OSS build system
changelog: [internal] Internal
Reviewed By: PeteTheHeat
Differential Revision: D23179390
fbshipit-source-id: 1c52e4f882853799a58d44876cadd392b4a35050
Summary:
`accessibilityElementsHidden` property hides a subtree starting from the node with this prop from VoiceOver and TalkBack, so the node with this prop should form the stacking context (otherwise the children will mount as siblings and the property will not have an effect).
Changelog: [Internal] Fabric-specific internal change.
Reviewed By: JoshuaGross
Differential Revision: D23202220
fbshipit-source-id: d87dfab5f791219551c1eb90cdf3b3fa86f9c51f
Summary:
This diff flattens the hierarchy of Text C++ files.
This is required to integrate Text into RN OSS
changelog: [internal] internal
Reviewed By: JoshuaGross
Differential Revision: D23173556
fbshipit-source-id: ce90000cae147460aa4ddad55b7c90369fa734a2
Summary:
Partial backout of D23123575 (https://github.com/facebook/react-native/commit/1e4d8d902daca8e524ba67fc3c1f4b77698c4d08). It's causing some crashes and there is a more efficient way of doing it, which I will land in a future diff.
Leaving unused feature-flags in place for now, they'll be used shortly.
Changelog: [Internal]
Reviewed By: mdvacca
Differential Revision: D23198625
fbshipit-source-id: 6e9cbc6b39898a604b8f4dfccf5a6dd238511a68
Summary:
Logbox inside bridgeless surfaces was crashing and not able to open, this diff should fix it.
Changelog: [Internal]
Reviewed By: PeteTheHeat
Differential Revision: D23043773
fbshipit-source-id: 6e584f97e53e626b9bfedd6dc997ba6ba8c08b8f
Summary:
Changelog: [internal]
Original commit changeset: 5cb78a3a75a7
In D23151218 (https://github.com/facebook/react-native/commit/dffec8bc7bee4d05b72d19b4efe4aeab980f4cd5) we switched from `ShadowTree::tryCommit` to `ShadowTree::commit` inside `UIManager::updateState`.
It fixes update state being dropped but can cause an infinite loop inside `ShadowTree::commit` because the shadow node that triggered `UIManager::updateState` can been removed before the `updateState` call is dispatched..
Reviewed By: JoshuaGross
Differential Revision: D23155228
fbshipit-source-id: f3339a4e4798880972366d6f894c14a58be1b9b2
Summary:
Changelog: [internal]
In D22940187 (https://github.com/facebook/react-native/commit/774dec1e17f6f250172a6d4d944121b82fa36efb) we introduced a mechanism to retry failed state updates from view layer.
The mechanism fixes an issue where state update is occasionally dropped with background executor enabled.
# Why is state dropped?
The state is dropped because with background executor enabled, it is possible to enter `ShadowTree::tryCommit` for the same tree from 2 different threads at once. One of thread changes `_rootShadowNode` causing the other commit to fail.
The code goes like this:
```
{
Lock mutex
grab reference to `rootShadowNode_` and call it `oldRootShadowNode`
Unlock mutex
}
State reconciliation
Trigger layout
`rootShadowNode_` is untouched in this section.
{
Lock mutex
Check if `oldRootShadowNode` is equal to `rootShadowNode_`, if not, return false signalling failure. Now this is what happens when
state update fails.
..... not relevant
Unlock mutex
}
..... not relevant
```
However, in D22940187 (https://github.com/facebook/react-native/commit/774dec1e17f6f250172a6d4d944121b82fa36efb), we have taken another path. Instead of retrying to commit transaction, client is informed about the failure and it is left up to them to retry. This is correct and works. But I think it is unnecessary to this retry can be done inside UIManager::updateState.
In this diff I call `ShadowTree::commit` (version with retries) in case no `stateUpdate.failureCallback` is provided.
This makes sure that we do retry if state update fails but if Android implements `stateUpdate.failureCallback`, it is left up to view layer to retry.
Eventually we might decide to converge these two approaches.
Reviewed By: shergin
Differential Revision: D23151218
fbshipit-source-id: 5cb78a3a75a754429a8e33bd7736e683e9ed34d4
Summary:
# Summary
In previous diffs earlier in 2020, we made changes to detect and optimize reordering of views when the order of views changed underneath the same parent.
However, until now we have ignored reparenting and there's evidence of issues because of that. Because Fabric flattens views more aggressively, reparenting is also marginally more likely to happen.
This diff introduces a very general Reparenting detection. It will work with view flattening/unflattening, as well as tree grafting - subtrees moved to entirely different parts of the tree, not just a single
parent disappearing or reappearing because of flattening/unflattening.
There is also another consideration: previously, we were generating strictly too many Create+Delete operations that were redundant and could cause consistency issues, crashes, or bugs on platforms that do not handle that gracefully -
especially since the ordering of the Create+Delete is not guaranteed (a reparented view could be created "first" and then the differ could later issue a "delete" for the same view).
Intuition behind how it works: we know the cases where we can detect reparenting: it's when nodes are *not* matched up with another node from the other tree, and we're either trying to delete an entire subtree, or create an entire subtree. For perf reasons, we generate whatever set of operations comes first (say, we generate all the Delete and Remove instructions) and take note in the `ReparentingMetadata` data-structure that Delete and/or Remove have been performed for each tag (if ordering is different, we do the same for Create+Insert if those come first). Then if we later detect a corresponding subtree creation/deletion, we don't generate those mutations and we mark the previous mutations for deletion. This incurs some map lookup cost, but this is only wasteful for commits where a large tree is deleted and a large tree is created, without reparenting.
We may be able to improve perf further for certain edge-cases in the future.
# Why can't we solve this in JS?
Two things:
1. We certainly can avoid reparenting situations in JS, but it's trickier than before because of Fabric's view flattening logic - product engineers would have to think much harder about how to prevent reparenting in the general case.
2. In the case of specific views like BottomSheet that may crash if they're reparented, the solution is to make sure that the BottomSheet and the first child of the BottomSheet is never memoized, so that lifecycle functions and render are called more often; and that in every render, the BottomSheet manually clones its child, so that when the Views are recreated, the child of the BottomSheet has a tag and is an entirely different instance. This is certainly possible to do but feels like an onerous requirement for product teams, and it could be challenging to track down every specific BottomSheet that is memoized and/or hoist them higher in the view hierarchy so they're not reparented as often.
Reviewed By: shergin
Differential Revision: D23123575
fbshipit-source-id: 2fa7e1f026f87b6f0c60cad469a3ba85cdc234de
Summary:
There are a few places where we cast JSI values to objects without much validation and without proper error logging, and in some places the crashes aren't symbolicated well. To make debugging easier in the short-term, I'm adding some additional logs.
Changelog: [Internal]
Reviewed By: mdvacca, RSNara
Differential Revision: D23033222
fbshipit-source-id: 9343d693a441f0af728e560a0c245bcc4eb97869
Summary:
Changelog: [Internal]
Fabric's UIManager.measureInWindow didn't take viewport's offset into account. This diff fixes it by including viewport's offset in `LayoutContext`.
Reviewed By: JoshuaGross
Differential Revision: D23021903
fbshipit-source-id: 9106a8789d66fe19d8cb0a9378ee5bc8f2c83005
Summary:
Changelog: [internal]
Use `Element<>` in `LayoutableShadowNodeTests`. It makes the tests cleaner and easier to understand.
Reviewed By: JoshuaGross
Differential Revision: D23028341
fbshipit-source-id: f7a2255581bdde667db0f68c222228a5b405b22f
Summary:
Changelog: [Internal]
Previous interface `Element<>.state` wasn't usable because creating ConcreteState requires ownership of component descriptor and family. Family isn't created until later and it isn't accessible to the caller.
To work around this shortcoming, we create `stateData` rather than state.
Reviewed By: JoshuaGross
Differential Revision: D23028296
fbshipit-source-id: fba35ea8e6986b77379b1dddaa37012f4234f86e
Summary:
Changelog: [Internal]
# Problem
## Step 1
JS clones a node that has size {100, 100} and changes props that cause the node to increase size to {200, 200}. JS holds pointer to this node.
Now, the size (stored in LayoutableShadowNode.layoutMetrics_) changes after Yoga layout is triggered.
However, the node gets cloned inside State Reconciliation before Yoga layout phase. The JS pointer points to a node with size {100, 100}, not to a node with size {200, 200}.
## Step 2
Again, JS clones node (with old reference, therefore gets old layoutMetrics_ with size {100, 100}) and it changes props that cause the node to decrease its size back to {100, 100}.
We go all the way to Yoga layout and looking for nodes that have been affected by the node. The node, affected by the layout because it went from {200, 200} to {100, 100}, will be evaluated as not affected. This causes onLayout event to not be fired.
# Fix
We can safely remove the frame equality check (please see below). This can be done because we already check for equality before dispatching onLayout. It happens here:
https://www.internalfb.com/intern/diffusion/FBS/browsefile/master/xplat/js/react-native-github/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp?commit=881853eb0c42625fd0812bd2652bf36fcbd614ee&lines=43
As far as I know, `affectedNodes` isn't used for anything else besides dispatching onLayout.
# Discussion
This problem manifests itself only when a node has two different sizes that it flips between. To better understand this, please watch the video in Test plan labelled "before". Notice how the text has 2 different values that it flips between.
Here is a code that was affected by it https://fburl.com/diffusion/3hwo0iy5
If you inspect it closely, you will notice that it depends on `onLayout` to return correct value to calculate offset from left.
Reviewed By: JoshuaGross
Differential Revision: D22999891
fbshipit-source-id: e2d0f5771c1bf3cd788e5e9da0155c92e33fb84e
Summary:
This diff extends fabric module to compile in OSS
NOTE: As a side effect of this diff, Fabric will be included into "reactnativejni" which is used by RN OSS.
I'm planning to remove this dependency in the near future - T71320460
changelog: [internal] internal
Reviewed By: JoshuaGross
Differential Revision: D22991877
fbshipit-source-id: 0ab3ee410dd448bbd87130114bec27c6e6bc65c6
Summary:
This diff extends the 'textlayoutmanager' module to compile in OSS
As part of this diff I also moved Android files in order to make the module compatible with Android.mk system
changelog: [internal] internal
Reviewed By: JoshuaGross
Differential Revision: D22963706
fbshipit-source-id: 14a7309f589fe12c21131c7d5cef02b4323d4a93