Fallback to Shadow Tree measurement if native measure fails#49
Fallback to Shadow Tree measurement if native measure fails#49hgarfinkle wants to merge 11 commits into0.72.3-discord-1from
Conversation
| turboModuleCalled = true; | ||
| } | ||
|
|
||
| if (turboModuleCalled) { |
There was a problem hiding this comment.
we're setting turboModuleCalled = true two lines above this. why check now if it's set? why not just return Value::undefined() above?
There was a problem hiding this comment.
Probably was a rough edge around the initial implementation of this. Your recommendation sounds like a cleaner way to do it.
pmick
left a comment
There was a problem hiding this comment.
Let me re-describe how I perceive this to work and you let me know if I'm getting all of this.
With the old code, we'd measure the native component. This would work by using the view's tag to find the view in native land and then measure that native view in java. This was throwing an exception though which would silently fail resulting in links being untappable. (or would it crash the app?) And this was because we couldn't actually find the native view?
The new code now catches that exception and will fire a native measure failure callback to the cpp layer. Once the cpp layer gets a failure callback it'll attempt to instead measure the component using the layout metrics from the shadow tree.
As a result of the change, the majority of measurements will continue to work exactly the same. Only in instances where we're looking for a view by an invalid react tag will we choose to measure by the shadow tree. This is probably anywhere that we're moving views around in the native view heirarchy after they've been measured and positioned by rn.
One other thought. Is this cpp exclusive to android or are we potentially changing some iOS implementation too?
| // We return onSuccessFunction out of the onFail | ||
| // instead of using the one defined above to avoid | ||
| // a scoping bug. |
There was a problem hiding this comment.
We might want to rewrite this comment to describe a bit better what the scoping bug is. Essentially something about how functions can't be passed into capture lists because they can't be copied so we need to get a new reference to the function. Unsure of how to phrase that 🤔
| uiManager_->setDelegate(nullptr); | ||
| } | ||
|
|
||
| void measureFromShadowTree( |
There was a problem hiding this comment.
I wanted to break this work out into a method. It's my understanding c++ doesn't allow you to introduce functions inside functions, so I had to do it here. Is that right? Is there not a way for this code to show up closer to where it's used?
There was a problem hiding this comment.
This seems correct. You could maybe move it closer by making a lambda, or a macro, but those introduce their own set of tradeoffs.
You probably want to make this a proper private method of the UIManagerBinding class though.
|
Wondering if we should add a log message to surface how often this fallback logic is happening, since ultimately this is workaround that shouldn't exist ideally. |
Flewp
left a comment
There was a problem hiding this comment.
After addressing comments, I think this would probably be okay to land. At the end of the day, this is hopefully a stopgap.
My biggest concern here is the usage of createFromHostFunction to create a callback to pass to Java/Obj-C for two reasons:
- I don't know if calling
createFromHostFunctionwith the same name repeatedly everymeasurecall is going to just rewrite the previous definition of the function, or produce errors in certain environments. - We're entangling C++ and Java here that feel against the grain. We're getting around C++ lambda capture by passing the success callback back to C++ as an argument of the failure callback; that doesn't feel right. If we needed to do this for iOS, I'm not sure if that platform would behave similarly or have means to do that.
From a high level, these measure and measureInWindow functions reside in the UIManagerBinding::get, which is a JSI HostObject override, and UIManagerBinding represents the global.nativeFabricUIManager object in JS. Therefore, whenever we call global.nativeFabricUIManager.METHOD_NAME, it'll call this get function with "METHOD_NAME" as the name argument. A potential way to clean this up and not have to do additional createFromHostFunction shenans is to introduce a new parameter to measure and measureInWindow: forceShadowTree. If forceShadowTree is true, it doesn't try to call measureNatively, and instead skips to the shadow tree logic immediately. In the JS impl where it's calling measure, if you get back an invalid measure return value, you can call measure again with forceShadowTree = true. It'll allow you to manage your callbacks and such completely in JS.
If you're down for it, give the above a shot. Otherwise, we can keep iterating on this when we return to Fabric work. Thanks for the spike into this, and nice job finding the underlying issue 👍
| onSuccess.invoke(x, y, width, height); | ||
| } | ||
| catch (IllegalViewOperationException e) { | ||
| onFail.invoke(onSuccess); |
There was a problem hiding this comment.
If keeping this approach, I'd put the above comment about scoping here too.
| uiManager_->setDelegate(nullptr); | ||
| } | ||
|
|
||
| void measureFromShadowTree( |
There was a problem hiding this comment.
This seems correct. You could maybe move it closer by making a lambda, or a macro, but those introduce their own set of tradeoffs.
You probably want to make this a proper private method of the UIManagerBinding class though.
| turboModuleCalled = true; | ||
| } | ||
|
|
||
| if (turboModuleCalled) { |
There was a problem hiding this comment.
Probably was a rough edge around the initial implementation of this. Your recommendation sounds like a cleaner way to do it.
| .asObject(runtime) | ||
| .asFunction(runtime) | ||
| .call(runtime, "NativeFabricMeasurerTurboModule"); | ||
| auto onSuccessFunction = |
| .asObject(runtime) | ||
| .asFunction(runtime) | ||
| .call(runtime, "NativeFabricMeasurerTurboModule"); | ||
| auto onSuccessFunction = |
| jsi::Function::createFromHostFunction( | ||
| runtime, | ||
| jsi::PropNameID::forAscii( | ||
| runtime, "onNativeMeasureFail"), |
There was a problem hiding this comment.
createFromHostFunction is decorating the JS runtime, and I'm not sure what the behavior would be if you had conflicting names. I'd rename this to something like onNativeMeasureInWindowFail
bd6a5ae to
66de003
Compare
00fe71c to
723d2f4
Compare
When tapping a hyperlink that exists as a child of another
<Text>element, the app crashes. This happens because the NativeFabricMeasurerTurboModule is called for the hyperlink's node tag as it exists in the c++ shadow tree, and that tag is (for reasons we don't understand) different from the actual tag that exists in the native layout. Thus, the TurboModule ends up looking for info on a node tag that doesn't exist, which crashes the native app. To resolve this, this PR gracefully handles that error, and in such a case we fallback to determining layout information from the shadow tree, as we were doing before Alex's TurboModule.