-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
new_audit(preconnect): add preconnect audit #4362
Conversation
wardpeet
commented
Jan 25, 2018
•
edited
Loading
edited
- Filter same origin
- Filter all records requested by main resource
- Filter all non http(s) records (urls with no origin)
- Filter out allready connected origins (connection time is 0)
- Filter out all records that took place after 10s
- Added unit tests
- Added smoketests
0eb4516
to
c2de538
Compare
|
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.
Details here: #3106 (comment)
basically let's do the first half of #4425 and then the two checkboxes below (exclude same origin, and depth===1). Then we're mostly good here.
c2de538
to
83c1ba1
Compare
const requestDelay = record._startTime - mainResource._endTime; | ||
const recordOrigin = URL.getOrigin(record.url); | ||
|
||
if (preconnectResults[recordOrigin]) { |
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.
better way to find unique origins?
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.
IMO use a Set for preconnectResults
instead
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.
yeah I might flip this around a bit ward
- create a set of all origins
- filter out the bad origins
- for each origin, find the earliest record that meets the other criteria
const requestDelay = record._startTime - mainResource._endTime; | ||
const recordOrigin = URL.getOrigin(record.url); | ||
|
||
if (preconnectResults[recordOrigin]) { |
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.
IMO use a Set for preconnectResults
instead
// filter out all resources that are not loaded by the document | ||
.filter(record => { | ||
return record.initiatorRequest() !== mainResource && record !== mainResource; | ||
// filter out urls that do not have an 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.
seems like L62 would already have taken care of these.
// filter out urls that do not have an origin | ||
}).filter(record => { | ||
return !!URL.getOrigin(record.url); | ||
// filter out all resources where origins are already resolved |
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.
since we're only displaying origins and not full request URLs, I feel like this is already taken care of by reduction down to unique origins below.
(The assumption being that connect is always a cost for new origins... which seems a reasonable assumption. right @patrickhulce ?)
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.
yeah but he's trying to find the first representative network record for the origin so you do need to filter out the network records that were already connected
const Util = require('../report/v2/renderer/util'); | ||
const UnusedBytes = require('./byte-efficiency/byte-efficiency-audit'); | ||
const URL = require('../lib/url-shim'); | ||
const PRECONNECT_SOCKET_MAX_IDLE = 10; |
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.
Can you add some comments?
Preconnect establishes a "clean" socket. Chrome's socket manager will keep an unused socket
around for 10s. Meaning, the time delta between processing preconnect a request should be <10s, otherwise it's wasted
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.
also I think we could actually use 12-15 for our threshold, tbh. we'd include slightly more connections and there's a decent chance in real loads, the preconnect would be used.
} | ||
|
||
// make sure the requests are below the PRECONNECT_SOCKET_MAX_IDLE (10s) mark | ||
if (Math.max(0, requestDelay) < PRECONNECT_SOCKET_MAX_IDLE) { |
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.
style wise it'd probably be better to check the inverse and return, like the other filters.
that way at the end we take everything that made it through the gauntlet and add to the set.
}); | ||
|
||
const headings = [ | ||
{key: 'url', itemType: 'url', text: 'URL'}, |
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.
text: '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.
based on your screenshot i'd say our url renderer is a little awkward in handling this origin-only URLs.
let's fix that. :)
we can check for pathname of /
and if that's the case, not separate the two.... probably want most of that in Util.parseURL
and small adjustments in the _renderTextURL
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.
nice work! excited to get this one in 👍
' before an HTTP request is actually sent to the server. This will reduce multiple, ' + | ||
`costly round trips to any origin. [Learn more](${learnMoreUrl}).`, | ||
requiredArtifacts: ['devtoolsLogs'], | ||
scoringMode: Audit.SCORING_MODES.NUMERIC, |
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.
scoring display mode
*/ | ||
static get meta() { | ||
return { | ||
category: 'Performance', |
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 more category :)
return !!URL.getOrigin(record.url); | ||
// filter out all resources where origins are already resolved | ||
}).filter(record => { | ||
return !UsesRelPreconnectAudit.hasAlreadyConnectedToOrigin(record); |
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.
might want to explicitly call out invalid timing information as well, i.e. if dnsStart/etc are not finite positive numbers
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.
call out? Do you mean log to sentry?
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 sorry, call out just in a comment with an extra check :)
I meant, "let's also filter requests with invalid timing information"
// filter out urls that do not have an origin | ||
}).filter(record => { | ||
return !!URL.getOrigin(record.url); | ||
// filter out all resources where origins are already resolved |
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.
yeah but he's trying to find the first representative network record for the origin so you do need to filter out the network records that were already connected
}); | ||
|
||
const results = Object.values(preconnectResults).map(record => { | ||
const wastedMs = record.timing.connectEnd - record.timing.dnsStart; |
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'll want to Math.min
this with the delta between connectStart
and main document resource end
const requestDelay = record._startTime - mainResource._endTime; | ||
const recordOrigin = URL.getOrigin(record.url); | ||
|
||
if (preconnectResults[recordOrigin]) { |
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.
yeah I might flip this around a bit ward
- create a set of all origins
- filter out the bad origins
- for each origin, find the earliest record that meets the other criteria
@paulirish @patrickhulce i'm not really sure about the displayUrl helpers though.. Also smoketests are failing on travis as it's such a low number. Could this be a docker issue? that host is has these origins cached, should I just check for items than? |
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.
review #️⃣2️⃣!
description: 'Avoid multiple, costly round trips to any origin', | ||
informative: true, | ||
helpText: | ||
'Consider using<link rel="preconnect dns-prefetch"> to set up early connections ' + |
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.
@paulirish may have opinions here too, but WDYT about?
Consider adding preconnect or dns-prefetch resource hints to establish early connections to important third-party origins. Learn more.
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.
fine by me i most of the time just copy what you guys say 😄 but I think <link tags shouldn't be in the description like you describe
|
||
const origins = networkRecords | ||
.filter(record => { | ||
// filter out all resources that have the same 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.
nit: move this comment down a line like the others :)
const results = []; | ||
preconnectOrigins.forEach(origin => { | ||
const records = networkRecords.filter(record => URL.getOrigin(record.url) === origin); | ||
if (!records.length) { |
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 seems impossible, you didn't run across this IRL did you?
seems like the same checks from above also belong in this spot though?
|
||
// Sometimes requests are done simultaneous and the connection has not been made | ||
// chrome will try to connect for each network record, we get the first record | ||
let firstRecordOfOrigin; |
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.
@paulirish is this an acceptable occasion for reduce in your 👀 ? :)
this works too
firstRecordOfOrigin._startTime * 1000 - | ||
mainResource._endTime * 1000 + | ||
firstRecordOfOrigin.timing.connectStart; | ||
// calculate delta between connectionTime and timeToConnectionStart from main resource |
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 comment is a bit confusing, didn't we already compute the delta and the line below is ensuring we just cap the connectionTime
savings?
@@ -110,7 +110,7 @@ class CriticalRequestChainRenderer { | |||
const {file, hostname} = Util.parseURL(segment.node.request.url); | |||
const treevalEl = dom.find('.crc-node__tree-value', chainsEl); | |||
dom.find('.crc-node__tree-file', treevalEl).textContent = `${file}`; | |||
dom.find('.crc-node__tree-hostname', treevalEl).textContent = `(${hostname})`; | |||
dom.find('.crc-node__tree-hostname', treevalEl).textContent = hostname ? `(${hostname})` : ''; |
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.
nice find :) if it doesn't have a hostname though should we be showing it? what type of request is that, data URI?
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 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.
url: 'https://www.example.com/', | ||
_endTime: 1, | ||
}; | ||
const mainResourceRecord = { |
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.
why do these two need to be different? :)
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.
good question 😛
assert.equal(extendedInfo.value.length, 2); | ||
assert.deepStrictEqual(extendedInfo.value, [ | ||
{url: 'https://cdn.example.com', wastedMs: Util.formatMilliseconds(200)}, | ||
{url: 'https://othercdn.example.com', wastedMs: Util.formatMilliseconds(400)}, |
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.
would you mind explaining this result? seems like it should be min of 500 (600 - 100)
and 300 (1000 * (1.2 - 1) + 100)
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.
when looking in code I use connectStart where I should have used dnsStart 🤦♂️
const connectionTime = 600 - 100;
const timeBetweenMainResourceAndConnectStart =
1.2 * 1000 -
1 * 1000 +
200;
// calculate delta between connectionTime and timeToConnectionStart from main resource
const wastedMs = Math.min(500, 400);
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 yeah I missed that too :) seems like timeBetweenMainResourceAndDnsStart
should fix it 👍 (or fallback to connect start I guess if dns start is invalid, is that a real case?)
93fc3a7
to
ab9137c
Compare
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.
few more comments sorry for the delay!
@@ -7,3 +7,6 @@ | |||
/* eslint-disable */ | |||
|
|||
document.write('<script src="level-2.js?delay=500"></script>') | |||
|
|||
// load another origin | |||
fetch('http://localhost:10503/preload.html', () => {}); |
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: do we even need the 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.
not sure why I even added it as it returns a promise
const PRECONNECT_SOCKET_MAX_IDLE = 15; | ||
|
||
const learnMoreUrl = | ||
'https://developers.google.com/web/fundamentals/performance/resource-prioritization#preconnect'; |
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.
as @brendankenny pointed out to me we can throw this inline without violating our max-len since it's a URL 👍
.filter(record => { | ||
return ( | ||
// filter out all resources that have the same origin | ||
!URL.originsMatch(mainResource.url, record.url) && |
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.
IIRC new URL
is actually pretty expensive and can take seconds in the worst case, so let's reorder for some cheaper ones first? maybe just sticking initiator request and hasAlreadyConnected
first is enough
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.
should we use webinspector parsedurl instead?
mainResource.parsedURL.securityOrigin() === record.parsedURL.securityOrigin()
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.
yeah even better! 👍
// filter out urls that do not have an origin (data, ...) | ||
!!URL.getOrigin(record.url) && | ||
// filter out all resources where origins are already resolved | ||
!UsesRelPreconnectAudit.hasAlreadyConnectedToOrigin(record) && |
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.
seems like we'll need a corresponding filter to make sure the timing information is valid
static hasValidTiming(record) {
return record.timing && record.timing.connectEnd > 0 && record.timing.connectStart > 0;
}
// filter out all resources where timing info was invalid
!UsesRelPreconnect...
in most cases it'll be the same, but we've seen some weird edge cases in other environments where timing is just nonsense negative numbers or undefined
maxWasted = Math.max(wastedMs, maxWasted); | ||
results.push({ | ||
url: new URL(firstRecordOfOrigin.url).origin, | ||
wastedMs: Util.formatMilliseconds(wastedMs), |
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 keep this a number and mark the wastedMs as a type: 'ms'
{key: 'wastedMs', itemType: 'ms', granularity: 1, text: 'Potential Savings'}
@@ -96,8 +96,8 @@ class DetailsRenderer { | |||
let title; | |||
try { | |||
const parsed = Util.parseURL(url); | |||
displayedPath = parsed.file; | |||
displayedHost = `(${parsed.hostname})`; | |||
displayedPath = parsed.file === '/' ? parsed.origin :parsed.file; |
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: space after colon : parsed.file
@patrickhulce @paulirish @brendankenny |
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.
LGTM % nits! nice work @wardpeet!
const preconnectOrigins = new Set(origins); | ||
const results = []; | ||
preconnectOrigins.forEach(origin => { | ||
const records = networkRecords.filter(record => URL.getOrigin(record.url) === 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.
I think we still want to double check that we're only looking at records with valid timing, also let's use parsedURL.securityOrigin to be consistent with above
could probably combine this with the forEach below too and do it in one pass :)
name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + | ||
// Show file extension | ||
`${ELLIPSIS}${name.slice(dotIndex)}`; | ||
name = |
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.
think we can revert some of this diff noise?
a5fb719
to
ce26b99
Compare
a7459fe
to
07bf3c0
Compare
btw for whatever reason 2/3rds of my last review comments are collapsed but they are unaddressed so far. Probably because I commented on that specific commit, and there was one following it. shrug. |
lgtm. ready to merge when green. |
\o/ Thanks for the diligent reviews getting this audit firmed up @paulirish @patrickhulce and @brendankenny. Big thanks for all your work getting this audit landed, @wardpeet high-fives |