This commit is contained in:
Eric Bidelman 2017-05-10 09:00:08 -07:00 коммит произвёл Brendan Kenny
Родитель 74ae236a81
Коммит 66941e56e8
26 изменённых файлов: 707 добавлений и 96 удалений

Просмотреть файл

@ -21,6 +21,10 @@ brace-expansion@^1.1.7:
balanced-match "^0.4.1"
concat-map "0.0.1"
browser-stdout@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
clang-format@^1.0.50:
version "1.0.50"
resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.0.50.tgz#b40926fd5c8573f7d37ed074a32da9a370dbdbcf"
@ -29,15 +33,35 @@ clang-format@^1.0.50:
glob "^7.0.0"
resolve "^1.1.6"
commander@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
graceful-readlink ">= 1.0.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
debug@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
dependencies:
ms "0.7.2"
diff@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
escape-string-regexp@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
glob@^7.0.0:
glob@7.1.1, glob@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@ -48,6 +72,18 @@ glob@^7.0.0:
once "^1.3.0"
path-is-absolute "^1.0.0"
"graceful-readlink@>= 1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
growl@1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -59,6 +95,57 @@ inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
json3@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
lodash._baseassign@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
dependencies:
lodash._basecopy "^3.0.0"
lodash.keys "^3.0.0"
lodash._basecopy@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
lodash._basecreate@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
lodash._isiterateecall@^3.0.0:
version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
lodash.create@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
dependencies:
lodash._baseassign "^3.0.0"
lodash._basecreate "^3.0.0"
lodash._isiterateecall "^3.0.0"
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.keys@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
dependencies:
lodash._getnative "^3.0.0"
lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0"
minimatch@^3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -115,6 +202,12 @@ resolve@^1.1.6:
dependencies:
path-parse "^1.0.5"
supports-color@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
dependencies:
has-flag "^1.0.0"
typescript@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.1.tgz#4862b662b988a4c8ff691cc7969622d24db76ae9"

Просмотреть файл

@ -68,7 +68,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
* @return {?Object}
*/
static computeWaste(image, viewportDimensions) {
const url = URL.getDisplayName(image.src, {preserveQuery: true});
const url = URL.getURLDisplayName(image.src, {preserveQuery: true});
const totalPixels = image.clientWidth * image.clientHeight;
const visiblePixels = this.computeVisiblePixels(image.clientRect, viewportDimensions);
// Treat images with 0 area as if they're offscreen. See https://github.com/GoogleChrome/lighthouse/issues/1914

Просмотреть файл

@ -65,7 +65,7 @@ class TotalByteWeight extends ByteEfficiencyAudit {
if (record.scheme === 'data' || !record.finished) return;
const result = {
url: URL.getDisplayName(record.url),
url: URL.getURLDisplayName(record.url),
totalBytes: record.transferSize,
totalKb: this.bytesToKbString(record.transferSize),
totalMs: this.bytesToMsString(record.transferSize, networkThroughput),

Просмотреть файл

@ -141,7 +141,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
const contentPreview = UnusedCSSRules.determineContentPreview(stylesheetInfo.content);
url = '*inline*```' + contentPreview + '```';
} else {
url = URL.getDisplayName(url);
url = URL.getURLDisplayName(url);
}
// If we don't know for sure how many bytes this sheet used on the network,

Просмотреть файл

@ -80,7 +80,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
return results;
}
const url = URL.getDisplayName(image.url);
const url = URL.getURLDisplayName(image.url);
const webpSavings = UsesOptimizedImages.computeSavings(image, 'webp');
if (webpSavings.bytes > WEBP_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES) {
@ -116,7 +116,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
let debugString;
if (failedImages.length) {
const urls = failedImages.map(image => URL.getDisplayName(image.url));
const urls = failedImages.map(image => URL.getURLDisplayName(image.url));
debugString = `Lighthouse was unable to decode some of your images: ${urls.join(', ')}`;
}

Просмотреть файл

@ -69,7 +69,7 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit {
}
// remove duplicates
const url = URL.getDisplayName(record.url);
const url = URL.getURLDisplayName(record.url);
const isDuplicate = results.find(res => res.url === url &&
res.totalBytes === record.resourceSize);
if (isDuplicate) {

Просмотреть файл

@ -54,7 +54,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
* @return {?Object}
*/
static computeWaste(image, DPR) {
const url = URL.getDisplayName(image.src);
const url = URL.getURLDisplayName(image.src);
const actualPixels = image.naturalWidth * image.naturalHeight;
const usedPixels = image.clientWidth * image.clientHeight * Math.pow(DPR, 2);
const wastedRatio = 1 - (usedPixels / actualPixels);

Просмотреть файл

@ -124,6 +124,8 @@ class CriticalRequestChains extends Audit {
walk(initialNavChildren, 0);
}
const longestChain = CriticalRequestChains._getLongestChain(chains);
return {
rawValue: chainCount <= this.meta.optimalValue,
displayValue: chainCount,
@ -132,8 +134,14 @@ class CriticalRequestChains extends Audit {
formatter: Formatter.SUPPORTED_FORMATS.CRITICAL_REQUEST_CHAINS,
value: {
chains,
longestChain: CriticalRequestChains._getLongestChain(chains)
longestChain
}
},
details: {
type: 'criticalrequestchain',
header: {type: 'text', text: 'View critical network waterfall:'},
chains,
longestChain
}
};
});

Просмотреть файл

@ -62,7 +62,7 @@ class Deprecations extends Audit {
const deprecationsV2 = entries.filter(log => log.entry.source === 'deprecation').map(log => {
// CSS deprecations can have missing URLs and lineNumbers. See https://crbug.com/680832.
const url = URL.isValid(log.entry.url) ? URL.getDisplayName(log.entry.url) : '';
const url = URL.isValid(log.entry.url) ? URL.getURLDisplayName(log.entry.url) : '';
return {
type: 'code',
text: log.entry.text,

Просмотреть файл

@ -77,7 +77,7 @@ class LinkBlockingFirstPaintAudit extends Audit {
endTime = Math.max(item.endTime, endTime);
return {
url: URL.getDisplayName(item.tag.url),
url: URL.getURLDisplayName(item.tag.url),
totalKb: `${Math.round(item.transferSize / 1024)} KB`,
totalMs: `${Math.round((item.endTime - startTime) * 1000)}ms`
};

Просмотреть файл

@ -68,7 +68,7 @@ class NoOldFlexboxAudit extends Audit {
if (!URL.isValid(url) || url === pageUrl) {
url = 'inline';
} else {
url = URL.getDisplayName(url);
url = URL.getURLDisplayName(url);
}
urlList.push({

Просмотреть файл

@ -58,7 +58,7 @@ class HTTPS extends Audit {
return artifacts.requestNetworkRecords(devtoolsLogs).then(networkRecords => {
const insecureRecords = networkRecords
.filter(record => !HTTPS.isSecureRecord(record))
.map(record => ({url: URL.getDisplayName(record.url, {preserveHost: true})}));
.map(record => ({url: URL.getURLDisplayName(record.url, {preserveHost: true})}));
let displayValue = '';
if (insecureRecords.length > 1) {

Просмотреть файл

@ -64,7 +64,7 @@ class TTFBMetric extends Audit {
if (networkRecord) {
const ttfb = TTFBMetric.caclulateTTFB(networkRecord);
results.push({
url: URL.getDisplayName(networkRecord._url),
url: URL.getURLDisplayName(networkRecord._url),
ttfb: `${Math.round(ttfb).toLocaleString()} ms`,
rawTTFB: ttfb
});

Просмотреть файл

@ -23,7 +23,7 @@
/* global self */
const ELLIPSIS = '\u2026';
const Util = require('../report/v2/renderer/util.js');
// TODO: Add back node require('url').URL parsing when bug is resolved:
// https://github.com/GoogleChrome/lighthouse/issues/1186
@ -77,63 +77,8 @@ URL.originsMatch = function originsMatch(urlA, urlB) {
* @param {{numPathParts: number, preserveQuery: boolean, preserveHost: boolean}=} options
* @return {string}
*/
URL.getDisplayName = function getDisplayName(url, options) {
options = Object.assign({
numPathParts: 2,
preserveQuery: true,
preserveHost: false,
}, options);
const parsed = new URL(url);
let name;
if (parsed.protocol === 'about:' || parsed.protocol === 'data:') {
// Handle 'about:*' and 'data:*' URLs specially since they have no path.
name = parsed.href;
} else {
name = parsed.pathname;
const parts = name.split('/').filter(part => part.length);
if (options.numPathParts && parts.length > options.numPathParts) {
name = ELLIPSIS + parts.slice(-1 * options.numPathParts).join('/');
}
if (options.preserveHost) {
name = `${parsed.host}/${name.replace(/^\//, '')}`;
}
if (options.preserveQuery) {
name = `${name}${parsed.search}`;
}
}
const MAX_LENGTH = 64;
// Always elide hash
name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`);
// Elide query params first
if (name.length > MAX_LENGTH && name.includes('?')) {
// Try to leave the first query parameter intact
name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`);
// Remove it all if it's still too long
if (name.length > MAX_LENGTH) {
name = name.replace(/\?.*/, `?${ELLIPSIS}`);
}
}
// Elide too long names next
if (name.length > MAX_LENGTH) {
const dotIndex = name.lastIndexOf('.');
if (dotIndex >= 0) {
name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) +
// Show file extension
`${ELLIPSIS}${name.slice(dotIndex)}`;
} else {
name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS;
}
}
return name;
URL.getURLDisplayName = function getURLDisplayName(url, options) {
return Util.getURLDisplayName(new URL(url), options);
};
// There is fancy URL rewriting logic for the chrome://settings page that we need to work around.

Просмотреть файл

@ -387,7 +387,7 @@ const handlebarHelpers = {
*/
parseURL: (resourceURL, opts) => {
const parsedURL = {
file: URL.getDisplayName(resourceURL),
file: URL.getURLDisplayName(resourceURL),
hostname: new URL(resourceURL).hostname
};

Просмотреть файл

@ -29,6 +29,8 @@ class CategoryRenderer {
this._detailsRenderer = detailsRenderer;
/** @private {!Document|!Element} */
this._templateContext = this._dom.document();
this._detailsRenderer.setTemplateContext(this._templateContext);
}
/**
@ -216,6 +218,7 @@ class CategoryRenderer {
*/
setTemplateContext(context) {
this._templateContext = context;
this._detailsRenderer.setTemplateContext(context);
}
/**

Просмотреть файл

@ -0,0 +1,244 @@
/**
* Copyright 2017 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';
/**
* @fileoverview This file contains helpers for constructing and rendering the
* critical request chains network tree.
*/
/* globals self Util */
class CriticalRequestChainRenderer {
/**
* @param {!DOM} dom
* @param {!Node} templateContext
*/
constructor(dom, templateContext) {
/** @private {!DOM} */
this._dom = dom;
/** @private {!DocumentFragment} */
this._tmpl = this._dom.cloneTemplate('#tmpl-lh-crc', templateContext);
/** @private {!CriticalRequestChainRenderer.CRCDetailsJSON} */
this._details; // eslint-disable-line no-unused-expressions
}
/**
* Create render context for critical-request-chain tree display.
* @param {!Object<string, !CriticalRequestChainRenderer.CRCNode>} tree
* @return {{tree: !Object<string, !CriticalRequestChainRenderer.CRCNode>, startTime: number, transferSize: number}}
*/
initTree(tree) {
let startTime = 0;
const rootNodes = Object.keys(tree);
if (rootNodes.length > 0) {
const node = tree[rootNodes[0]];
startTime = node.request.startTime;
}
return {tree, startTime, transferSize: 0};
}
/**
* Helper to create context for each critical-request-chain node based on its
* parent. Calculates if this node is the last child, whether it has any
* children itself and what the tree looks like all the way back up to the root,
* so the tree markers can be drawn correctly.
* @param {!Object<string, !CriticalRequestChainRenderer.CRCNode>} parent
* @param {string} id
* @param {number} startTime
* @param {number} transferSize
* @param {!Array<boolean>=} treeMarkers
* @param {boolean=} parentIsLastChild
* @return {!CriticalRequestChainRenderer.CRCSegment}
*/
createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) {
const node = parent[id];
const siblings = Object.keys(parent);
const isLastChild = siblings.indexOf(id) === (siblings.length - 1);
const hasChildren = Object.keys(node.children).length > 0;
// Copy the tree markers so that we don't change by reference.
const newTreeMarkers = Array.isArray(treeMarkers) ? treeMarkers.slice(0) : [];
// Add on the new entry.
if (typeof parentIsLastChild !== 'undefined') {
newTreeMarkers.push(!parentIsLastChild);
}
return {
node,
isLastChild,
hasChildren,
startTime,
transferSize: transferSize + node.request.transferSize,
treeMarkers: newTreeMarkers
};
}
/**
* Creates the DOM for a tree segment.
* @param {!CriticalRequestChainRenderer.CRCSegment} segment
* @return {!Node}
*/
createChainNode(segment) {
const chainsEl = this._dom.cloneTemplate('#tmpl-lh-crc__chains', this._tmpl);
// Hovering over request shows full URL.
this._dom.find('.crc-node', chainsEl).setAttribute('title', segment.node.request.url);
const treeMarkeEl = this._dom.find('.crc-node__tree-marker', chainsEl);
// Construct lines and add spacers for sub requests.
segment.treeMarkers.forEach(separator => {
if (separator) {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker vert'));
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker'));
} else {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker'));
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker'));
}
});
if (segment.isLastChild) {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker up-right'));
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker right'));
} else {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker vert-right'));
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker right'));
}
if (segment.hasChildren) {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker horiz-down'));
} else {
treeMarkeEl.appendChild(this._dom.createElement('span', 'tree-marker right'));
}
// Fill in url, host, and request size information.
const {file, hostname} = Util.parseURL(segment.node.request.url);
const treevalEl = this._dom.find('.crc-node__tree-value', chainsEl);
this._dom.find('.crc-node__tree-file', treevalEl).textContent = `${file}`;
this._dom.find('.crc-node__tree-hostname', treevalEl).textContent = `(${hostname})`;
if (!segment.hasChildren) {
const span = this._dom.createElement('span', 'crc-node__chain-duration');
span.textContent = ' - ' + Util.chainDuration(
segment.node.request.startTime, segment.node.request.endTime) + 'ms, ';
const span2 = this._dom.createElement('span', 'crc-node__chain-duration');
span2.textContent = Util.formateBytesToKB(this._details.longestChain.transferSize) + 'KB';
treevalEl.appendChild(span);
treevalEl.appendChild(span2);
}
return chainsEl;
}
/**
* Recursively builds a tree from segments.
* @param {!CriticalRequestChainRenderer.CRCSegment} segment
* @param {!Element} detailsEl Parent details element.
*/
buildTree(segment, detailsEl) {
detailsEl.appendChild(this.createChainNode(segment));
for (const key of Object.keys(segment.node.children)) {
const childSegment = this.createSegment(segment.node.children, key,
segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild);
this.buildTree(childSegment, detailsEl);
}
}
/**
* @param {!CriticalRequestChainRenderer.CRCDetailsJSON} details
* @return {!Node}
*/
render(details) {
this._details = details;
// Fill in top summary.
this._dom.find('.lh-crc__longest_duration', this._tmpl).textContent =
Util.formatNumber(details.longestChain.duration) + 'ms';
this._dom.find('.lh-crc__longest_length', this._tmpl).textContent = details.longestChain.length;
this._dom.find('.lh-crc__longest_transfersize', this._tmpl).textContent =
Util.formateBytesToKB(details.longestChain.transferSize) + 'KB';
const detailsEl = this._dom.find('.lh-details', this._tmpl);
this._dom.find('.lh-details > summary', this._tmpl).textContent = details.header.text;
// Construct visual tree.
const root = this.initTree(details.chains);
for (const key of Object.keys(root.tree)) {
const segment = this.createSegment(root.tree, key, root.startTime, root.transferSize);
this.buildTree(segment, detailsEl);
}
return this._tmpl;
}
}
// Allow Node require()'ing.
if (typeof module !== 'undefined' && module.exports) {
module.exports = CriticalRequestChainRenderer;
} else {
self.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
}
/** @typedef {{
* type: string,
* header: ({text: string}|undefined),
* longestChain: {duration: number, length: number, transferSize: number},
* chains: !Object<string, !CriticalRequestChainRenderer.CRCNode>
* }}
*/
CriticalRequestChainRenderer.CRCDetailsJSON; // eslint-disable-line no-unused-expressions
/** @typedef {{
* endTime: number,
* responseReceivedTime: number,
* startTime: number,
* transferSize: number,
* url: string
* }}
*/
CriticalRequestChainRenderer.CRCRequest; // eslint-disable-line no-unused-expressions
/**
* Record type so children can circularly have CRCNode values.
* @struct
* @record
*/
CriticalRequestChainRenderer.CRCNode = function() {};
/** @type {!Object<string, !CriticalRequestChainRenderer.CRCNode>} */
CriticalRequestChainRenderer.CRCNode.prototype.children; // eslint-disable-line no-unused-expressions
/** @type {!CriticalRequestChainRenderer.CRCRequest} */
CriticalRequestChainRenderer.CRCNode.prototype.request; // eslint-disable-line no-unused-expressions
/** @typedef {{
* node: !CriticalRequestChainRenderer.CRCNode,
* isLastChild: boolean,
* hasChildren: boolean,
* startTime: number,
* transferSize: number,
* treeMarkers: !Array<boolean>
* }}
*/
CriticalRequestChainRenderer.CRCSegment; // eslint-disable-line no-unused-expressions

Просмотреть файл

@ -15,7 +15,7 @@
*/
'use strict';
/* globals self */
/* globals self CriticalRequestChainRenderer */
class DetailsRenderer {
/**
@ -24,11 +24,20 @@ class DetailsRenderer {
constructor(dom) {
/** @private {!DOM} */
this._dom = dom;
/** @private {!Document|!Element} */
this._templateContext; // eslint-disable-line no-unused-expressions
}
/**
* @param {!Document|!Element} context
*/
setTemplateContext(context) {
this._templateContext = context;
}
/**
* @param {!DetailsRenderer.DetailsJSON} details
* @return {!Element}
* @return {!Node}
*/
render(details) {
switch (details.type) {
@ -46,6 +55,10 @@ class DetailsRenderer {
return this._renderCode(details);
case 'node':
return this.renderNode(/** @type {!DetailsRenderer.NodeDetailsJSON} */(details));
case 'criticalrequestchain': // eslint-disable-line no-case-declarations
const crcRenderer = new CriticalRequestChainRenderer(this._dom, this._templateContext);
return crcRenderer.render(
/** @type {!CriticalRequestChainRenderer.CRCDetailsJSON} */ (details));
case 'list':
return this._renderList(/** @type {!DetailsRenderer.ListDetailsJSON} */ (details));
default:
@ -111,7 +124,6 @@ class DetailsRenderer {
return element;
}
/**
* @param {!DetailsRenderer.TableDetailsJSON} details
* @return {!Element}
@ -255,7 +267,6 @@ DetailsRenderer.NodeDetailsJSON; // eslint-disable-line no-unused-expressions
*/
DetailsRenderer.TableDetailsJSON; // eslint-disable-line no-unused-expressions
/** @typedef {{
* type: string,
* url: ({text: string}|undefined),

Просмотреть файл

@ -15,7 +15,9 @@
*/
'use strict';
/* globals self */
/* globals self URL */
const ELLIPSIS = '\u2026';
const RATINGS = {
PASS: {label: 'pass', minScore: 75},
@ -48,6 +50,15 @@ class Util {
return number.toLocaleString(undefined, {maximumFractionDigits: 1});
}
/**
* @param {number} size
* @param {number=} decimalPlaces Number of decimal places to include. Defaults to 2.
* @return {string}
*/
static formateBytesToKB(size, decimalPlaces = 2) {
return (size / 1024).toLocaleString(undefined, {maximumFractionDigits: decimalPlaces});
}
/**
* Format time.
* @param {string} date
@ -69,6 +80,82 @@ class Util {
}
return formatter.format(new Date(date));
}
/**
* @param {!URL} parsedUrl
* @param {{numPathParts: (number|undefined), preserveQuery: (boolean|undefined), preserveHost: (boolean|undefined)}=} options
* @return {string}
*/
static getURLDisplayName(parsedUrl,
{numPathParts = 2, preserveQuery = true, preserveHost = false} = {}) {
let name;
if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') {
// Handle 'about:*' and 'data:*' URLs specially since they have no path.
name = parsedUrl.href;
} else {
name = parsedUrl.pathname;
const parts = name.split('/').filter(part => part.length);
if (numPathParts && parts.length > numPathParts) {
name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/');
}
if (preserveHost) {
name = `${parsedUrl.host}/${name.replace(/^\//, '')}`;
}
if (preserveQuery) {
name = `${name}${parsedUrl.search}`;
}
}
const MAX_LENGTH = 64;
// Always elide hash
name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`);
// Elide query params first
if (name.length > MAX_LENGTH && name.includes('?')) {
// Try to leave the first query parameter intact
name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`);
// Remove it all if it's still too long
if (name.length > MAX_LENGTH) {
name = name.replace(/\?.*/, `?${ELLIPSIS}`);
}
}
// Elide too long names next
if (name.length > MAX_LENGTH) {
const dotIndex = name.lastIndexOf('.');
if (dotIndex >= 0) {
name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) +
// Show file extension
`${ELLIPSIS}${name.slice(dotIndex)}`;
} else {
name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS;
}
}
return name;
}
/**
* Split a URL into a file and hostname for easy display.
* @param {string} url
* @return {{file: string, hostname: string}}
*/
static parseURL(url) {
const parsedUrl = new URL(url);
return {file: Util.getURLDisplayName(parsedUrl), hostname: parsedUrl.hostname};
}
/**
* @param {number} startTime
* @param {number} endTime
* @return {string}
*/
static chainDuration(startTime, endTime) {
return Util.formatNumber((endTime - startTime) * 1000);
}
}
if (typeof module !== 'undefined' && module.exports) {

Просмотреть файл

@ -23,6 +23,7 @@ const REPORT_JAVASCRIPT = [
fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'),

Просмотреть файл

@ -435,3 +435,78 @@
<div class="lh-gauge__label"><!-- fill me --></div>
</a>
</template>
<!-- Lighthouse crtiical request chains component -->
<template id="tmpl-lh-crc">
<style>
.lh-crc .tree-marker {
width: 12px;
height: 26px;
display: block;
float: left;
background-position: top left;
}
.lh-crc .horiz-down {
background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><g fill="#D8D8D8" fill-rule="evenodd"><path d="M16 12v2H-2v-2z"/><path d="M9 12v14H7V12z"/></g></svg>');
}
.lh-crc .right {
background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M16 12v2H0v-2z" fill="#D8D8D8" fill-rule="evenodd"/></svg>');
}
.lh-crc .up-right {
background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v14H7zm2 12h7v2H9z" fill="#D8D8D8" fill-rule="evenodd"/></svg>');
}
.lh-crc .vert-right {
background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v27H7zm2 12h7v2H9z" fill="#D8D8D8" fill-rule="evenodd"/></svg>');
}
.lh-crc .vert {
background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v26H7z" fill="#D8D8D8" fill-rule="evenodd"/></svg>');
}
.lh-crc .crc-tree {
font-size: 14px;
width: 100%;
overflow-x: auto;
}
.lh-crc .crc-node {
height: 26px;
line-height: 26px;
white-space: nowrap;
}
.lh-crc .crc-node__tree-value {
margin-left: 10px;
}
.lh-crc .crc-node__chain-duration {
font-weight: 700;
}
.lh-crc .crc-node__tree-hostname {
color: #595959;
}
.lh-crc .crc-initial-nav {
color: #595959;
font-style: italic;
}
</style>
<div class="lh-score__description">
Longest chain: <b class="lh-crc__longest_duration"><!-- fill me: longestChain.duration --></b>
over <b class="lh-crc__longest_length"><!-- fill me: longestChain.length --></b> requests, totalling
<b class="lh-crc__longest_transfersize"><!-- fill me: longestChain.length --></b>
</div>
<div class="lh-crc">
<details class="lh-details">
<summary><!-- fill me --></summary>
<div class="crc-initial-nav">Initial Navigation</div>
<!-- stamp for each chain -->
<template id="tmpl-lh-crc__chains">
<div class="crc-node">
<span class="crc-node__tree-marker">
<!-- fill me -->
</span>
<span class="crc-node__tree-value">
<span class="crc-node__tree-file"><!-- fill me: node.request.url.file --></span>
<span class="crc-node__tree-hostname">(<!-- fill me: node.request.url.host -->)</span>
<!-- fill me -->
</span>
</div>
</template>
</details>
</div>
</template>

Просмотреть файл

@ -15,7 +15,7 @@
# https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/audits2/module.json#L20
# (Currently this doesnt include logger or report-features)
files_to_include="\"lighthouse\/renderer\/util.js\", \"lighthouse\/renderer\/dom.js\", \"lighthouse\/renderer\/category-renderer.js\", \"lighthouse\/renderer\/details-renderer.js\", \"lighthouse\/renderer\/report-renderer.js\","
files_to_include="\"lighthouse\/renderer\/util.js\", \"lighthouse\/renderer\/dom.js\", \"lighthouse\/renderer\/category-renderer.js\", \"lighthouse\/renderer\/crc-details-renderer.js\", \"lighthouse\/renderer\/details-renderer.js\", \"lighthouse\/renderer\/report-renderer.js\","
# -----------------------------

Просмотреть файл

@ -15,6 +15,8 @@
*/
'use strict';
/* eslint-env mocha */
const Audit = require('../../audits/critical-request-chains.js');
const assert = require('assert');
@ -91,17 +93,18 @@ const mockArtifacts = (mockChain) => {
};
};
/* eslint-env mocha */
describe('Performance: critical-request-chains audit', () => {
it('calculates the correct chain result for failing example', () => {
return Audit.audit(mockArtifacts(FAILING_REQUEST_CHAIN)).then(output => {
assert.equal(output.displayValue, 2);
assert.equal(output.rawValue, false);
assert.ok(output.details);
});
});
it('calculates the correct chain result for passing example', () => {
return Audit.audit(mockArtifacts(PASSING_REQUEST_CHAIN)).then(output => {
assert.equal(output.details.longestChain.duration, 1000);
assert.equal(output.displayValue, 0);
assert.equal(output.rawValue, true);
});

Просмотреть файл

@ -82,51 +82,51 @@ describe('URL Shim', () => {
assert.equal(URL.originsMatch(urlA, urlB), false);
});
describe('getDisplayName', () => {
describe('getURLDisplayName', () => {
it('respects numPathParts option', () => {
const url = 'http://example.com/a/deep/nested/file.css';
const result = URL.getDisplayName(url, {numPathParts: 3});
const result = URL.getURLDisplayName(url, {numPathParts: 3});
assert.equal(result, '\u2026deep/nested/file.css');
});
it('respects preserveQuery option', () => {
const url = 'http://example.com/file.css?aQueryString=true';
const result = URL.getDisplayName(url, {preserveQuery: false});
const result = URL.getURLDisplayName(url, {preserveQuery: false});
assert.equal(result, '/file.css');
});
it('respects preserveHost option', () => {
const url = 'http://example.com/file.css';
const result = URL.getDisplayName(url, {preserveHost: true});
const result = URL.getURLDisplayName(url, {preserveHost: true});
assert.equal(result, 'example.com/file.css');
});
it('Elides hashes', () => {
const url = 'http://example.com/file-f303dec6eec305a4fab8025577db3c2feb418148ac75ba378281399fb1ba670b.css';
const result = URL.getDisplayName(url);
const result = URL.getURLDisplayName(url);
assert.equal(result, '/file-f303dec\u2026.css');
});
it('Elides hashes in the middle', () => {
const url = 'http://example.com/file-f303dec6eec305a4fab80378281399fb1ba670b-somethingmore.css';
const result = URL.getDisplayName(url);
const result = URL.getURLDisplayName(url);
assert.equal(result, '/file-f303dec\u2026-somethingmore.css');
});
it('Elides query strings when can first parameter', () => {
const url = 'http://example.com/file.css?aQueryString=true&other_long_query_stuff=false&some_other_super_long_query';
const result = URL.getDisplayName(url);
const result = URL.getURLDisplayName(url);
assert.equal(result, '/file.css?aQueryString=\u2026');
});
it('Elides query strings when cannot preserve first parameter', () => {
const url = 'http://example.com/file.css?superDuperNoGoodVeryLongExtraSpecialOnlyTheBestEnourmousQueryString=true';
const result = URL.getDisplayName(url);
const result = URL.getURLDisplayName(url);
assert.equal(result, '/file.css?\u2026');
});
it('Elides long names', () => {
const result = URL.getDisplayName(superLongName);
const result = URL.getURLDisplayName(superLongName);
const expected = '/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichWe\u2026.js';
assert.equal(result, expected);
});
@ -134,20 +134,20 @@ describe('URL Shim', () => {
it('Elides long names with hash', () => {
const url = superLongName.slice(0, -3) +
'-f303dec6eec305a4fab8025577db3c2feb418148ac75ba378281399fb1ba670b.css';
const result = URL.getDisplayName(url);
const result = URL.getURLDisplayName(url);
const expected = '/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichW\u2026.css';
assert.equal(result, expected);
});
it('Elides path parts properly', () => {
assert.equal(URL.getDisplayName('http://example.com/file.css'), '/file.css');
assert.equal(URL.getDisplayName('http://t.co//file.css'), '//file.css');
assert.equal(URL.getDisplayName('http://t.co/a/file.css'), '/a/file.css');
assert.equal(URL.getDisplayName('http://t.co/a/b/file.css'), '\u2026b/file.css');
assert.equal(URL.getURLDisplayName('http://example.com/file.css'), '/file.css');
assert.equal(URL.getURLDisplayName('http://t.co//file.css'), '//file.css');
assert.equal(URL.getURLDisplayName('http://t.co/a/file.css'), '/a/file.css');
assert.equal(URL.getURLDisplayName('http://t.co/a/b/file.css'), '\u2026b/file.css');
});
it('Elides path parts properly when used with preserveHost', () => {
const getResult = path => URL.getDisplayName(`http://g.co${path}`, {preserveHost: true});
const getResult = path => URL.getURLDisplayName(`http://g.co${path}`, {preserveHost: true});
assert.equal(getResult('/file.css'), 'g.co/file.css');
assert.equal(getResult('/img/logo.jpg'), 'g.co/img/logo.jpg');
assert.equal(getResult('//logo.jpg'), 'g.co//logo.jpg');

Просмотреть файл

@ -88,7 +88,7 @@ describe('CRC partial generation', () => {
Handlebars.registerHelper(handlebarHelpers);
const template = Handlebars.compile(partialHtml);
const output = template(extendedInfo).split('\n').join('');
const filename = URL.getDisplayName(superLongName);
const filename = URL.getURLDisplayName(superLongName);
const filenameRegExp = new RegExp(filename, 'im');
const expectedTreeOne = new RegExp([
'<div class="cnc-node" title="https://example.com/">',

Просмотреть файл

@ -0,0 +1,141 @@
/**
* Copyright 2017 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';
/* eslint-env mocha */
const assert = require('assert');
const fs = require('fs');
const jsdom = require('jsdom');
const URL = require('../../../../lib/url-shim');
const Util = require('../../../../report/v2/renderer/util.js');
const DOM = require('../../../../report/v2/renderer/dom.js');
const CriticalRequestChainRenderer =
require('../../../../report/v2/renderer/crc-details-renderer.js');
const TEMPLATE_FILE = fs.readFileSync(__dirname + '/../../../../report/v2/templates.html', 'utf8');
const superLongURL =
'https://example.com/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichWeWantToTest.js';
const DETAILS = {
type: 'criticalrequestchain',
header: {type: 'text', text: 'CRC Header'},
chains: {
0: {
request: {
endTime: 1,
responseReceivedTime: 5,
startTime: 0,
url: 'https://example.com/',
transferSize: 1
},
children: {
1: {
request: {
endTime: 16,
responseReceivedTime: 14,
startTime: 11,
url: 'https://example.com/b.js',
transferSize: 1
},
children: {}
},
2: {
request: {
endTime: 17.123456789,
responseReceivedTime: 15,
startTime: 12,
url: superLongURL,
transferSize: 1
},
children: {}
},
3: {
request: {
endTime: 18,
responseReceivedTime: 16,
startTime: 13,
url: 'about:blank',
transferSize: 1
},
children: {}
}
}
}
},
longestChain: {
duration: 7000,
length: 2,
transferSize: 1
}
};
describe('DetailsRenderer', () => {
let renderer;
let dom;
before(() => {
global.URL = URL;
global.Util = Util;
const document = jsdom.jsdom(TEMPLATE_FILE);
dom = new DOM(document);
renderer = new CriticalRequestChainRenderer(dom, dom.document());
});
after(() => {
global.URL = undefined;
global.Util = undefined;
});
it('renders tree structure', () => {
const el = renderer.render(DETAILS);
const details = el.querySelector('.lh-details');
const chains = details.querySelectorAll('.crc-node');
assert.equal(chains.length, 4, 'generates correct number of chain nodes');
const div = dom.createElement('div');
div.innerHTML = `<span class="crc-node__tree-marker">
<!-- fill me -->
<span class="tree-marker up-right"></span>
<span class="tree-marker right"></span>
<span class="tree-marker horiz-down"></span>
</span>
<span class="crc-node__tree-value">
<span class="crc-node__tree-file">/</span>
<span class="crc-node__tree-hostname">(example.com)</span>
<!-- fill me -->
</span>`;
assert.ok(chains[0].innerHTML, div.innerHTML);
div.innerHTML = `<span class="crc-node__tree-marker">
<!-- fill me -->
<span class="tree-marker"></span>
<span class="tree-marker"></span>
<span class="tree-marker vert-right"></span>
<span class="tree-marker right"></span>
<span class="tree-marker right"></span>
</span>
<span class="crc-node__tree-value">
<span class="crc-node__tree-file">/b.js</span>
<span class="crc-node__tree-hostname">(example.com)</span>
<!-- fill me -->
<span class="crc-node__chain-duration"> - 5,000ms, </span>
<span class="crc-node__chain-duration">0KB</span>
</span>`;
assert.ok(chains[1].innerHTML, div.innerHTML);
});
});