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

intersect-resources: Refactor to a lower risk integration #27497

Merged
merged 12 commits into from
Apr 2, 2020

Conversation

dreamofabear
Copy link

@dreamofabear dreamofabear commented Mar 30, 2020

Partial for #25428. An alternative to #27176.

I originally was hoping to eventually remove the "measure pass" (discoverWork_()) from Resources, because once we no longer need explicit measures we won't need to carefully phase measures/mutates to avoid layout thrashing. However, since discovering a tricky race condition (#27483), I'm more wary about diverging from the "pass" system in Resources due to other hidden dragons that may be lurking.

This refactor changes IntersectionObserver to simply store client rects on a Resource such that it'll be consumed during the next measure pass. Reconstructed resources (element reparenting) will be unobserved/observed and receive a fresh callback and client rect.

Here are some flame charts on examples/article.amp.html on the Nokia 2.3. It's hard to get a consistent baseline across runs due to noise, but you can see the width of several flames are much smaller in the "after".

Before

before

After

after

/to @dvoytenko @jridgewell

William Chou added 8 commits March 30, 2020 18:49
Add comment about first intersection callback in Chrome 82.

Only prerender 1 viewport (if prerenderSize > 0).

Create 3vp observer after first 1vp observer callback.

Wait until visible to start 3vp observer.

First firstCallback.

Avoid double-handling by early-out when isIntersecting didn't change.

Add comment.

Simplify handling of first intersection callback.

Tweak comment.

Use ampdoc instead of viewer.

Add comments.

Don't handle prerenderSize_ == 0.

Refactor stuff.

Less efficient but much more simple.

Missing ivar init.

Fix types.

Remove logs for apply sizes/media query.

Remove commented out code.

Refactor to use one observer and calculate 1vp.

Support 25% larger 'visible rect'.

Move log, fix types.

Fix missing experiment guard.

Add assert in resource.measure, early out if resource was removed.
Copy link
Member

@samouri samouri left a comment

Choose a reason for hiding this comment

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

Smart idea for making it a smaller diff! Great way to incrementally move towards IO. New flamegraph is definitely looking better as well.

this.state_ = ResourceState.NOT_LAID_OUT;
// With IntersectionObserver, measure can happen before build
// so check if we're "ready for layout" (measured and built) here.
if (this.intersect_) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: you don't need to experiment-gate this if you make hasBeenMeasured() return false outside the experiment

Copy link
Author

Choose a reason for hiding this comment

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

I think it's better to be explicit here rather than hide it a subfunction. In the classical system, resources sequentially follow the phases in ResourceState (e.g. build -> measure -> layout) which is somewhat subverted here.

Also hasBeenMeasured() is used in many places.

Copy link
Member

@samouri samouri Apr 2, 2020

Choose a reason for hiding this comment

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

whoops, i was mixing up hasBeenMeasured and hasBeenPremeasured

src/service/resources-impl.js Show resolved Hide resolved
element.whenUpgraded().then(() => {
this.intersectionObserver_.observe(resource.element);
});
this.intersectionObserver_.observe(element);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we no longer care about waiting for whenUpgraded?

Copy link
Author

Choose a reason for hiding this comment

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

Similar to removal of other minor edge case handling -- I thought it's better to keep things simple for now to catch the bigger problems first. E.g. this was complicating reasoning about the reparenting race condition. Might need to reintroduce later though.

Copy link
Member

Choose a reason for hiding this comment

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

What edge case did it handle? Or was it a perf optimization?

Copy link
Author

Choose a reason for hiding this comment

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

The edge case is that the IO callback happens too early and the resource stores a stale width/height. One possible fix to fix is it to delay observation (e.g. applyStaticLayout happens before upgrade). Another way is to use a ResizeObserver.

src/service/resources-impl.js Show resolved Hide resolved
*/
measure(opt_premeasuredRect) {
measure(usePremeasuredRect = false) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to always use premeasuredRect if available? When will there ever be a premeasured rect that we don't intend on using?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, sometimes we still do explicit measures e.g. after a component requests a size-change. The only time we use the "premeasured rect" is in the pass immediately after an intersection callback.

Copy link
Member

@samouri samouri Apr 2, 2020

Choose a reason for hiding this comment

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

Yes, sometimes we still do explicit measures e.g. after a component requests a size-change.

In that case, there shouldn't be a premeasuredRect though, since it is set to null after being used. Unless you are worried about a race condition between a size-change and IO callback

Copy link
Author

Choose a reason for hiding this comment

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

Yea this is a defensive check e.g. make sure the premeasuredRect is not accidentally consumed by a different codepath.

Copy link
Member

@samouri samouri left a comment

Choose a reason for hiding this comment

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

We should extensively manually test before turning up the experiment.

@dreamofabear
Copy link
Author

Thanks for the review!

@dreamofabear dreamofabear merged commit d8be2b6 into ampproject:master Apr 2, 2020
@dreamofabear dreamofabear deleted the intobs-use-pass branch April 2, 2020 21:19
Copy link
Contributor

@dvoytenko dvoytenko left a comment

Choose a reason for hiding this comment

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

LGTM. Sorry for the delay. This version is definitely a lot lighter.

calebcordry added a commit that referenced this pull request Apr 22, 2020
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.

4 participants