-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fallback to Shadow Tree measurement if native measure fails #49
base: 0.72.3-discord-1
Are you sure you want to change the base?
Conversation
uiManager, shadowNode, onSuccessFunction, rt); | ||
return jsi::Value::undefined(); | ||
})); | ||
turboModuleCalled = true; | ||
} | ||
|
||
if (turboModuleCalled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably was a rough edge around the initial implementation of this. Your recommendation sounds like a cleaner way to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 🤔
@@ -140,6 +140,63 @@ void UIManagerBinding::invalidate() const { | |||
uiManager_->setDelegate(nullptr); | |||
} | |||
|
|||
void measureFromShadowTree( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
createFromHostFunction
with the same name repeatedly everymeasure
call 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If keeping this approach, I'd put the above comment about scoping here too.
@@ -140,6 +140,63 @@ void UIManagerBinding::invalidate() const { | |||
uiManager_->setDelegate(nullptr); | |||
} | |||
|
|||
void measureFromShadowTree( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
uiManager, shadowNode, onSuccessFunction, rt); | ||
return jsi::Value::undefined(); | ||
})); | ||
turboModuleCalled = true; | ||
} | ||
|
||
if (turboModuleCalled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this into the if
statement
.asObject(runtime) | ||
.asFunction(runtime) | ||
.call(runtime, "NativeFabricMeasurerTurboModule"); | ||
auto onSuccessFunction = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the if
comment above
jsi::Function::createFromHostFunction( | ||
runtime, | ||
jsi::PropNameID::forAscii( | ||
runtime, "onNativeMeasureFail"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.