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

Separate render-start and loaded phases for Ads #7391

Merged
merged 11 commits into from Feb 9, 2017

Conversation

dvoytenko
Copy link
Contributor

This is a significant overhaul for loading. The loading is split into two phases: render-start and load. Render-start is used to update UX for loading indicator and sticky ad functionality.

Some notes:

  1. Signals are now a more precise and asynchronous way to wait for particular element states, such as built and render-start.
  2. amp-sticky-ad:0.1 will be redone in this PR, after review of 1.0.
  3. Cleanup of events will have to be done after amp-sticky-ad changes in prod. See Cleanup amp:built and amp:load events in favor of signals #7389.
  4. render-start will continue to be optional signal for now so we have to use both: render-start and load-end in most of cases.

// If support render-start, create a race between render-start no-content
this.adResponsePromise_ = listenForOncePromise(this.iframe,
// Calculate render-start and no-content signal. These signals are mutually
// exlcusive. Whichever arrives first wins.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: exclusive

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

@@ -1520,6 +1542,11 @@ function createBaseCustomElementClass(win) {
*/
toggleLoading_(state, opt_cleanup) {
assertNotTemplate(this);
if (state &&
(this.layoutCount_ > 0 || this.signals_.get('render-start'))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this.layoutCount_ > 0 is same as this.isFirstLayoutCompleted_? Personally feel the other one is more clear

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 had a "gotcha" with this as well. The old code uses layoutCount_ > 0 everywhere. I simply moved it here as a more central place. Basically what happens is that layoutCount_ is like an attempt - it's always incremented: failure or success. While isFirstLayoutCompleted_ only set on success.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ha! Good catch

// If NOT support render-start, listen to bootstrap-loaded no-content
// respectively
this.adResponsePromise_ = listenForOncePromise(this.iframe,
'bootstrap-loaded', true);
Copy link
Contributor

Choose a reason for hiding this comment

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

bootstrap-loaded is not used anywhere then, we can remove it from integration.js as well.

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 thinking about bringing it back in the similar fashion we had before. It's not a very decisive points since we are strongly moving toward having render-start in all of the important cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't feel bootstrap-loaded is useful even we bring it back though.

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 described in comments below and in the code why it's advantageous to keep it. I'd rather go with it and we can reconsider later.

@@ -125,60 +122,78 @@ export class AmpAdXOriginIframeHandler {
this.sendEmbedInfo_(this.baseInstance_.isInViewport());
}));

// Iframe.onload normally called by the Ad after full load.
const iframeLoadPromise = loadPromise(this.iframe).then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite get the change here.
From what I understand, the order should be load-start render-start load-end.
But here I think iframeLoadPromise can be resolved before renderStartPromise. Thus it is possible that we resolve layoutCallback too soon here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possible. But in reality this almost never happens. The only real situation I saw is when render-start and onload are triggered at the same time. In that case, onload arrives a few milliseconds earlier.

Copy link
Contributor

Choose a reason for hiding this comment

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

if onload always arrive after, then it makes sense.

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 went through about a dozen of Ad networks and x20 ad impressions. I had onload arrive earlier than render-start in less than 1% of cases and never more than 10ms earlier. That being said, this code fully supports onload to arrive earlier - just doesn't optimize around it.

@@ -125,60 +122,78 @@ export class AmpAdXOriginIframeHandler {
this.sendEmbedInfo_(this.baseInstance_.isInViewport());
}));

// Iframe.onload normally called by the Ad after full load.
const iframeLoadPromise = loadPromise(this.iframe).then(() => {
// Wait just a little to allow messages to arrive.
Copy link
Contributor

Choose a reason for hiding this comment

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

what messages are we thinking of here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

render-start and no-content.

Copy link
Contributor

Choose a reason for hiding this comment

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

i see. I guess we're only interested no-content at this point. can you just update the doc:

 // Wait just a little to allow no-content to arrive.

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

iframeLoadPromise,
noContentPromise,
]);
return adLoadPromise.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

i thought we discussed that layoutCallback should be resolved purely on iframeLoad?
This way we separate concepts so that:

  • iframe onload controls layoutCallback, which controls resource scheduling
  • render-start and no-content controls UI experience

Copy link
Contributor

Choose a reason for hiding this comment

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

in the case of no-content, iframe will continue loading. If it is for resource scheduling, we probably only want to resolve adLoadPromise when iframeLoad resolve?
Also is it possible to kill iframe loading in the case of no-content?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what we do now - we kill the iframe. If iframe.onload hasn't arrive yet, it never will. Thus, I use no-content to resolve the layout promise.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see we kill iframe in noContent_().

@dvoytenko
Copy link
Contributor Author

@lannka @zhouyx I brought back bootstrap-loaded event in essentially the same form as before. The reason: I didn't want to keep the Ad hidden for the complete load sequence.

it('should resolve on timeout', done => {
const noContentSpy =
sandbox.spy/*OK*/(iframeHandler, 'freeXOriginIframe');
it('should trigger render-start on timeout', done => {
Copy link
Contributor

Choose a reason for hiding this comment

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

the description is misleading. renderStartedSpy is not called.

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. Meant to say "trigger visibility"

iframeLoadPromise,
noContentPromise,
]);
return adLoadPromise.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see we kill iframe in noContent_().

// If ad loading has succeeded, so should either visibilityPromise or
// no-content. Return it here to ensure that rendering has been fully
// processed when layout promise is complete.
return Promise.race([visibilityPromise, noContentPromise]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I get it is safer to add another check, but is it really necessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

i think it's redundant as well.

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's necessary to ensure that the visibility actions have been completed when the main promise is resolved. Otherwise we will have a risk condition between layoutCallback and visibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, either iframeLoadPromise or noContentPromise is resolved.
In case of noContentPromise is resolved, this Promise.race is immediately resolved.
In case of iframeLoadPromise is resolved, visibilityPromise is resolved as well, because it's race of iframeLoadPromise with others. So this Promise.race is also immediately resolved.

So, this Promise.race will always be immediately resolved, meaning we don't need 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.

Not entirely correct. A risk condition is still possible. Both visibility and layout promises are joined on iframeLoadPromise. When this promise is resolved, both promises will be scheduled to be resolved in microtasks. W/o returning visibility promise here, there's no way to guarantee that visibility's then will be executed before layout promise is resolved.

As a proof, we can remove this return and see tests start failing. Or rather they will become flaky. (I just tried, that's indeed the case. I can demonstrate it if desired.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, you're right, I missed the then block in visibilityPromise.
It's quite brain twisting here. I doubt 3 months later we still understand all the relations.
Maybe we are running into the wrong direction?

So why does layoutCallback care about the UI treatment that visibilityPromise is handling? Again, I thought we want do decouple the concept here. To me, layoutCallback should only care if the iframe is loaded or deleted.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see!
But I also agree with @lannka that layoutCallback should not care about UI. As long as the visiblity's then is executed, it is fine to be executed a bit afterwards. Then in our test, we probably should use poll to check UI behaviors?

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 agree. I redid this part and remove the dependency on visibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see you remove the visiblityPromise w/o changing the test case.

there's no way to guarantee that visibility's then will be executed before layout promise is resolved.

As you described, wouldn't removing the extra check make tests flaky?

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 thought it would. But I'm not seeing flakes right now. So I figured it's just my paranoia. Sorry for the confusion.

listenForOncePromise(this.iframe,
['render-start', 'no-content'], true).then(info => {
const data = info.data;
if (data.type == 'render-start') {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd much rather see this split into two calls to listenForOncePromise. Then we can remove the two extra promises, and just use the listenForOncePromise return values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Couple of things here:

  1. They are mutually exclusive. The first one wins, but they do have significant consequences to DOM/UX.
  2. The second branch (the one that doesn't support renderStart) is going away eventually.

Copy link
Contributor

Choose a reason for hiding this comment

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

They are mutually exclusive.

Isn't that's what the Promise.race is for? All I'm asking for is renderStartPromise and noContentPromise be the return value from listenForOncePromise. We'll still have this if statement after the race.

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 fallback branch (bootstrap) makes that difficult, b/c it doesn't work as a race. Once we remove that branch, we can do exactly as you say.

@@ -125,60 +122,78 @@ export class AmpAdXOriginIframeHandler {
this.sendEmbedInfo_(this.baseInstance_.isInViewport());
}));

// Iframe.onload normally called by the Ad after full load.
const iframeLoadPromise = loadPromise(this.iframe).then(() => {
// Wait just a little to allow messages to arrive.
Copy link
Contributor

Choose a reason for hiding this comment

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

i see. I guess we're only interested no-content at this point. can you just update the doc:

 // Wait just a little to allow no-content to arrive.

// If ad loading has succeeded, so should either visibilityPromise or
// no-content. Return it here to ensure that rendering has been fully
// processed when layout promise is complete.
return Promise.race([visibilityPromise, noContentPromise]);
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, either iframeLoadPromise or noContentPromise is resolved.
In case of noContentPromise is resolved, this Promise.race is immediately resolved.
In case of iframeLoadPromise is resolved, visibilityPromise is resolved as well, because it's race of iframeLoadPromise with others. So this Promise.race is also immediately resolved.

So, this Promise.race will always be immediately resolved, meaning we don't need it.

if (this.baseInstance_.emitLifecycleEvent) {
this.baseInstance_.emitLifecycleEvent('renderCrossDomainStart');
this.baseInstance_.renderStarted();
if (opt_info) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: return early when opt_info is undefined. to save the indentation.

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

}
});
// The actual ad load is eariliest of iframe.onload event and no-content.
const adLoadPromise = Promise.race([
Copy link
Contributor

Choose a reason for hiding this comment

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

we can inline the variable now.

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

iframeLoadPromise,
noContentPromise,
]);
return adLoadPromise.catch(reason => {
Copy link
Contributor

Choose a reason for hiding this comment

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

shall we just do catch on iframeLoadPromise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed completely. Not possible. And already well processed by the framework itself.

this.updateSize_(data.height, data.width,
info.source, info.origin);
this.baseInstance_.signals().signal('render-start');
opt_info.source, opt_info.origin);
Copy link
Contributor

Choose a reason for hiding this comment

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

pls fix indentation

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

@dvoytenko
Copy link
Contributor Author

@lannka @zhouyx PTAL.

return false;
it('should wait for built and load-end signals', () => {
impl.ad_.isBuilt = () => false;
impl.vsync_.mutate = function(callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

one last question. I see we use vsync_.mutatePromise but mock vsync_mutate here. What are the differences?

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 real difference. mutatePromise calls mutate.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. good to sync the 0.1 version

@dvoytenko dvoytenko merged commit 8fc6792 into ampproject:master Feb 9, 2017
@dvoytenko dvoytenko deleted the fie14-adlayout branch February 9, 2017 21:01
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Feb 14, 2017
* Separate render-start and loaded phases for Ads

* lints

* fixes

* Process bootstrap-start event

* fixes

* fixes

* remove visibility expectation from load promise

* lints

* review fixes

* lints

* sticky-ad-0.1
mrjoro pushed a commit to mrjoro/amphtml that referenced this pull request Apr 28, 2017
* Separate render-start and loaded phases for Ads

* lints

* fixes

* Process bootstrap-start event

* fixes

* fixes

* remove visibility expectation from load promise

* lints

* review fixes

* lints

* sticky-ad-0.1
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.

None yet

4 participants