-
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
Get critical chains: pure js version #310
Changes from 23 commits
0cdc88e
c4964d8
8d5b7c7
e9a4850
f01db28
6916f5e
d742c05
ad660da
b76cfde
123d133
b3c7e46
c3f293b
93b7c2a
72163c2
2a1be4a
0ad2fc4
cfa80ac
de54e42
9f2ac89
b1fd37b
187b348
da1da08
056ebe8
b940c7a
e6804c7
4065e12
6270862
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/** | ||
* @license | ||
* Copyright 2016 Google Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const Gather = require('./gather'); | ||
const log = require('../lib/log.js'); | ||
|
||
const flatten = arr => arr.reduce((a, b) => a.concat(b), []); | ||
const includes = (arr, elm) => arr.indexOf(elm) > -1; | ||
|
||
class RequestNode { | ||
get requestId() { | ||
return this.request.requestId; | ||
} | ||
|
||
constructor(request, parent) { | ||
// The children of a RequestNode are the requests initiated by it | ||
this.children = []; | ||
// The parent of a RequestNode is the request that initiated it | ||
this.parent = parent; | ||
|
||
this.request = request; | ||
} | ||
|
||
setParent(parentNode) { | ||
this.parent = parentNode; | ||
} | ||
|
||
addChild(childNode) { | ||
this.children.push(childNode); | ||
} | ||
|
||
toJSON() { | ||
// Prevents circular reference so we can print nodes when needed | ||
return `{ | ||
id: ${this.requestId}, | ||
parent: ${this.parent ? this.parent.requestId : null}, | ||
children: ${JSON.stringify(this.children.map(child => child.requestId))} | ||
}`; | ||
} | ||
|
||
} | ||
|
||
class CriticalNetworkChains extends Gather { | ||
|
||
get criticalPriorities() { | ||
// For now, critical request == render blocking request (as decided by | ||
// blink). Blink treats requests with the following priority levels as | ||
// render blocking. | ||
// See https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc | ||
return ['VeryHigh', 'High', 'Medium']; | ||
} | ||
|
||
getCriticalChains(networkRecords) { | ||
// Drop the first request because it's uninteresting - it's the page html | ||
// and always critical. No point including it in every request | ||
const criticalRequests = networkRecords.slice(1).filter( | ||
req => includes(this.criticalPriorities, req.initialPriority())); | ||
|
||
// Build a map of requestID -> Node. | ||
const requestIdToNodes = new Map(); | ||
for (let request of criticalRequests) { | ||
const requestNode = new RequestNode(request, null); | ||
requestIdToNodes.set(requestNode.requestId, requestNode); | ||
} | ||
|
||
// Connect the parents and children | ||
for (let request of criticalRequests) { | ||
if (request.initiatorRequest()) { | ||
const parentRequestId = request.initiatorRequest().requestId; | ||
const childNode = requestIdToNodes.get(request.requestId); | ||
const parentNode = requestIdToNodes.get(parentRequestId); | ||
if (childNode && parentNode) { | ||
// Both child and parent must be critical | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just curious: what does it mean (and is it possible) for a parent to not be critical but one of its children is critical? If possible, is that worth capturing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very good question. One way this case can happen is if a low priority script ends up initiating a high priority script. I don't know the internals of blink well enough yet to answer if this case is possible. However, I really don't know if we should call the initiated script "critical" in that case, because it's very possible the page already loaded by the time we see this high priority request, and no request should be called "critical" at that point. We can improve this by checking the timestamp of requests and discarding any request that gets fired after domContentLoaded, since it's not render-blocking any more. I haven't seen this happen in practice yet though. Also, when we have better critical request data that are not just based on request priority, this issue will disappear. The other way this happens and is absolutely worth fixing is when there are redirects. Having a request redirected sometimes sets the priority of the new request as 'VeryLow'. For example, the first request to theverge.com has prio |
||
// TODO: We may need handle redirects carefully. Investigate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. file an issue to track this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as soon as we merge this. |
||
childNode.setParent(parentNode); | ||
parentNode.addChild(childNode); | ||
} | ||
} | ||
} | ||
|
||
const nodesList = [...requestIdToNodes.values()]; | ||
const orphanNodes = nodesList.filter(node => node.parent === null); | ||
const nodeChains = flatten(orphanNodes.map( | ||
node => this._getChainsDFS(node))); | ||
const requestChains = nodeChains.map(chain => chain.map( | ||
node => node.request)); | ||
return requestChains; | ||
} | ||
|
||
postProfiling(options, tracingData) { | ||
const chains = this.getCriticalChains(tracingData.networkRecords); | ||
|
||
if (options.flags.useNetDepGraph) { | ||
// There logs are here so we can test this gatherer | ||
// Will be removed when we have a way to surface them in the report | ||
const nonTrivialChains = chains.filter(chain => chain.length > 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets sort them desc by length?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah actually we want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
// Note: Approximately, | ||
// startTime: time when request was dispatched | ||
// responseReceivedTime: either time to first byte, or time of receiving | ||
// the end of response headers | ||
// endTime: time when response loading finished | ||
const debuggingData = nonTrivialChains.map(chain => ({ | ||
urls: chain.map(request => request._url), | ||
totalRequests: chain.length, | ||
times: chain.map(request => ({ | ||
startTime: request.startTime, | ||
endTime: request.endTime, | ||
responseReceivedTime: request.responseReceivedTime | ||
})), | ||
totalTimeBetweenBeginAndEnd: | ||
(chain[chain.length - 1].endTime - chain[0].startTime), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's |
||
totalLoadingTime: chain.reduce((acc, req) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this number is valuable yet. But interesting to keep an eye on. |
||
acc + (req.endTime - req.responseReceivedTime), 0) | ||
})); | ||
log.log('info', 'cricital chains', JSON.stringify(debuggingData)); | ||
log.log('info', 'lengths of critical chains', debuggingData.map(d => d.totalRequests)); | ||
} | ||
return {CriticalNetworkChains: chains}; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type docs would help here (and above) for clarity, even if not checked. Hard to wade through all the flattens :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added some types. Did not annotate everything but I hope it makes it little more readable |
||
_getChainsDFS(startNode) { | ||
if (startNode.children.length === 0) { | ||
return [[startNode]]; | ||
} | ||
|
||
const childrenChains = flatten(startNode.children.map(child => | ||
this._getChainsDFS(child))); | ||
return childrenChains.map(chain => [startNode].concat(chain)); | ||
} | ||
} | ||
|
||
module.exports = CriticalNetworkChains; |
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.
Given this is pure JS, do we not want to remove these python files?
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.
(see the initial PR comment :)
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 okay... I'm not sure. Feels like we should keep it around in a separate branch on origin, but I don't want to merge it in unless we know we 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.
I'm not 200% confident about how correct my approach is or whether we'll end up needing the clovis graph for something else. Once we're sure clovis is never happening I'll purge all python from lighthouse.
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 didn't see your other comment. The python files were already on master. Do you want me to remove them and put them on other branch? There is still use for them in the sense you can do
lighthouse --save-artifacts
and then use the artifacts through clovis to generate the dependency graph png picture. If you want I can send out a separate PR removing all python/clovis code.