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
🐛Intersection Observer Polyfill Mutation Observer - Fixed Race Condition #19168
🐛Intersection Observer Polyfill Mutation Observer - Fixed Race Condition #19168
Conversation
…image element is created.
// Than our layout box, in order to avoid race conditions | ||
// with the resource scheduler | ||
if (opt_calledFromMutationObserver) { | ||
elementRect = element./*OK*/getBoundingClientRect(); |
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.
@torch2424 elementRect
is the rect relative to the document not viewport. We can't use getBoundingClientRect()
directly.
Made requested changes, this PR is good to go 😄 |
@jridgewell would really like to get your feedback on this. The reason why we need to call Do you think it's possible to have mutationObserver only observe the AMP element's attribute change that is selected by visibility trigger. So that in this example InOb will use the |
// Than our layout box, in order to avoid race conditions | ||
// with the resource scheduler | ||
if (opt_calledFromMutationObserver) { | ||
elementRect = this.viewport_.getLayoutRect(element); |
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.
@jridgewell Do we need different method when layer is enabled?
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, layers is corrected implemented in getLayoutRect
. Note it returns a viewport relative box (not a page relative box), but that's no different than the current code.
// Than our layout box, in order to avoid race conditions | ||
// with the resource scheduler | ||
if (opt_calledFromMutationObserver) { | ||
elementRect = this.viewport_.getLayoutRect(element); |
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, layers is corrected implemented in getLayoutRect
. Note it returns a viewport relative box (not a page relative box), but that's no different than the current code.
// with the resource scheduler | ||
if (opt_calledFromMutationObserver) { | ||
elementRect = this.viewport_.getLayoutRect(element); | ||
ownerRect = owner && this.viewport_.getLayoutRect(owner); |
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.
These need to be inside measure phases.
* Whether this was called from a mutation observer | ||
* This will signal that the layout of the elements | ||
* must use getBoundingClientRect, as Resource Scheduler | ||
* could be called too late, and not clear the layout cache. |
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.
You could instead trigger tick on the next Resource pass, which will mean everything's been remeasured. Then we don't need booleans floating around.
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.
Oh nice! Yeah let's try that, I think that would be the most optimal solution. Are the Resource passes usually done fast? As in there is nothing that could make it take longer than a second?
And what is the function/service to wait for this?
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.
Are the Resource passes usually done fast? As in there is nothing that could make it take longer than a second?
They can take up to 30s if there's no activity on the page (or it's in the background). But since there was a mutation, it'll happen in the next 100ms.
And what is the function/service to wait for this?
There's not currently one to do this. We can add a method to Resources
, say onNextPass
. Then, at the end of Resources.p.doPass
, we resolve call all the pending callbacks, and clear out the array.
class Resources {
constructor() {
// ...
/** @const @private {!Array<function()>} */
this.onNextPass_ = [];
}
doPass() {
//...
for (let i = 0; i < this.onNextPass_.length; i++) {
const fn = this.onNextPass_[i];
fn();
}
this.onNextPass_.length = 0;
}
/**
* Registers a callback to be called when the next pass happens.
* @param {function()}
*/
onNextPass(fn) {
this.onNextPass_.push(fn);
}
}
Made all requested changes, using the suggested Lastly, confirmed that this solution works on my example! 🎉 Thank you everyone for the help! 😄 |
@aghassemi Just opened a P1 at #19252 Would like to get this pulled in so we can make a fix for this. Will fix the remaining travis tests once I am sure this is the way we want to go. Thanks! 😄 |
this.tick(this.viewport_.getRect(), undefined, true); | ||
} | ||
}); | ||
this.resources_ = Services.resourcesForDoc(ampdoc); |
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.
There's no need to set and unset this. Just store it as a const reference.
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 can keep the reference, however, I can't store it as a constant, as we do a check for if the window has MutationObserver before attempting to bind everything.
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.
There's no need to gate the resources reference on whether the browser supports MutationObserver
.
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 yes! Good Point 😄 But another thing is, we don't have access to the ampdoc, until we get our first element observe. As the constructor of the polyfill only takes in a callback. 😮
return; | ||
if (this.viewport_ && this.resources_) { | ||
this.resources_.onNextPass(() => { | ||
this.tick(this.viewport_.getRect(), undefined, 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.
This can cause multiple ticks to happen back to back. If tick
is not already throttled, it should be.
@@ -1201,11 +1212,16 @@ export class Resources { | |||
this.viewer_.sendMessage('documentHeight', | |||
dict({'height': measuredContentHeight}), /* cancelUnsent */true); | |||
this.contentHeight_ = measuredContentHeight; | |||
dev().fine(TAG_, 'document height changed: ' + this.contentHeight_); | |||
dev().fine(TAG_, 'document height changed: %s', this.contentHeight_); |
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.
Thanks!
src/service/resources-impl.js
Outdated
this.viewport_.contentHeightChanged(); | ||
} | ||
}); | ||
} | ||
|
||
this.passCallbacks_.forEach(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.
Runtime has a different set of performance requirements. Please use a for-loop here.
Also when you fix this, don't do passCallbacks[i]()
because that'll leak passCallbacks
as the this
context of the function invocation. Store fn = passCallbacks[i]
, then invoke fn()
.
test/functional/test-resources.js
Outdated
@@ -1634,6 +1634,34 @@ describe('Resources discoverWork', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('passCallbacks_', () => { |
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.
"onNextPass"
test/functional/test-resources.js
Outdated
resources.onNextPass(passCallback); | ||
resources.doPass(); | ||
|
||
expect(resources.passCallbacks_.length).to.be.equal(0); |
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's not write tests on passCallbacks_
, since it's private. The intention here is that the fn
isn't called twice, so let's run two passes and assert the fn
isn't called twice.
Made requested changes. This is good to go 😄 |
* @private | ||
*/ | ||
handleMutationObserverPass_() { | ||
if (this.viewport_ && this.resources_) { |
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 need to check this.
this.tick(this.viewport_.getRect()); | ||
}); | ||
} | ||
return; |
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.
Useless return.
handleMutationObserverPass_() { | ||
if (this.viewport_ && this.resources_) { | ||
this.resources_.onNextPass(() => { | ||
this.tick(this.viewport_.getRect()); |
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.
tick
still needs to be throttled.
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.
Tick is being throttled by the pass isn't it? 😮 Or do you want the pass inside the tick itself?
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.
Oh, I totally forgot we were using a Pass. This is fine, then.
Replied to comments, and made more requested changes 😄 This is good to go. |
const fn = this.passCallbacks_[i]; | ||
fn(); | ||
} | ||
this.passCallbacks_.length = 0; |
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! Didn't know we can set length to 0 to clean an array : )
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.
Gotta give the credit to @jridgewell on that one, I didn't know either 😂
|
||
const passCallback = sandbox.spy(); | ||
resources.onNextPass(passCallback); | ||
resources.doPass(); |
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: if I understand it correctly, the callback will be called the first doPass()
is called. would be great to add that check as well with expect(passCallback).to.be.calledOnce;
} | ||
}); | ||
|
||
if (!this.viewport_) { |
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.
Is this.viewport_
and this.resources_
used elsewhere? And can element
and ampdoc
be passed to the handleMutationObserverPass
? Asking because would prefer use calling Services.viewportForDoc(element)
and Services.resourcesForDoc(ampdoc)
directly instead. The cost difference should be minimum.
Made requested changes 😄 This is good to go. |
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.
Thank you for the fix!!
…ndango-bug-fix-no-example
Thank you! @zhouyx Whenever you get the time @jridgewell let me know what you think and we can merge this in 😄 |
…ion (ampproject#19168) * Got a working half-broken page that we can test with * COnfirmed the bug is that the pass isn't long enough * Going to go home. But I learned that it doesn't fire until the child image element is created. * I was doing something * Fixed the polyfill completely * Fixed linting * Fixed all precommit checks * Removed the one-off fandango example * Made PR Changes * Created an onNextPass for resources * Fixed tests for ampdoc * Removed the original getLayoutRect * Removed a dangling opt_calledFromMutationObserver * Made PR Changes * Fixed refresh tests for intersection observer * Removed returns * Fixed some linting errors * Made PR Changes * Fixed linting errors
continuation of #19034
This makes the Mutation observer use
getBoundingClientRect()
to get around the resource scheduler not handling amp elements and updating their Layout Box fromgetLayoutBox()
in time before the mutation pass is called to detect the changes.cc @jridgewell Just wanted to make you aware of this. If you know of a better way to handle this please let us know. This is the best solution @zhouyx , @lannka , and I could come up with for this specific situation.
Example
This example was pulled by recreating the Fandango site by hand from the output AMP and "decompiling" it.