Commit a745fe44 authored by Paul Irish's avatar Paul Irish Committed by Commit Bot

DevTools: [Audits] Roll Lighthouse to v3.0-beta

Bug: 772558, 846211
Change-Id: I4f74ea0d4a84908d2e7c4ccfe7035ee421bdf023
Reviewed-on: https://chromium-review.googlesource.com/1070532
Commit-Queue: Paul Irish <paulirish@chromium.org>
Reviewed-by: default avatarPavel Feldman <pfeldman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#561412}
parent cb0ab8d4
......@@ -2343,6 +2343,9 @@ crbug.com/467477 virtual/disable-spv175/fast/multicol/vertical-rl/nested-columns
crbug.com/674225 [ Mac ] fast/replaced/input-radio-height-inside-auto-container.html [ Failure ]
crbug.com/846211 [ Linux ] http/tests/devtools/audits2/audits2-limited-run.js [ Skip ]
crbug.com/846211 [ Linux ] http/tests/devtools/audits2/audits2-successful-run.js [ Skip ]
crbug.com/400841 media/video-canvas-draw.html [ Failure ]
crbug.com/400841 virtual/video-surface-layer/media/video-canvas-draw.html [ Failure ]
crbug.com/400829 media/video-object-fit.html [ Failure ]
......
......@@ -12,20 +12,23 @@ Run audits: enabled visible
=============== Lighthouse Results ===============
bootup-time: true
consistently-interactive: false
critical-request-chains: true
dom-size: true
estimated-input-latency: false
first-interactive: false
first-meaningful-paint: false
efficient-animated-content: false
estimated-input-latency: true
first-contentful-paint: true
first-cpu-idle: true
first-meaningful-paint: true
font-display: true
link-blocking-first-paint: false
interactive: true
mainthread-work-breakdown: true
metrics: false
network-requests: true
offscreen-images: false
redirects: false
render-blocking-resources: false
screenshot-thumbnails: false
script-blocking-first-paint: false
speed-index-metric: false
speed-index: false
time-to-first-byte: true
total-byte-weight: true
unminified-css: false
......@@ -34,8 +37,9 @@ unused-css-rules: false
user-timings: true
uses-long-cache-ttl: false
uses-optimized-images: false
uses-rel-preconnect: false
uses-rel-preload: false
uses-request-compression: false
uses-responsive-images: false
uses-text-compression: false
uses-webp-images: false
......@@ -26,7 +26,7 @@
Object.keys(results.audits).sort().forEach(auditName => {
var audit = results.audits[auditName];
TestRunner.addResult(`${audit.name}: ${Boolean(audit.rawValue)}`);
TestRunner.addResult(`${audit.id}: ${Boolean(audit.rawValue)}`);
});
TestRunner.completeTest();
......
......@@ -16,7 +16,6 @@ Initializing…
Loading page & waiting for onload
Retrieving trace
Retrieving devtoolsLog and network records
Retrieving: URL
Retrieving: Scripts
Retrieving: CSSUsage
Retrieving: Viewport
......@@ -44,6 +43,7 @@ Retrieving: MetaRobots
Retrieving: Hreflang
Retrieving: EmbeddedContent
Retrieving: Canonical
Retrieving: RobotsTxt
Retrieving: Fonts
Loading page & waiting for onload
Retrieving devtoolsLog and network records
......@@ -62,18 +62,19 @@ Evaluating: Registers a service worker
Evaluating: Responds with a 200 when offline
Evaluating: Has a `<meta name="viewport">` tag with `width` or `initial-scale`
Evaluating: Contains some content when JavaScript is not available
Evaluating: First meaningful paint
Evaluating: First Contentful Paint
Evaluating: First Meaningful Paint
Evaluating: Page load is fast enough on 3G
Evaluating: Perceptual Speed Index
Evaluating: Speed Index
Evaluating: Screenshot Thumbnails
Evaluating: Estimated Input Latency
Evaluating: No browser errors logged to the console
Evaluating: Keep server response times low (TTFB)
Evaluating: First Interactive (beta)
Evaluating: Consistently Interactive (beta)
Evaluating: First CPU Idle
Evaluating: Time to Interactive
Evaluating: User Timing marks and measures
Evaluating: Critical Request Chains
Evaluating: Avoids page redirects
Evaluating: Avoid multiple page redirects
Evaluating: User can be prompted to Install the Web App
Evaluating: Configured for a custom splash screen
Evaluating: Address bar matches brand colors
......@@ -81,10 +82,13 @@ Evaluating: Manifest's `short_name` won't be truncated when displayed on homescr
Evaluating: Content is sized correctly for the viewport
Evaluating: Displays images with correct aspect ratio
Evaluating: Avoids deprecated APIs
Evaluating: Main thread work breakdown
Evaluating: Minimizes main thread work
Evaluating: JavaScript boot-up time
Evaluating: Preload key requests
Evaluating: Avoid multiple, costly round trips to any origin
Evaluating: All text remains visible during webfont loads
Evaluating: Network Requests
Evaluating: Metrics
Evaluating: Site works cross-browser
Evaluating: Page transitions don't feel like they block on the network
Evaluating: Each page has a URL
......@@ -135,26 +139,26 @@ Evaluating: HTML5 landmark elements are used to improve navigation
Evaluating: Visual order on the page follows DOM order
Evaluating: Uses efficient cache policy on static assets
Evaluating: Avoids enormous network payloads
Evaluating: Offscreen images
Evaluating: Defer offscreen images
Evaluating: Eliminate render-blocking resources
Evaluating: Minify CSS
Evaluating: Minify JavaScript
Evaluating: Unused CSS rules
Evaluating: Defer unused CSS
Evaluating: Serve images in next-gen formats
Evaluating: Optimize images
Evaluating: Efficiently encode images
Evaluating: Enable text compression
Evaluating: Properly size images
Evaluating: Use video formats for animated content
Evaluating: Avoids Application Cache
Evaluating: Avoids an excessive DOM size
Evaluating: Opens external anchors using `rel="noopener"`
Evaluating: Links to cross-origin destinations are safe
Evaluating: Avoids requesting the geolocation permission on page load
Evaluating: Reduce render-blocking stylesheets
Evaluating: Avoids `document.write()`
Evaluating: Avoids Mutation Events in its own scripts
Evaluating: Avoids front-end JavaScript libraries with known security vulnerabilities
Evaluating: Avoids WebSQL DB
Evaluating: Avoids requesting the notification permission on page load
Evaluating: Allows users to paste into password fields
Evaluating: Reduce render-blocking scripts
Evaluating: Uses HTTP/2 for its own resources
Evaluating: Uses passive listeners to improve scrolling performance
Evaluating: Document has a meta description
......@@ -162,6 +166,7 @@ Evaluating: Page has successful HTTP status code
Evaluating: Document uses legible font sizes
Evaluating: Links have descriptive text
Evaluating: Page isn’t blocked from indexing
Evaluating: robots.txt is valid
Evaluating: Document has a valid `hreflang`
Evaluating: Document avoids plugins
Evaluating: Document has a valid `rel=canonical`
......@@ -171,78 +176,81 @@ Generating results...
=============== Lighthouse Results ===============
URL: http://127.0.0.1:8000/devtools/resources/inspected-page.html
Version: 2.9.1
Version: 3.0.0-beta.0
accesskeys: false
accesskeys: true
appcache-manifest: true
aria-allowed-attr: false
aria-required-attr: false
aria-required-children: false
aria-required-parent: false
aria-roles: false
aria-valid-attr: false
aria-valid-attr-value: false
audio-caption: false
aria-allowed-attr: true
aria-required-attr: true
aria-required-children: true
aria-required-parent: true
aria-roles: true
aria-valid-attr: true
aria-valid-attr-value: true
audio-caption: true
bootup-time: true
button-name: false
bypass: false
button-name: true
bypass: true
canonical: true
color-contrast: false
consistently-interactive: false
color-contrast: true
content-width: false
critical-request-chains: true
custom-controls-labels: false
custom-controls-roles: false
definition-list: false
definition-list: true
deprecations: true
dlitem: false
dlitem: true
document-title: false
dom-size: true
duplicate-id: false
duplicate-id: true
efficient-animated-content: false
errors-in-console: false
estimated-input-latency: false
estimated-input-latency: true
external-anchors-use-rel-noopener: true
first-interactive: false
first-meaningful-paint: false
first-contentful-paint: true
first-cpu-idle: true
first-meaningful-paint: true
focus-traps: false
focusable-controls: false
font-display: true
font-size: false
frame-title: false
frame-title: true
geolocation-on-start: true
heading-levels: false
hreflang: true
html-has-lang: false
html-lang-valid: false
html-lang-valid: true
http-status-code: true
image-alt: false
image-alt: true
image-aspect-ratio: true
input-image-alt: false
input-image-alt: true
interactive: true
is-crawlable: true
is-on-https: true
label: false
layout-table: false
link-blocking-first-paint: false
link-name: false
label: true
layout-table: true
link-name: true
link-text: true
list: false
listitem: false
load-fast-enough-for-pwa: false
list: true
listitem: true
load-fast-enough-for-pwa: true
logical-tab-order: false
mainthread-work-breakdown: true
managed-focus: false
manifest-short-name-length: false
meta-description: false
meta-refresh: false
meta-viewport: false
meta-refresh: true
meta-viewport: true
metrics: false
mobile-friendly: false
network-requests: true
no-document-write: true
no-mutation-events: true
no-vulnerable-libraries: true
no-websql: true
notification-on-start: true
object-alt: false
object-alt: true
offscreen-content-hidden: false
offscreen-images: false
password-inputs-can-be-pasted-into: true
......@@ -252,15 +260,16 @@ pwa-each-page-has-url: false
pwa-page-transitions: false
redirects: false
redirects-http: false
render-blocking-resources: false
robots-txt: true
screenshot-thumbnails: false
script-blocking-first-paint: false
service-worker: false
speed-index-metric: false
speed-index: false
splash-screen: false
structured-data: false
tabindex: false
td-headers-attr: false
th-has-data-cells: false
tabindex: true
td-headers-attr: true
th-has-data-cells: true
themed-omnibox: false
time-to-first-byte: true
total-byte-weight: true
......@@ -273,18 +282,19 @@ uses-http2: false
uses-long-cache-ttl: false
uses-optimized-images: false
uses-passive-event-listeners: true
uses-rel-preconnect: false
uses-rel-preload: false
uses-request-compression: false
uses-responsive-images: false
uses-text-compression: false
uses-webp-images: false
valid-lang: false
video-caption: false
video-description: false
valid-lang: true
video-caption: true
video-description: true
viewport: false
visual-order-follows-dom: false
webapp-install-banner: false
without-javascript: false
works-offline: false
# of .lh-audit divs: 109
# of .lh-audit divs: 110
......@@ -16,13 +16,13 @@
var results = await Audits2TestRunner.waitForResults();
TestRunner.addResult(`\n=============== Lighthouse Results ===============`);
TestRunner.addResult(`URL: ${results.url}`);
TestRunner.addResult(`URL: ${results.finalUrl}`);
TestRunner.addResult(`Version: ${results.lighthouseVersion}`);
TestRunner.addResult('\n');
Object.keys(results.audits).sort().forEach(auditName => {
var audit = results.audits[auditName];
TestRunner.addResult(`${audit.name}: ${Boolean(audit.rawValue)}`);
TestRunner.addResult(`${audit.id}: ${Boolean(audit.rawValue)}`);
});
const resultsElement = Audits2TestRunner.getResultsElement();
......
......@@ -54,6 +54,7 @@ all_devtools_files = [
"front_end/audits2/lighthouse/renderer/dom.js",
"front_end/audits2/lighthouse/renderer/details-renderer.js",
"front_end/audits2/lighthouse/renderer/category-renderer.js",
"front_end/audits2/lighthouse/renderer/performance-category-renderer.js",
"front_end/audits2/lighthouse/renderer/crc-details-renderer.js",
"front_end/audits2/lighthouse/renderer/report-renderer.js",
"front_end/audits2/lighthouse/renderer/util.js",
......
......@@ -254,29 +254,50 @@ Audits2.RuntimeSettings = [
},
{
setting: Common.settings.createSetting('audits2.throttling', 'default'),
description: ls`Apply network and CPU throttling during performance auditing`,
setFlags: (flags, value) => {
flags.disableNetworkThrottling = value === 'off';
flags.disableCpuThrottling = value === 'off';
switch (value) {
case 'devtools':
flags.throttlingMethod = 'devtools';
break;
case 'off':
flags.throttlingMethod = 'provided';
break;
default:
flags.throttlingMethod = 'simulate';
}
},
options: [
{label: ls`Fast 3G with 4x CPU Slowdown`, value: 'default'},
{label: ls`No throttling`, value: 'off'},
{
label: ls`Simulated Fast 3G, 4x CPU Slowdown`,
value: 'default',
title: 'Throttling is simulated, resulting in faster audit runs with similar measurement accuracy'
},
{
label: ls`Applied Fast 3G, 4x CPU Slowdown`,
value: 'devtools',
title: 'Typical DevTools throttling, with actual traffic shaping and CPU slowdown applied'
},
{
label: ls`No throttling`,
value: 'off',
title: 'No network or CPU throttling used. (Useful when not evaluating performance)'
},
],
},
{
setting: Common.settings.createSetting('audits2.clear_storage', true),
title: ls`Clear storage`,
description: ls`Reset storage (localStorage, IndexedDB, etc) to a clean baseline before auditing`,
description: ls
`Reset storage (localStorage, IndexedDB, etc) before auditing. (Recommended for performance & PWA testing)`,
setFlags: (flags, value) => {
flags.disableStorageReset = !value;
},
},
];
Audits2.Events = {
PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
AuditProgressChanged: Symbol('AuditProgressChanged'),
RequestAuditStart: Symbol('RequestAuditStart'),
RequestAuditCancel: Symbol('RequestAuditCancel'),
};
\ No newline at end of file
Audits2.Events = {
PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
AuditProgressChanged: Symbol('AuditProgressChanged'),
RequestAuditStart: Symbol('RequestAuditStart'),
RequestAuditCancel: Symbol('RequestAuditCancel'),
};
......@@ -116,8 +116,9 @@ Audits2.Audits2Panel = class extends UI.Panel {
/**
* @param {!ReportRenderer.ReportJSON} lighthouseResult
* @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
*/
_renderReport(lighthouseResult) {
_renderReport(lighthouseResult, artifacts) {
this.contentElement.classList.toggle('in-progress', false);
this._startView.hideWidget();
this._statusView.hide();
......@@ -135,8 +136,7 @@ Audits2.Audits2Panel = class extends UI.Panel {
const dom = new DOM(/** @type {!Document} */ (this._auditResultsElement.ownerDocument));
const detailsRenderer = new Audits2.DetailsRenderer(dom);
const categoryRenderer = new Audits2.CategoryRenderer(dom, detailsRenderer);
categoryRenderer.setTraceArtifact(lighthouseResult);
const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
const renderer = new Audits2.ReportRenderer(dom, categoryRenderer);
const templatesHTML = Runtime.cachedResources['audits2/lighthouse/templates.html'];
......@@ -145,20 +145,22 @@ Audits2.Audits2Panel = class extends UI.Panel {
return;
renderer.setTemplateContext(templatesDOM);
renderer.renderReport(lighthouseResult, reportContainer);
const el = renderer.renderReport(lighthouseResult, reportContainer);
Audits2.ReportRenderer.addViewTraceButton(el, artifacts);
this._cachedRenderedReports.set(lighthouseResult, reportContainer);
}
/**
* @param {!ReportRenderer.ReportJSON} lighthouseResult
* @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
*/
_buildReportUI(lighthouseResult) {
_buildReportUI(lighthouseResult, artifacts) {
if (lighthouseResult === null)
return;
const optionElement = new Audits2.ReportSelector.Item(
lighthouseResult, () => this._renderReport(lighthouseResult), this._renderStartView.bind(this));
lighthouseResult, () => this._renderReport(lighthouseResult, artifacts), this._renderStartView.bind(this));
this._reportSelector.prepend(optionElement);
this._refreshToolbarUI();
this._renderReport(lighthouseResult);
......@@ -206,21 +208,21 @@ Audits2.Audits2Panel = class extends UI.Panel {
this._renderStatusView(inspectedURL);
const lighthouseResult = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags);
const lighthouseResponse = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags);
if (lighthouseResult && lighthouseResult.fatal) {
const error = new Error(lighthouseResult.message);
error.stack = lighthouseResult.stack;
if (lighthouseResponse && lighthouseResponse.fatal) {
const error = new Error(lighthouseResponse.message);
error.stack = lighthouseResponse.stack;
throw error;
}
if (!lighthouseResult)
if (!lighthouseResponse)
throw new Error('Auditing failed to produce a result');
Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Finished);
await this._resetEmulationAndProtocolConnection();
this._buildReportUI(lighthouseResult);
this._buildReportUI(lighthouseResponse.lhr, lighthouseResponse.artifacts);
} catch (err) {
if (err instanceof Error)
this._statusView.renderBugReport(err);
......
......@@ -28,7 +28,7 @@ Audits2.ProtocolService = class extends Common.Object {
* @param {string} auditURL
* @param {!Array<string>} categoryIDs
* @param {!Object} flags
* @return {!Promise<!ReportRenderer.ReportJSON>}
* @return {!Promise<!ReportRenderer.RunnerResult>}
*/
startLighthouse(auditURL, categoryIDs, flags) {
return this._send('start', {url: auditURL, categoryIDs, flags});
......@@ -80,7 +80,7 @@ Audits2.ProtocolService = class extends Common.Object {
/**
* @param {string} method
* @param {!Object=} params
* @return {!Promise<!ReportRenderer.ReportJSON>}
* @return {!Promise<!ReportRenderer.RunnerResult>}
*/
_send(method, params) {
if (!this._backendPromise)
......
......@@ -7,21 +7,22 @@
*/
Audits2.ReportRenderer = class extends ReportRenderer {
/**
* Provides empty element for left nav
* @override
* @returns {!DocumentFragment}
* @param {!Element} el Parent element to render the report into.
* @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
*/
_renderReportNav() {
return createDocumentFragment();
}
static addViewTraceButton(el, artifacts) {
if (!artifacts || !artifacts.traces || !artifacts.traces.defaultPass)
return;
/**
* @param {!ReportRenderer.ReportJSON} report
* @override
* @return {!DocumentFragment}
*/
_renderReportHeader(report) {
return createDocumentFragment();
const defaultPassTrace = artifacts.traces.defaultPass;
const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
el.querySelector('.lh-metric-column').appendChild(timelineButton);
return el;
async function onViewTraceClick() {
await UI.inspectorView.showPanel('timeline');
Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
}
}
};
......@@ -33,48 +34,6 @@ class ReportUIFeatures {
}
}
Audits2.CategoryRenderer = class extends CategoryRenderer {
/**
* @override
* @param {!DOM} dom
* @param {!DetailsRenderer} detailsRenderer
*/
constructor(dom, detailsRenderer) {
super(dom, detailsRenderer);
this._defaultPassTrace = null;
}
/**
* @param {!ReportRenderer.ReportJSON} lhr
*/
setTraceArtifact(lhr) {
if (!lhr.artifacts || !lhr.artifacts.traces || !lhr.artifacts.traces.defaultPass)
return;
this._defaultPassTrace = lhr.artifacts.traces.defaultPass;
}
/**
* @override
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groups
* @return {!Element}
*/
renderPerformanceCategory(category, groups) {
const defaultPassTrace = this._defaultPassTrace;
const element = super.renderPerformanceCategory(category, groups);
if (!defaultPassTrace)
return element;
const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
element.querySelector('.lh-audit-group').prepend(timelineButton);
return element;
async function onViewTraceClick() {
await UI.inspectorView.showPanel('timeline');
Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
}
}
};
Audits2.DetailsRenderer = class extends DetailsRenderer {
/**
......
......@@ -111,8 +111,8 @@ Audits2.ReportSelector.Item = class {
this._renderReport = renderReport;
this._showLandingCallback = showLandingCallback;
const url = new Common.ParsedURL(lighthouseResult.url);
const timestamp = lighthouseResult.generatedTime;
const url = new Common.ParsedURL(lighthouseResult.finalUrl);
const timestamp = lighthouseResult.fetchTime;
this._element = createElement('option');
this._element.label = `${new Date(timestamp).toLocaleTimeString()} - ${url.domain()}`;
}
......@@ -135,9 +135,9 @@ Audits2.ReportSelector.Item = class {
}
download() {
const url = new Common.ParsedURL(this._lighthouseResult.url).domain();
const timestamp = this._lighthouseResult.generatedTime;
const url = new Common.ParsedURL(this._lighthouseResult.finalUrl).domain();
const timestamp = this._lighthouseResult.fetchTime;
const fileName = `${url}-${new Date(timestamp).toISO8601Compact()}.json`;
Workspace.fileManager.save(fileName, JSON.stringify(this._lighthouseResult), true);
}
};
\ No newline at end of file
};
......@@ -16,13 +16,15 @@ Audits2.RadioSetting = class {
this._radioElements = [];
for (const option of this._options) {
const fragment = UI.Fragment.build`
<label class="audits2-radio">
<label $="label" class="audits2-radio">
<input $="input" type="radio" value=${option.value} name=${setting.name}>
${option.label}
</label>
`;
this.element.appendChild(fragment.element());
if (option.title)
UI.Tooltip.install(fragment.$('label'), option.title);
const radioElement = fragment.$('input');
radioElement.addEventListener('change', this._valueChanged.bind(this));
this._radioElements.push(radioElement);
......@@ -57,4 +59,4 @@ Audits2.RadioSetting = class {
const selectedRadio = this._radioElements.find(radio => radio.checked);
this._setting.set(selectedRadio.value);
}
};
\ No newline at end of file
};
......@@ -40,8 +40,7 @@
position: relative;
}
button.view-trace {
position: absolute;
right: 0;
margin: 10px;
}
.audits2-results-container {
......
......@@ -12,11 +12,13 @@
/* globals self Util */
/** @typedef {import('./dom.js')} DOM */
class CriticalRequestChainRenderer {
/**
* 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}}
* @param {LH.Audit.SimpleCriticalRequestNode} tree
* @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}}
*/
static initTree(tree) {
let startTime = 0;
......@@ -34,13 +36,13 @@ class CriticalRequestChainRenderer {
* 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 {LH.Audit.SimpleCriticalRequestNode} parent
* @param {string} id
* @param {number} startTime
* @param {number} transferSize
* @param {!Array<boolean>=} treeMarkers
* @param {Array<boolean>=} treeMarkers
* @param {boolean=} parentIsLastChild
* @return {!CriticalRequestChainRenderer.CRCSegment}
* @return {CRCSegment}
*/
static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) {
const node = parent[id];
......@@ -68,10 +70,10 @@ class CriticalRequestChainRenderer {
/**
* Creates the DOM for a tree segment.
* @param {!DOM} dom
* @param {!DocumentFragment} tmpl
* @param {!CriticalRequestChainRenderer.CRCSegment} segment
* @return {!Node}
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {CRCSegment} segment
* @return {Node}
*/
static createChainNode(dom, tmpl, segment) {
const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl);
......@@ -110,14 +112,14 @@ 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})` : '';
if (!segment.hasChildren) {
const span = dom.createElement('span', 'crc-node__chain-duration');
span.textContent = ' - ' + Util.chainDuration(
segment.node.request.startTime, segment.node.request.endTime) + 'ms, ';
const span2 = dom.createElement('span', 'crc-node__chain-duration');
span2.textContent = Util.formatBytesToKB(segment.node.request.transferSize);
span2.textContent = Util.formatBytesToKB(segment.node.request.transferSize, 0.01);
treevalEl.appendChild(span);
treevalEl.appendChild(span2);
......@@ -128,52 +130,48 @@ class CriticalRequestChainRenderer {
/**
* Recursively builds a tree from segments.
* @param {!DOM} dom
* @param {!DocumentFragment} tmpl
* @param {!CriticalRequestChainRenderer.CRCSegment} segment
* @param {!Element} detailsEl Parent details element.
* @param {!CriticalRequestChainRenderer.CRCDetailsJSON} details
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {CRCSegment} segment
* @param {Element} elem Parent element.
* @param {CRCDetailsJSON} details
*/
static buildTree(dom, tmpl, segment, detailsEl, details) {
detailsEl.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment));
static buildTree(dom, tmpl, segment, elem, details) {
elem.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment));
for (const key of Object.keys(segment.node.children)) {
const childSegment = CriticalRequestChainRenderer.createSegment(segment.node.children, key,
segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild);
CriticalRequestChainRenderer.buildTree(dom, tmpl, childSegment, detailsEl, details);
CriticalRequestChainRenderer.buildTree(dom, tmpl, childSegment, elem, details);
}
}
/**
* @param {!DOM} dom
* @param {!Node} templateContext
* @param {!CriticalRequestChainRenderer.CRCDetailsJSON} details
* @return {!Node}
* @param {DOM} dom
* @param {ParentNode} templateContext
* @param {CRCDetailsJSON} details
* @return {Element}
*/
static render(dom, templateContext, details) {
const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext);
const containerEl = dom.find('.lh-crc', tmpl);
// Fill in top summary.
dom.find('.lh-crc__longest_duration', tmpl).textContent =
Util.formatNumber(details.longestChain.duration) + 'ms';
dom.find('.lh-crc__longest_length', tmpl).textContent = details.longestChain.length;
dom.find('.lh-crc__longest_length', tmpl).textContent = details.longestChain.length.toString();
dom.find('.lh-crc__longest_transfersize', tmpl).textContent =
Util.formatBytesToKB(details.longestChain.transferSize);
const detailsEl = dom.find('.lh-details', tmpl);
detailsEl.open = true;
dom.find('.lh-details > summary', tmpl).textContent = details.header.text;
// Construct visual tree.
const root = CriticalRequestChainRenderer.initTree(details.chains);
for (const key of Object.keys(root.tree)) {
const segment = CriticalRequestChainRenderer.createSegment(root.tree, key,
root.startTime, root.transferSize);
CriticalRequestChainRenderer.buildTree(dom, tmpl, segment, detailsEl, details);
CriticalRequestChainRenderer.buildTree(dom, tmpl, segment, containerEl, details);
}
return tmpl;
return dom.find('.lh-crc-container', tmpl);
}
}
......@@ -185,44 +183,19 @@ if (typeof module !== 'undefined' && module.exports) {
}
/** @typedef {{
* type: string,
* header: {text: string},
* 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
* }}
type: string,
header: {text: string},
longestChain: {duration: number, length: number, transferSize: number},
chains: LH.Audit.SimpleCriticalRequestNode
}} CRCDetailsJSON
*/
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>
* }}
node: LH.Audit.SimpleCriticalRequestNode[string],
isLastChild: boolean,
hasChildren: boolean,
startTime: number,
transferSize: number,
treeMarkers: Array<boolean>
}} CRCSegment
*/
CriticalRequestChainRenderer.CRCSegment; // eslint-disable-line no-unused-expressions
......@@ -9,20 +9,21 @@
class DOM {
/**
* @param {!Document} document
* @param {Document} document
*/
constructor(document) {
/** @private {!Document} */
/** @type {Document} */
this._document = document;
}
// TODO(bckenny): can pass along `createElement`'s inferred type
/**
* @param {string} name
* @param {string=} className
* @param {!Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {!Element}
* @return {Element}
*/
createElement(name, className, attrs = {}) {
const element = this._document.createElement(name);
......@@ -39,13 +40,20 @@ class DOM {
}
/**
* @param {!Element} parentElem
* @return {DocumentFragment}
*/
createFragment() {
return this._document.createDocumentFragment();
}
/**
* @param {Element} parentElem
* @param {string} elementName
* @param {string=} className
* @param {!Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {!Element}
* @return {Element}
*/
createChildOf(parentElem, elementName, className, attrs) {
const element = this.createElement(elementName, className, attrs);
......@@ -55,8 +63,8 @@ class DOM {
/**
* @param {string} selector
* @param {!Node} context
* @return {!DocumentFragment} A clone of the template content.
* @param {ParentNode} context
* @return {DocumentFragment} A clone of the template content.
* @throws {Error}
*/
cloneTemplate(selector, context) {
......@@ -65,15 +73,14 @@ class DOM {
throw new Error(`Template not found: template${selector}`);
}
const clone = /** @type {!DocumentFragment} */ (this._document.importNode(
template.content, true));
const clone = this._document.importNode(template.content, true);
// Prevent duplicate styles in the DOM. After a template has been stamped
// for the first time, remove the clone's styles so they're not re-added.
if (template.hasAttribute('data-stamped')) {
this.findAll('style', clone).forEach(style => style.remove());
}
template.setAttribute('data-stamped', true);
template.setAttribute('data-stamped', 'true');
return clone;
}
......@@ -89,7 +96,7 @@ class DOM {
/**
* @param {string} text
* @return {!Element}
* @return {Element}
*/
convertMarkdownLinkSnippets(text) {
const element = this.createElement('span');
......@@ -104,7 +111,7 @@ class DOM {
// Append link if there are any.
if (linkText && linkHref) {
const a = /** @type {!HTMLAnchorElement} */ (this.createElement('a'));
const a = /** @type {HTMLAnchorElement} */ (this.createElement('a'));
a.rel = 'noopener';
a.target = '_blank';
a.textContent = linkText;
......@@ -118,7 +125,7 @@ class DOM {
/**
* @param {string} text
* @return {!Element}
* @return {Element}
*/
convertMarkdownCodeSnippets(text) {
const element = this.createElement('span');
......@@ -129,7 +136,7 @@ class DOM {
const [preambleText, codeText] = parts.splice(0, 2);
element.appendChild(this._document.createTextNode(preambleText));
if (codeText) {
const pre = /** @type {!HTMLPreElement} */ (this.createElement('code'));
const pre = /** @type {HTMLPreElement} */ (this.createElement('code'));
pre.textContent = codeText;
element.appendChild(pre);
}
......@@ -139,20 +146,29 @@ class DOM {
}
/**
* @return {!Document}
* @return {Document}
*/
document() {
return this._document;
}
/**
* TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
* @return {boolean}
*/
isDevTools() {
return !!this._document.querySelector('.lh-devtools');
}
/**
* Guaranteed context.querySelector. Always returns an element or throws if
* nothing matches query.
* @param {string} query
* @param {!Node} context
* @return {!Element}
* @param {ParentNode} context
* @return {HTMLElement}
*/
find(query, context) {
/** @type {?HTMLElement} */
const result = context.querySelector(query);
if (result === null) {
throw new Error(`query ${query} not found`);
......@@ -163,8 +179,8 @@ class DOM {
/**
* Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
* @param {string} query
* @param {!Node} context
* @return {!Array<!Element>}
* @param {ParentNode} context
* @return {Array<HTMLElement>}
*/
findAll(query, context) {
return Array.from(context.querySelectorAll(query));
......
/**
* @license Copyright 2018 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';
/* globals self, Util, CategoryRenderer */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./report-renderer.js').CategoryJSON} CategoryJSON */
/** @typedef {import('./report-renderer.js').GroupJSON} GroupJSON */
/** @typedef {import('./report-renderer.js').AuditJSON} AuditJSON */
/** @typedef {import('./details-renderer.js').OpportunitySummary} OpportunitySummary */
/** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {AuditJSON} audit
* @return {Element}
*/
_renderMetric(audit) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
const element = this.dom.find('.lh-metric', tmpl);
element.id = audit.result.id;
const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
element.classList.add(`lh-metric--${rating}`);
const titleEl = this.dom.find('.lh-metric__title', tmpl);
titleEl.textContent = audit.result.title;
const valueEl = this.dom.find('.lh-metric__value', tmpl);
valueEl.textContent = Util.formatDisplayValue(audit.result.displayValue);
const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
if (audit.result.scoreDisplayMode === 'error') {
descriptionEl.textContent = '';
valueEl.textContent = 'Error!';
const tooltip = this.dom.createChildOf(descriptionEl, 'span');
tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
}
return element;
}
/**
* @param {AuditJSON} audit
* @param {number} index
* @param {number} scale
* @return {Element}
*/
_renderOpportunity(audit, index, scale) {
const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.populateAuditValues(audit, index, oppTmpl);
element.id = audit.result.id;
const details = audit.result.details;
if (!details) {
return element;
}
const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
return element;
}
// Overwrite the displayValue with opportunity's wastedMs
const displayEl = this.dom.find('.lh-audit__display-text', element);
const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`;
this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
// Set [title] tooltips
if (audit.result.displayValue) {
const displayValue = Util.formatDisplayValue(audit.result.displayValue);
this.dom.find('.lh-load-opportunity__sparkline', element).title = displayValue;
displayEl.title = displayValue;
}
return element;
}
/**
* Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
* Opportunties with an error won't have a summary object, so MIN_VALUE is returned to keep any
* erroring opportunities last in sort order.
* @param {AuditJSON} audit
* @return {number}
*/
_getWastedMs(audit) {
if (
audit.result.details &&
audit.result.details.summary &&
typeof audit.result.details.summary.wastedMs === 'number'
) {
return audit.result.details.summary.wastedMs;
} else {
return Number.MIN_VALUE;
}
}
/**
* @param {CategoryJSON} category
* @param {Object<string, GroupJSON>} groups
* @return {Element}
* @override
*/
render(category, groups) {
const element = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category));
// Metrics
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
const metricAuditsEl = this.renderAuditGroup(groups['metrics'], {expandable: false});
const keyMetrics = metricAudits.filter(a => a.weight >= 3);
const otherMetrics = metricAudits.filter(a => a.weight < 3);
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metric-container');
const metricsColumn1El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
const metricsColumn2El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
keyMetrics.forEach(item => {
metricsColumn1El.appendChild(this._renderMetric(item));
});
otherMetrics.forEach(item => {
metricsColumn2El.appendChild(this._renderMetric(item));
});
const estValuesEl = this.dom.createChildOf(metricsColumn2El, 'div',
'lh-metrics__disclaimer lh-metrics__disclaimer');
estValuesEl.textContent = 'Values are estimated and may vary.';
metricAuditsEl.classList.add('lh-audit-group--metrics');
element.appendChild(metricAuditsEl);
// Filmstrip
const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
if (thumbnailResult && thumbnailResult.details) {
timelineEl.id = thumbnailResult.id;
const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
timelineEl.appendChild(filmstripEl);
}
// Opportunities
const opportunityAudits = category.auditRefs
.filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
if (opportunityAudits.length) {
// Scale the sparklines relative to savings, minimum 2s to not overstate small savings
const minimumScale = 2000;
const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
const maxWaste = Math.max(...wastedMsValues);
const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
const groupEl = this.renderAuditGroup(groups['load-opportunities'], {expandable: false});
const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
groupEl.appendChild(headerEl);
opportunityAudits.forEach((item, i) =>
groupEl.appendChild(this._renderOpportunity(item, i, scale)));
groupEl.classList.add('lh-audit-group--opportunities');
element.appendChild(groupEl);
}
// Diagnostics
const diagnosticAudits = category.auditRefs
.filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
.sort((a, b) => {
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
return scoreA - scoreB;
});
if (diagnosticAudits.length) {
const groupEl = this.renderAuditGroup(groups['diagnostics'], {expandable: false});
diagnosticAudits.forEach((item, i) => groupEl.appendChild(this.renderAudit(item, i)));
groupEl.classList.add('lh-audit-group--diagnostics');
element.appendChild(groupEl);
}
// Passed audits
const passedElements = category.auditRefs
.filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
Util.showAsPassed(audit.result))
.map((audit, i) => this.renderAudit(audit, i));
if (!passedElements.length) return element;
const passedElem = this.renderPassedAuditsSection(passedElements);
element.appendChild(passedElem);
return element;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = PerformanceCategoryRenderer;
} else {
self.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
}
......@@ -22,6 +22,7 @@
"lighthouse/renderer/util.js",
"lighthouse/renderer/dom.js",
"lighthouse/renderer/category-renderer.js",
"lighthouse/renderer/performance-category-renderer.js",
"lighthouse/renderer/details-renderer.js",
"lighthouse/renderer/crc-details-renderer.js",
"lighthouse/renderer/report-renderer.js",
......@@ -40,5 +41,14 @@
"audits2Panel.css",
"lighthouse/report-styles.css",
"lighthouse/templates.html"
]
],
"skip_compilation": [
"lighthouse/renderer/util.js",
"lighthouse/renderer/dom.js",
"lighthouse/renderer/category-renderer.js",
"lighthouse/renderer/performance-category-renderer.js",
"lighthouse/renderer/details-renderer.js",
"lighthouse/renderer/crc-details-renderer.js",
"lighthouse/renderer/report-renderer.js"
]
}
......@@ -37,7 +37,7 @@ var Audits2Service = class { // eslint-disable-line
}
/**
* @return {!Promise<!ReportRenderer.ReportJSON>}
* @return {!Promise<!ReportRenderer.RunnerResult>}
*/
start(params) {
if (Runtime.queryParam('isUnderTest'))
......@@ -49,7 +49,7 @@ var Audits2Service = class { // eslint-disable-line
return Promise.resolve()
.then(_ => self.runLighthouseInWorker(this, params.url, {flags: params.flags}, params.categoryIDs))
.then(/** @type {!ReportRenderer.ReportJSON} */ result => {
.then(/** @type {!ReportRenderer.RunnerResult} */ result => {
// Keep all artifacts on the result, no trimming
return result;
})
......
......@@ -143,7 +143,7 @@ Bindings.ChunkedFileReader = class {
if (event.target.readyState !== FileReader.DONE)
return;
const buffer = event.target.result;
const buffer = this._reader.result;
this._loadedSize += buffer.byteLength;
const endOfFile = this._loadedSize === this._fileSize;
const decodedString = this._decoder.decode(buffer, {stream: !endOfFile});
......
......@@ -836,3 +836,164 @@ const ls = function(strings, vararg) {};
* @param {function(!Array<*>)} callback
*/
const ResizeObserver = function(callback) {};
// Lighthouse Report Renderer
/**
* @constructor
* @param {!Document} document
*/
const DOM = function(document) {};
/**
* @constructor
* @param {!DOM} dom
*/
const ReportRenderer = function(dom) {};
ReportRenderer.prototype = {
/**
* @param {!ReportRenderer.ReportJSON} report
* @param {!Element} container Parent element to render the report into.
*/
renderReport: function(report, container) {},
/**
* @param {!Document|!Element} context
*/
setTemplateContext: function(context) {},
};
/**
* @typedef {{
* rawValue: (number|boolean|undefined),
* id: string,
* title: string,
* description: string,
* explanation: (string|undefined),
* errorMessage: (string|undefined),
* displayValue: (string|Array<string|number>|undefined),
* scoreDisplayMode: string,
* error: boolean,
* score: (number|null),
* details: (!DetailsRenderer.DetailsJSON|undefined),
* }}
*/
ReportRenderer.AuditResultJSON;
/**
* @typedef {{
* id: string,
* score: (number|null),
* weight: number,
* group: (string|undefined),
* result: ReportRenderer.AuditResultJSON
* }}
*/
ReportRenderer.AuditJSON;
/**
* @typedef {{
* title: string,
* id: string,
* score: (number|null),
* description: (string|undefined),
* manualDescription: string,
* auditRefs: !Array<!ReportRenderer.AuditJSON>
* }}
*/
ReportRenderer.CategoryJSON;
/**
* @typedef {{
* title: string,
* description: (string|undefined),
* }}
*/
ReportRenderer.GroupJSON;
/**
* @typedef {{
* lighthouseVersion: string,
* userAgent: string,
* fetchTime: string,
* timing: {total: number},
* requestedUrl: string,
* finalUrl: string,
* runWarnings: (!Array<string>|undefined),
* artifacts: {traces: {defaultPass: {traceEvents: !Array}}},
* audits: !Object<string, !ReportRenderer.AuditResultJSON>,
* categories: !Object<string, !ReportRenderer.CategoryJSON>,
* categoryGroups: !Object<string, !ReportRenderer.GroupJSON>,
* }}
*/
ReportRenderer.ReportJSON;
/**
* @typedef {{
* traces: {defaultPass: {traceEvents: !Array}},
* }}
*/
ReportRenderer.RunnerResultArtifacts;
/**
* @typedef {{
* lhr: !ReportRenderer.ReportJSON,
* artifacts: ReportRenderer.RunnerResultArtifacts,
* report: string
* }}
*/
ReportRenderer.RunnerResult;
/**
* @constructor
* @param {!DOM} dom
* @param {!DetailsRenderer} detailsRenderer
*/
const CategoryRenderer = function(dom, detailsRenderer) {};
/**
* @constructor
* @param {!DOM} dom
*/
const DetailsRenderer = function(dom) {};
DetailsRenderer.prototype = {
/**
* @param {!DetailsRenderer.NodeDetailsJSON} item
* @return {!Element}
*/
renderNode: function(item) {},
};
/**
* @typedef {{
* type: string,
* value: (string|number|undefined),
* summary: (DetailsRenderer.OpportunitySummary|undefined),
* granularity: (number|undefined),
* displayUnit: (string|undefined)
* }}
*/
DetailsRenderer.DetailsJSON;
/**
* @typedef {{
* type: string,
* path: (string|undefined),
* selector: (string|undefined),
* snippet:(string|undefined)
* }}
*/
DetailsRenderer.NodeDetailsJSON;
/** @typedef {{
* wastedMs: (number|undefined),
* wastedBytes: (number|undefined),
* }}
*/
DetailsRenderer.OpportunitySummary;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment