lighthouse
Version:
> Stops you crashing into the rocks; lights the way
434 lines (346 loc) • 11.1 kB
JavaScript
/**
* @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 traceviewer = require('traceviewer');
const URL = require('url');
class TraceProcessor {
get RESPONSE() {
return 'Response';
}
get ANIMATION() {
return 'Animation';
}
get LOAD() {
return 'Load';
}
analyzeTrace(contents, opts) {
let contentsJSON = null;
try {
contentsJSON = typeof contents === 'string' ? JSON.parse(contents) :
contents;
// If the file already wrapped the trace events in a
// traceEvents object, grab the contents of the object.
if (contentsJSON !== null &&
typeof contentsJSON.traceEvents !== 'undefined') {
contentsJSON = contentsJSON.traceEvents;
}
} catch (e) {
throw new Error('Invalid trace contents; not JSON');
}
const events = [JSON.stringify({
traceEvents: contentsJSON
})];
// Switch on all the globals we need, and import tracing.
const model = this.convertEventsToModel(events);
const processes = model.getAllProcesses();
let summarizable = [];
let traceProcess = null;
processes.forEach(candidate => {
if (typeof candidate.labels !== 'undefined' &&
candidate.labels.length > 0 &&
candidate.labels[0] !== 'chrome://tracing' &&
candidate.labels[0] !== 'BackgroundPage') {
summarizable.push(candidate);
}
if (typeof candidate.labels !== 'undefined' &&
candidate.labels[0] === 'BackgroundPage') {
const error = 'Extensions running during capture; ' +
'see http://bit.ly/bigrig-extensions';
if (typeof opts !== 'undefined' && opts.strict) {
throw error;
}
}
});
if (summarizable.length === 0) {
throw new Error('Zero processes (tabs) found.');
}
if (summarizable.length > 1) {
summarizable = summarizable
.filter(pr => !!pr.labels[0])
.slice(-1);
}
traceProcess = summarizable.pop();
return this.processTrace(model, traceProcess, opts);
}
convertEventsToModel(events) {
const io = new traceviewer.importer.ImportOptions();
io.showImportWarnings = false;
io.pruneEmptyContainers = false;
const model = new traceviewer.Model();
const importer = new traceviewer.importer.Import(model, io);
importer.importTraces(events);
return model;
}
processTrace(model, traceProcess, opts) {
const threads = this.getThreads(traceProcess);
const rendererThread = this.getThreadByName(traceProcess, 'CrRendererMain');
if (!rendererThread) {
throw new Error('Can\'t find renderer thread');
}
let timeRanges = this.getTimeRanges(rendererThread);
if (timeRanges.length === 0) {
timeRanges = [{
title: this.LOAD,
start: model.bounds.min,
duration: (model.bounds.max - model.bounds.min)
}];
}
return this.createRangesForTrace(timeRanges, threads, opts);
}
createRangesForTrace(timeRanges, threads, opts) {
const results = [];
const baseTypes = {
Load: this.LOAD
};
if (typeof opts === 'undefined') {
opts = {
types: baseTypes
};
}
if (typeof opts.types === 'undefined') {
opts.types = baseTypes;
}
/* eslint-disable */
// Disable linting because eslint can't differentiate JSON from non-JSON
// @see https://github.com/eslint/eslint/issues/3484
timeRanges.forEach(timeRange => {
let frames = 0;
const timeRangeEnd = timeRange.start + timeRange.duration;
const result = {
"start": timeRange.start,
"end": timeRangeEnd,
"duration": timeRange.duration,
"parseHTML": 0,
"javaScript": 0,
"javaScriptCompile": 0,
"styles": 0,
"updateLayerTree": 0,
"layout": 0,
"paint": 0,
"raster": 0,
"composite": 0,
"extendedInfo": {
"domContentLoaded": 0,
"loadTime": 0,
"firstPaint": 0,
"javaScript": {
}
},
"title": timeRange.title,
"type": opts.types[timeRange.title]
};
/* eslint-enable */
threads.forEach(thread => {
const slices = thread.sliceGroup.topLevelSlices;
let slice = null;
for (let s = 0; s < slices.length; s++) {
slice = slices[s];
if (slice.start < timeRange.start || slice.end > timeRangeEnd) {
continue;
}
slice.iterateAllDescendents(subslice => {
this.addDurationToResult(subslice, result);
});
}
thread.iterateAllEvents(evt => {
if (evt.start < timeRange.start || evt.end > timeRangeEnd) {
return;
}
switch (evt.title) {
case 'DrawFrame':
frames++;
break;
case 'MarkDOMContent':
result.extendedInfo.domContentLoaded = evt.start;
break;
case 'MarkLoad':
result.extendedInfo.loadTime = evt.start;
break;
case 'MarkFirstPaint':
result.extendedInfo.firstPaint = evt.start;
break;
default:
// Ignore
break;
}
});
});
if (typeof result.type === 'undefined') {
if (timeRange.title === this.LOAD) {
result.type = this.LOAD;
} else if (frames > 5) {
result.type = this.ANIMATION;
} else {
result.type = this.RESPONSE;
}
}
// Convert to fps.
if (result.type === this.ANIMATION) {
result.fps = Math.floor(frames / (result.duration / 1000));
result.frameCount = frames;
}
results.push(result);
});
return results;
}
hasStackInfo(slice) {
return slice.args &&
slice.args.beginData &&
slice.args.beginData.stackTrace &&
slice.args.beginData.stackTrace.length;
}
getJavascriptUrlFromStackInfo(slice) {
let url = null;
if (typeof slice.args.data === 'undefined') {
return url;
}
// Check for the URL in the slice.
// Failing that, look for scriptName.
if (typeof slice.args.data.url !== 'undefined' &&
slice.args.data.url !== '' &&
/^http/.test(slice.args.data.url)) {
url = slice.args.data.url;
} else if (typeof slice.args.data.scriptName !== 'undefined' &&
slice.args.data.scriptName !== '' &&
/^http/.test(slice.args.data.scriptName)) {
url = slice.args.data.scriptName;
}
return url;
}
addDurationToResult(slice, result) {
const duration = this.getBestDurationForSlice(slice);
let hasStack;
let owner;
switch (slice.title) {
case 'ParseHTML':
result.parseHTML += duration;
break;
case 'FunctionCall':
case 'EvaluateScript':
case 'V8.Execute':
case 'MajorGC':
case 'MinorGC':
case 'GCEvent':
result.javaScript += duration;
// If we have JS Stacks find out who the culprits are for the
// JavaScript that is running.
owner = this.getJavascriptUrlFromStackInfo(slice);
if (owner !== null) {
const url = URL.parse(owner);
const host = url.host;
if (!result.extendedInfo.javaScript[host]) {
result.extendedInfo.javaScript[host] = 0;
}
result.extendedInfo.javaScript[host] += duration;
}
break;
case 'v8.compile':
result.javaScriptCompile += duration;
break;
case 'UpdateLayoutTree':
case 'RecalculateStyles':
case 'ParseAuthorStyleSheet':
result.styles += duration;
// If there's a stack trace then this has been forced.
hasStack = this.hasStackInfo(slice);
if (hasStack) {
if (typeof result.extendedInfo.forcedRecalcs === 'undefined') {
result.extendedInfo.forcedRecalcs = 0;
}
result.extendedInfo.forcedRecalcs++;
}
break;
case 'UpdateLayerTree':
result.updateLayerTree += duration;
break;
case 'Layout':
result.layout += duration;
// If there's a stack trace then this has been forced.
hasStack = this.hasStackInfo(slice);
if (hasStack) {
if (typeof result.extendedInfo.forcedLayouts === 'undefined') {
result.extendedInfo.forcedLayouts = 0;
}
result.extendedInfo.forcedLayouts++;
}
break;
case 'Paint':
result.paint += duration;
break;
case 'RasterTask':
case 'Rasterize':
result.raster += duration;
break;
case 'CompositeLayers':
result.composite += duration;
break;
default:
// Disregard unknown types.
break;
}
}
getBestDurationForSlice(slice) {
let duration = 0;
if (typeof slice.cpuSelfTime !== 'undefined') {
duration = slice.cpuSelfTime;
} else if (typeof slice.cpuDuration !== 'undefined') {
duration = slice.cpuDuration;
} else if (typeof slice.duration !== 'undefined') {
duration = slice.duration;
}
return duration;
}
getThreads(traceProcess) {
const threadKeys = Object.keys(traceProcess.threads);
const threads = [];
threadKeys.forEach(threadKey => {
const thread = traceProcess.threads[threadKey];
if (typeof thread.name === 'undefined') {
return;
}
if (thread.name === 'Compositor' ||
thread.name === 'CrRendererMain' ||
thread.name.indexOf('CompositorTileWorker') >= 0) {
threads.push(thread);
}
});
return threads;
}
getThreadByName(traceProcess, name) {
const threadKeys = Object.keys(traceProcess.threads);
let threadKey = null;
let thread = null;
for (let t = 0; t < threadKeys.length; t++) {
threadKey = threadKeys[t];
thread = traceProcess.threads[threadKey];
if (thread.name === name) {
return thread;
}
}
return null;
}
getTimeRanges(thread) {
const timeRanges = [];
thread.iterateAllEvents(evt => {
if (evt.category === 'blink.console' && typeof evt.start === 'number') {
timeRanges.push(evt);
}
});
return timeRanges;
}
}
module.exports = new TraceProcessor();