Skip to content
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

Open
wants to merge 11 commits into
base: 0.72.3-discord-1
Choose a base branch
from

Conversation

hgarfinkle
Copy link

@hgarfinkle hgarfinkle commented Dec 8, 2023

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.

uiManager, shadowNode, onSuccessFunction, rt);
return jsi::Value::undefined();
}));
turboModuleCalled = true;
}

if (turboModuleCalled) {
Copy link
Author

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?

Copy link

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.

@hgarfinkle hgarfinkle marked this pull request as ready for review December 22, 2023 22:25
Copy link

@pmick pmick left a 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?

Comment on lines 671 to 673
// We return onSuccessFunction out of the onFail
// instead of using the one defined above to avoid
// a scoping bug.
Copy link

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(
Copy link
Author

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?

Copy link

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.

@joemun
Copy link

joemun commented Jan 3, 2024

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.

Copy link

@Flewp Flewp left a 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:

  1. I don't know if calling createFromHostFunction with the same name repeatedly every measure call is going to just rewrite the previous definition of the function, or produce errors in certain environments.
  2. 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);
Copy link

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(
Copy link

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) {
Copy link

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 =
Copy link

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 =
Copy link

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"),
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants