-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Conversation
// 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. |
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.
nit: exclusive
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.
Done
@@ -1520,6 +1542,11 @@ function createBaseCustomElementClass(win) { | |||
*/ | |||
toggleLoading_(state, opt_cleanup) { | |||
assertNotTemplate(this); | |||
if (state && | |||
(this.layoutCount_ > 0 || this.signals_.get('render-start'))) { |
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.layoutCount_ > 0
is same as this.isFirstLayoutCompleted_
? Personally feel the other one is more clear
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.
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.
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.
Ha! Good catch
// If NOT support render-start, listen to bootstrap-loaded no-content | ||
// respectively | ||
this.adResponsePromise_ = listenForOncePromise(this.iframe, | ||
'bootstrap-loaded', true); |
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.
bootstrap-loaded
is not used anywhere then, we can remove it from integration.js
as well.
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'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.
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 don't feel bootstrap-loaded
is useful even we bring it back though.
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 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(() => { |
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 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?
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.
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.
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 onload
always arrive after, then it makes sense.
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 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. |
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.
what messages are we thinking of here?
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.
render-start
and no-content
.
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 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.
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.
Done
iframeLoadPromise, | ||
noContentPromise, | ||
]); | ||
return adLoadPromise.then(() => { |
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 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
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.
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
?
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.
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.
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 see we kill iframe in noContent_()
.
it('should resolve on timeout', done => { | ||
const noContentSpy = | ||
sandbox.spy/*OK*/(iframeHandler, 'freeXOriginIframe'); | ||
it('should trigger render-start on timeout', done => { |
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.
the description is misleading. renderStartedSpy is not called.
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.
Fixed. Meant to say "trigger visibility"
iframeLoadPromise, | ||
noContentPromise, | ||
]); | ||
return adLoadPromise.then(() => { |
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 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]); |
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 get it is safer to add another check, but is it really necessary?
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 think it's redundant as well.
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.
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.
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.
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.
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.
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.)
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.
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.
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 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?
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 agree. I redid this part and remove the dependency on visibility.
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 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?
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 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') { |
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'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.
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.
Couple of things here:
- They are mutually exclusive. The first one wins, but they do have significant consequences to DOM/UX.
- The second branch (the one that doesn't support renderStart) is going away eventually.
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.
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.
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.
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. |
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 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]); |
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.
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) { |
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.
nit: return early when opt_info is undefined. to save the indentation.
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.
Done
} | ||
}); | ||
// The actual ad load is eariliest of iframe.onload event and no-content. | ||
const adLoadPromise = Promise.race([ |
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 can inline the variable now.
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.
Done
iframeLoadPromise, | ||
noContentPromise, | ||
]); | ||
return adLoadPromise.catch(reason => { |
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.
shall we just do catch on iframeLoadPromise?
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.
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); |
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.
pls fix indentation
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.
Done
return false; | ||
it('should wait for built and load-end signals', () => { | ||
impl.ad_.isBuilt = () => false; | ||
impl.vsync_.mutate = function(callback) { |
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.
one last question. I see we use vsync_.mutatePromise
but mock vsync_mutate
here. What are the differences?
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.
No real difference. mutatePromise
calls mutate
.
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.
Got it. good to sync the 0.1 version
* 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
* 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
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: