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

Add firstFrameDidRender to FlutterViewController #10816

Merged
merged 14 commits into from
Aug 14, 2019

Conversation

dnfield
Copy link
Contributor

@dnfield dnfield commented Aug 9, 2019

Earl Grey wants this to be able to more reliably/sensibly detect when the rendering is read. Android offers something somewhat similar to this - tagged @matthew-carroll to help evaluate consistency there.

/cc @AlbertWang0116

@dnfield
Copy link
Contributor Author

dnfield commented Aug 9, 2019

/cc @gaaclarke who may find this interesting when you get back

@dnfield dnfield requested a review from jmagman August 9, 2019 23:53
[rootVC presentViewController:self.flutterViewController animated:NO completion:nil];
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
int countDownMs = 2000;
while (!flutterViewController.firstFrameRendered && countDownMs > 0) {
Copy link
Member

@jmagman jmagman Aug 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should almost never have to manually spin the main run loop in tests. The above testFirstFrameCallback test should be:

XCTestExpectation *callbackExpecation = [self expectationWithDescription:@"callback"];
[self.flutterViewController setFlutterViewDidRenderCallback:^{
  [callbackExpecation fulfill];
}];
...
[rootVC presentViewController:self.flutterViewController animated:NO completion:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];

For this one you can use a XCTKVOExpectation. Something like (I didn't test this):

XCTestExpectation *firstFrameRenderedExpecation = [self keyValueObservingExpectationForObject:flutterViewController keyPath:@"setFirstFrameRendered" expectedValue:@YES];
...
[rootVC presentViewController:self.flutterViewController animated:NO completion:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];

Or better yet, can you get rid of firstFrameDidRender and setFirstFrameRendered if it's just for this purpose, and fulfill the expectation however _flutterViewRenderedCallback is set (I don't know how _flutterViewRenderedCallback works).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this actually testing that's different than the above test? It looks like they are both checking that the _flutterViewRenderedCallback was called.

* This method is called before the callback set by any previous call to
* `setFlutterViewDidRenderCallback` is called, if any.
*/
- (void)firstFrameDidRender;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is just for tests. Can you just detect the -callViewRenderedCallback message in the test instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlbertWang0116 how do you feel about this becoming a readonly BOOL?

@property(nonatomic, readonly) BOOL firstFrameRendered;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works. But if the setter is ensured being called symmetrically (NO->YES->NO->YES...), EarlGrey can perform a more optimal event-based (other than polling-based) synchronization that wait for the Flutter app to be loaded.

I think this is the case for now, and if we would like to guarantee that, probably add a doc-string to the readwrite version property saying that it should be assigned with NO/YES symmetrically? (So it can be the reference for future update on this class)

Copy link
Member

@jmagman jmagman Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dnfield If needed, you can accomplish this KVO contract with a backing instance variable without hand-shake documentation. Note with a getter the KVO key to observe on the unit test/Earl Grey side is renderingFrames and not isRenderingFrames (common mistake).

@interface FlutterViewController
@property (nonatomic, readonly, getter = isRenderingFrames) BOOL renderingFrames;
@end
@interface FlutterViewController () <FlutterBinaryMessenger>
@property (nonatomic, readwrite, getter = isRenderingFrames) BOOL renderingFrames;
@end
@implementation FlutterViewController {
...
}
@synthesize renderingFrames = _renderingFrames;
+ (BOOL)automaticallyNotifiesObserversForRenderingFrames {
    return NO;
}

- (void)setRenderingFrames:(BOOL)renderingFrames {
    if (_renderingFrames != renderingFrames) {
      [self willChangeValueForKey:@"renderingFrames"];
       _renderingFrames = renderingFrames;
      [self didChangeValueForKey:@"renderingFrames"];
    }
}

or you can stick to isRenderingFrames everywhere if you don't want to deal with the confusion of the property getter.

@@ -264,6 +264,7 @@ - (void)installSplashScreenViewIfNecessary {
}

- (void)callViewRenderedCallback {
[self firstFrameDidRender];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In here in the implementation would be an interface:
@property(nonatomic, readwrite) BOOL firstFrameRendered;
and here would be:
self.firstFrameRendered = YES;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we need to be able to do is set this and unset it each time the viewcontroller appears/disappears. Will that still work with a property?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the property is always set in the view controller implementation, then yes.

Copy link
Contributor Author

@dnfield dnfield Aug 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case a better name might be isRenderingFrames. If that works for @AlbertWang0116 I'll make that change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just went ahead and made them - I think this makes themost sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isRenderingFrames sounds to me that it changes the value for each frame lifecycle. But it actually indicates the first frame (after view controller being loaded) is rendering?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be set to YES after at least one frame has rendered after showing the view controller, and set to now upon dismissing the view controller.

Right now, it's not guaranteed to only go YES / NO / YES / NO but it sounds like Jenn's suggestion could fix that.

@flutter flutter deleted a comment from xster Aug 10, 2019
@jmagman
Copy link
Member

jmagman commented Aug 10, 2019

jmagman deleted a comment from xster 5 minutes ago

🤦‍♀ that was an accident. @xster said:

In testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m:

> @@ -6,6 +6,16 @@
 #import <XCTest/XCTest.h>
 #import "AppDelegate.h"
 
+@interface FlutterViewControllerFirstFrameOverride : FlutterViewController
+@property(nonatomic) BOOL firstFrameRendered;
+@end
+
+@implementation FlutterViewControllerFirstFrameOverride
+- (void)firstFrameDidRender {

\o/

That's my general feedback too. Either make it actually functional in production like on Android or make it test only and private.

@dnfield
Copy link
Contributor Author

dnfield commented Aug 10, 2019 via email

@@ -395,6 +397,7 @@ - (void)surfaceUpdated:(BOOL)appeared {
[_engine.get() platformViewsController] -> SetFlutterViewController(self);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know this code, but would it make more sense to set isRenderingFrames to YES in the if of this statement to make it symmetrical? It seems like an unexpected side-effect for (void)callViewRenderedCallback to set it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We present the surface before we actually are rendering frames

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to double check this - I'm realizing this might not get invoked again in a serize of push/pop/push

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's ok - we install the first frame callback every time we appear.


XCTAssertFalse(self.flutterViewController.isRenderingFrames);

[self keyValueObservingExpectationForObject:self.flutterViewController
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget the exact API but there's another one that let's you set the expected value as a parameter to the expectation. @YES will probably work even though it's a primitive, though I might be wrong. Then you won't need the assert true at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

* True if at least one frame has rendered and the ViewController has appeared.
*
* This property is reset to false when the ViewController disappears.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this now does the same thing as what io.flutter.embedding.android.hasRenderedFirstFrame() does. Though it's named differently :D

Can we keep the names consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that name works here. This can change from true to false depending on State and back to true again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android does that too on the same instance. Though you could made an argument to change the Android name too.

cc @matthew-carroll

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for consistency. Or isRenderingFirstFrame, which can be used on both iOS and Android?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think isRenderingFirstFrame is quite right either - this currently works as indicating the state as to whether Flutter has rendered at least one frame since the view has become visible, or if the view is not visible and thus no frames will be rendered.

Copy link
Contributor

@matthew-carroll matthew-carroll Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderingFrames seems problematic for multiple reasons. First, it is easily confused with a noun, e.g., these are some frames that are being rendered. Second, it doesn't have any sense of temporal context in the name. I think that hasRenderedFirstFrame() implies a somewhat specific place in time. There was a time before any frames were rendered, and then a time after the first frame was rendered, and this method tells if that moment has passed, as opposed to the somewhat ambiguous on-going "time of rendering frames". I think it's totally fine that hasRenderedFirstFrame() can toggle between true/false, so long as the reason is documented. That toggle is not arbitrary, it only occurs when specific resets or detachments occur. In other words a singular Flutter experience will never flip that value from true to false - only introducing a new Flutter experience, or detaching and re-attaching the original Flutter experience will result in that flag flipping, which coincides with Flutter's visual display disappearing and later re-appearing, thus repeating the concept of a "1st frame".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part: "In other words a singular Flutter experience will never flip that value from true to false - only introducing a new Flutter experience, or detaching and re-attaching the original Flutter experience " really confused me. Let's discuss offline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scenario most on my mind for this is showing the camera.

FlutterViewController appears -> isRenderingFrames = NO
Frame callback fires -> isRenderingFrames = YES
Camera starts -> isRenderingFrames = NO
Camera goes away -> isRenderingFrames = YES

In this situation, I find it confusing to think about which frame is the "first" frame, and I don't want to know if a frame "has" rendered - I want to know if at least one frame has rendered and Flutter could still potentially continue to render.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated property name based on discussion - PTAL

*
* This property is reset to false when the ViewController disappears.
*/
@property(nonatomic, readonly) BOOL isRenderingFrames;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. But if anything, your doc is in the past tense so this would be hasRenderedFrames. (At which point, we might as well go hasRenderedFirstFrame :D)

@xster
Copy link
Member

xster commented Aug 13, 2019

Would be nice to get the names consistent but otherwise LGTM

- (void)callViewRenderedCallback {
self.isRenderingFrames = YES;
[self setRenderingFrames:YES];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still use dot notation for properties: self.renderingFrames = YES

@AlbertWang0116
Copy link
Contributor

LGTM. Thanks Dan!

@xster
Copy link
Member

xster commented Aug 13, 2019

LGTM

- (void)callViewRenderedCallback {
self.isDisplayingFlutterUI = YES;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate to keep harping on this PR, but did we figure out if it's a problem that this isn't set in a queue-confined way to guarantee YES -> NO -> YES -> NO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - I think the bigger problem is that we're not guaranteed to call this accurately though. I'll think about this some more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in c6923c6

@dnfield
Copy link
Contributor Author

dnfield commented Aug 14, 2019

I'm going to go ahead and land this - Chinmay worked with me on the latest code offline, I bleieve I've addressed all feedback, and I have a few LGTMs :)

If anyone has any follow ups on this, I'm happy to open a new CL for it.

@dnfield dnfield merged commit 90656d8 into flutter:master Aug 14, 2019
@dnfield dnfield deleted the first_frame_rendered_ios branch August 14, 2019 16:10
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Aug 14, 2019
engine-flutter-autoroll added a commit to flutter/flutter that referenced this pull request Aug 14, 2019
…ngine#10816) (#38553)

[email protected]:flutter/engine.git/compare/d75af60810f6...90656d8

git log d75af60..90656d8 --no-merges --oneline
2019-08-14 [email protected] Add isDisplayingFlutterUI to FlutterViewController (flutter/engine#10816)

The AutoRoll server is located here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md

If the roll is causing failures, please contact the current sheriff ([email protected]), and stop
the roller if necessary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants