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

core: add cap to amp stylesheet links for simulated throttling #11069

Merged
merged 27 commits into from
Jul 13, 2020

Conversation

adamraine
Copy link
Member

Summary
Since AMP defers loading external stylesheets if they take more than 1s we need to cap the duration in render blocking resources at 1s. The cap has already been applied for active throttling and this PR will do the same for simulated throttling.

@patrickhulce we currently just cap any stylesheet when AMP is loaded. Let me know if that is too broad.

Related Issues/PRs
#2832

@@ -54,6 +55,12 @@ function getNodesAndTimingByUrl(nodeTimings) {
const nodeTiming = nodeTimings.get(node);
if (!nodeTiming) return;

if (usesAMP && node.record.resourceType === 'Stylesheet'
Copy link
Collaborator

Choose a reason for hiding this comment

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

We'll definitely need a beefy comment here on why this is fair and not totally cheating/playing nice for AMP.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, a big beefy comment with a link to our invitation for any stack owner with similar logic to do this would be awesome 👍

it'd also be great to link to the amp source (it should be in that issue) so that later we can double check that the behavior still exists.

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

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

Thanks @adamraine this is a great start!

To @connorjclark 's point, it would also be nice if the override logic were extracted to computeStackSpecificTiming functions where the AMP-specific logic (and potential future others) could live rather than a boolean parameter intermingled in the core code. For one it makes it clear where future stacks would be added, and for another it marks the stack-specific logic as clear extra optional niceness instead of a core part of the logic of this audit.

To that end, I don't think we're going to want the nodeTiming mutation done inside getNodesAndTimingByUrl if we're going to be capping the overall savings as well.

There are two parts to cap.

  1. wastedMs per stylesheet
  2. overallWastedMs

Mutating the object that's supposed to reflect the true timings makes this need slightly less clear and doesn't fully solve the second part of the problem. Assuming we extract two computeStackSpecificTiming functions for the two problems, we can the override just the "wasted" variables instead of the simulated request data. How does that sound?

Unit tests for this would also be great :)

@@ -54,6 +55,12 @@ function getNodesAndTimingByUrl(nodeTimings) {
const nodeTiming = nodeTimings.get(node);
if (!nodeTiming) return;

if (usesAMP && node.record.resourceType === 'Stylesheet'
Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, a big beefy comment with a link to our invitation for any stack owner with similar logic to do this would be awesome 👍

it'd also be great to link to the amp source (it should be in that issue) so that later we can double check that the behavior still exists.

@@ -95,7 +103,7 @@ class RenderBlockingResources extends Audit {
const fcpSimulation = await FirstContentfulPaint.request(metricComputationData, context);
const fcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000;

const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTimings);
const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTimings, usesAMP);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is the layer at which we're going to want to cap the timing, see more in my overall review comment

* @param {Node} node
* @param {LH.Gatherer.Simulation.NodeTiming} nodeTiming
* @param {LH.Artifacts.DetectedStack[]} Stacks
*/
function computeStackSpecificTiming(node, nodeTiming, Stacks) {
let stackSpecificTiming = Object.assign({}, nodeTiming);
if(!Stacks) return stackSpecificTiming;
const stackSpecificTiming = Object.assign({}, nodeTiming);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: prefer object spreading.

Suggested change
const stackSpecificTiming = Object.assign({}, nodeTiming);
const stackSpecificTiming = {...nodeTiming};

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

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

nice @adamraine we're getting there 👍

friendly reminder that adding some early unit tests will be your friend here and help catch potential bugs! :)

const originalEstimate = simulator.simulate(fcpGraph).timeInMs;
static estimateSavingsWithGraphs(simulator, fcpGraph, deferredIds, wastedCssBytesByUrl, Stacks) {
const originalEstimate = simulator.simulate(fcpGraph, {label: 'rbr-estimate'}).timeInMs;
const originalNodeTimings = Simulator.ALL_NODE_TIMINGS.get('rbr-estimate');
Copy link
Collaborator

Choose a reason for hiding this comment

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

very clever use of the API surface! :)

I don't think we've ever used the label to do anything more than output debug information before though, and I'm not sure it's worth doing it this time around when simulate already returns the nodeTimings

const nodeTiming = originalNodeTimings.get(node);
if (nodeTiming) {
const stackSpecificTiming = computeStackSpecificTiming(node, nodeTiming, Stacks);
stackSpecificEstimate -= nodeTiming.duration - stackSpecificTiming.duration;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm trying to wrap my head around how this works, so let me see if I follow what you're trying to do here.

  • we lookup the node timing in the "before" version of the graph
  • compute its stack-specific timing
  • take the difference from the non-stack-specific timing and subtract it from the overall "before" estimate
  • this lowers the effective "before" estimate to better reflect the fact that we might be overestimating it

right?

This is on the right track but I think it will run into issues with parallel requests.

Consider the page dependency graph

|-index.html 1000ms
    |- style-1.css 2000ms
    |- style-2.css 2000ms
    |- style-3.css 2000ms

In this case we'll subtract 3 full seconds off the before time which was only 3s to begin with. A fully correct solution in this style would require threading the duration changes all the way down through core lantern, but that's a bit more involved than I think we need here :)

Instead, you could adjust all of the nodeTimings coming from simulate and recompute const originalEstimate as the Math.max(...Array.from(adjustedNodeTimings.map(timing => timing.endTime))), you will also have to adjust the endTime of any dependent requests of the stylesheet (see line 137 for an example of how to find dependent nodes)

const stackSpecificTiming = Object.assign({}, nodeTiming);
if (!Stacks) return stackSpecificTiming;
if (Stacks.some(stack => stack.id === 'amp')) {
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js]
Copy link
Collaborator

Choose a reason for hiding this comment

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

protip: press y while on a github blob to turn the shortcut ref into a permalink of the current hash, this way if amp ever renames its master branch to main or moves the file future readers will still be able to find what you were talking about :)

Suggested change
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js]
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second
// https://github.com/ampproject/amphtml/blob/8e03ac2f315774070651584a7e046ff24212c9b1/src/font-stylesheet-timeout.js#L54-L59

const stackSpecificTiming = Object.assign({}, nodeTiming);
if (!Stacks) return stackSpecificTiming;
if (Stacks.some(stack => stack.id === 'amp')) {
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js]
Copy link
Collaborator

Choose a reason for hiding this comment

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

based on their latest master it actually looks like they cap the stylesheet end load time to 2.1s now instead of the stylesheet load time to 1s which is pretty neat, we'll need to adjust our logic accordingly

if (Stacks.some(stack => stack.id === 'amp')) {
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js]
// Any potential savings for AMP stylesheet nodes must therefore be capped at 1 second.
// https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's move this comment line to the jsdoc description of the function explaining this is where any stack-specific overrides should happen

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

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

spoke in our video call, but going back to the original issue I think we have to do a little more around dependent requests.

I added an example trace from an AMP page we should try to test against :)

@@ -60,6 +60,56 @@ function getNodesAndTimingByUrl(nodeTimings) {
return urlMap;
}

/**
* Adjust the timing of a node and it's dependencies to account for stack specific overrides.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* Adjust the timing of a node and it's dependencies to account for stack specific overrides.
* Adjust the timing of a node and its dependencies to account for stack specific overrides.

sorry if that was been copied from one of my comments 😆


/**
* Any stack specific timing overrides should go in this function.
* https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081
* @see https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081

@@ -170,13 +228,18 @@ class RenderBlockingResources extends Audit {
return !canDeferRequest;
}));

// Recalculate the "before" time based on our adjusted node timings.
const stackSpecificEstimate = Math.max(...Array.from(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Keeping with the global name change WDYT about...

Suggested change
const stackSpecificEstimate = Math.max(...Array.from(
const estimateBeforeInline = Math.max(...Array.from(

// Saving 1000 + 1000 + 100ms for TCP handshake + 1 RT savings + server response time
assert.equal(result, 2100);
});

it('computes savings for stylesheets deferred by AMP', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
it('computes savings for stylesheets deferred by AMP', () => {
it('does not report savings from AMP-stack when document already exceeds 2.1s', () => {

@googlebot
Copy link

All (the pull request submitter and all commit authors) CLAs are signed, but one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that by leaving a comment that contains only @googlebot I consent. in this pull request.

Note to project maintainer: There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent. In those cases, you can manually confirm consent of the commit author(s), and set the cla label to yes (if enabled on your project).

ℹ️ Googlers: Go here for more info.

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

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

LGTM 🎉

It's worth continued noting in the issue that this audit will still produce results because AMP's method does still use render-blocking requests. It just limits the impact they have to ~2.1s. Since most the comments in that issue are complaining about results that are less than this threshold, I wouldn't expect many of them to be satisfied but at least we know our results are technically sound now :)

Before After
image image

const difference = nodeTiming.duration - stackSpecificTiming.duration;
if (!difference) return;

// Adjust timing of all dependent nodes
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Adjust timing of all dependent nodes
// AMP's method of removal of stylesheets effectively removes all dependent nodes from the FCP graph
// Ensure all dependent nodes are not affecting our estimated savings either.

Copy link
Collaborator

Choose a reason for hiding this comment

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

just to make it clear this is still AMP specific if we ever add more

Comment on lines 77 to 78
node.traverse(node => {
adjustedNodeTimings.delete(node);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: shadowing can be confusing to those who are trying to understand it for the first time :)

Suggested change
node.traverse(node => {
adjustedNodeTimings.delete(node);
node.traverse(childNode => {
adjustedNodeTimings.delete(childNode);

@adamraine
Copy link
Member Author

@googlebot I consent.

1 similar comment
@patrickhulce
Copy link
Collaborator

@googlebot I consent.

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants