Skip to content

Commit

Permalink
core(screenshots): align filmstrip to observed metrics (#4965)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored and brendankenny committed Apr 13, 2018
1 parent 9ea3c17 commit 67947e8
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 91 deletions.
116 changes: 63 additions & 53 deletions lighthouse-core/audits/screenshot-thumbnails.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
'use strict';

const Audit = require('./audit');
const TTFI = require('./first-interactive');
const TTCI = require('./consistently-interactive');
const LHError = require('../lib/errors');
const jpeg = require('jpeg-js');

const NUMBER_OF_THUMBNAILS = 10;
Expand All @@ -23,7 +22,7 @@ class ScreenshotThumbnails extends Audit {
informative: true,
description: 'Screenshot Thumbnails',
helpText: 'This is what the load of your site looked like.',
requiredArtifacts: ['traces'],
requiredArtifacts: ['traces', 'devtoolsLogs'],
};
}

Expand Down Expand Up @@ -67,62 +66,73 @@ class ScreenshotThumbnails extends Audit {
* @param {!Artifacts} artifacts
* @return {!AuditResult}
*/
static audit(artifacts, context) {
static async audit(artifacts, context) {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
const cachedThumbnails = new Map();

return Promise.all([
artifacts.requestSpeedline(trace),
TTFI.audit(artifacts, context).catch(() => ({rawValue: 0})),
TTCI.audit(artifacts, context).catch(() => ({rawValue: 0})),
]).then(([speedline, ttfi, ttci]) => {
const thumbnails = [];
const analyzedFrames = speedline.frames.filter(frame => !frame.isProgressInterpolated());
const maxFrameTime =
speedline.complete ||
Math.max(...speedline.frames.map(frame => frame.getTimeStamp() - speedline.beginning));
// Find thumbnails to cover the full range of the trace (max of last visual change and time
// to interactive).
const timelineEnd = Math.max(maxFrameTime, ttfi.rawValue, ttci.rawValue);

for (let i = 1; i <= NUMBER_OF_THUMBNAILS; i++) {
const targetTimestamp = speedline.beginning + timelineEnd * i / NUMBER_OF_THUMBNAILS;

let frameForTimestamp = null;
if (i === NUMBER_OF_THUMBNAILS) {
frameForTimestamp = analyzedFrames[analyzedFrames.length - 1];
} else {
analyzedFrames.forEach(frame => {
if (frame.getTimeStamp() <= targetTimestamp) {
frameForTimestamp = frame;
}
});
}

const imageData = frameForTimestamp.getParsedImage();
const thumbnailImageData = ScreenshotThumbnails.scaleImageToThumbnail(imageData);
const base64Data =
cachedThumbnails.get(frameForTimestamp) ||
jpeg.encode(thumbnailImageData, 90).data.toString('base64');

cachedThumbnails.set(frameForTimestamp, base64Data);
thumbnails.push({
timing: Math.round(targetTimestamp - speedline.beginning),
timestamp: targetTimestamp * 1000,
data: base64Data,
const speedline = await artifacts.requestSpeedline(trace);

let minimumTimelineDuration = 0;
// Ensure thumbnails cover the full range of the trace (TTI can be later than visually complete)
if (context.settings.throttlingMethod !== 'simulate') {
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
const metricComputationData = {trace, devtoolsLog, settings: context.settings};
const ttci = artifacts.requestConsistentlyInteractive(metricComputationData);
try {
minimumTimelineDuration = (await ttci).timing;
} catch (_) {
minimumTimelineDuration = 0;
}
}

const thumbnails = [];
const analyzedFrames = speedline.frames.filter(frame => !frame.isProgressInterpolated());
const maxFrameTime =
speedline.complete ||
Math.max(...speedline.frames.map(frame => frame.getTimeStamp() - speedline.beginning));
const timelineEnd = Math.max(maxFrameTime, minimumTimelineDuration);

if (!analyzedFrames.length || !Number.isFinite(timelineEnd)) {
throw new LHError(LHError.errors.INVALID_SPEEDLINE);
}

for (let i = 1; i <= NUMBER_OF_THUMBNAILS; i++) {
const targetTimestamp = speedline.beginning + timelineEnd * i / NUMBER_OF_THUMBNAILS;

let frameForTimestamp = null;
if (i === NUMBER_OF_THUMBNAILS) {
frameForTimestamp = analyzedFrames[analyzedFrames.length - 1];
} else {
analyzedFrames.forEach(frame => {
if (frame.getTimeStamp() <= targetTimestamp) {
frameForTimestamp = frame;
}
});
}

return {
score: 1,
rawValue: thumbnails.length > 0,
details: {
type: 'filmstrip',
scale: timelineEnd,
items: thumbnails,
},
};
});
const imageData = frameForTimestamp.getParsedImage();
const thumbnailImageData = ScreenshotThumbnails.scaleImageToThumbnail(imageData);
const base64Data =
cachedThumbnails.get(frameForTimestamp) ||
jpeg.encode(thumbnailImageData, 90).data.toString('base64');

cachedThumbnails.set(frameForTimestamp, base64Data);
thumbnails.push({
timing: Math.round(targetTimestamp - speedline.beginning),
timestamp: targetTimestamp * 1000,
data: base64Data,
});
}

return {
score: 1,
rawValue: thumbnails.length > 0,
details: {
type: 'filmstrip',
scale: timelineEnd,
items: thumbnails,
},
};
}
}

Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const ERRORS = {
NO_SPEEDLINE_FRAMES: {message: strings.didntCollectScreenshots},
SPEEDINDEX_OF_ZERO: {message: strings.didntCollectScreenshots},
NO_SCREENSHOTS: {message: strings.didntCollectScreenshots},
INVALID_SPEEDLINE: {message: strings.didntCollectScreenshots},

// Trace parsing errors
NO_TRACING_STARTED: {message: strings.badTraceRecording},
Expand Down
73 changes: 35 additions & 38 deletions lighthouse-core/test/audits/screenshot-thumbnails-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,26 @@ const assert = require('assert');

const Runner = require('../../runner.js');
const ScreenshotThumbnailsAudit = require('../../audits/screenshot-thumbnails');
const TTFIAudit = require('../../audits/first-interactive');
const TTCIAudit = require('../../audits/consistently-interactive');
const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
const pwaDevtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');

/* eslint-env mocha */

describe('Screenshot thumbnails', () => {
let computedArtifacts;
let ttfiOrig;
let ttciOrig;
let ttfiReturn;
let ttciReturn;

before(() => {
computedArtifacts = Runner.instantiateComputedArtifacts();

// Monkey patch TTFI to simulate result
ttfiOrig = TTFIAudit.audit;
ttciOrig = TTCIAudit.audit;
TTFIAudit.audit = () => ttfiReturn || Promise.reject(new Error('oops!'));
TTCIAudit.audit = () => ttciReturn || Promise.reject(new Error('oops!'));
});

after(() => {
TTFIAudit.audit = ttfiOrig;
TTCIAudit.audit = ttciOrig;
});

beforeEach(() => {
ttfiReturn = null;
ttciReturn = null;
});

it('should extract thumbnails from a trace', () => {
const settings = {throttlingMethod: 'provided'};
const artifacts = Object.assign({
traces: {defaultPass: pwaTrace},
devtoolsLogs: {}, // empty devtools logs to test just thumbnails without TTI behavior
}, computedArtifacts);

return ScreenshotThumbnailsAudit.audit(artifacts).then(results => {
return ScreenshotThumbnailsAudit.audit(artifacts, {settings}).then(results => {
results.details.items.forEach((result, index) => {
const framePath = path.join(__dirname,
`../fixtures/traces/screenshots/progressive-app-frame-${index}.jpg`);
Expand All @@ -65,34 +46,50 @@ describe('Screenshot thumbnails', () => {
});
}).timeout(10000);

it('should scale the timeline to TTFI', () => {
it('should scale the timeline to TTCI when observed', () => {
const settings = {throttlingMethod: 'devtools'};
const artifacts = Object.assign({
traces: {defaultPass: pwaTrace},
devtoolsLogs: {defaultPass: pwaDevtoolsLog},
}, computedArtifacts);

ttfiReturn = Promise.resolve({rawValue: 4000});
return ScreenshotThumbnailsAudit.audit(artifacts).then(results => {
assert.equal(results.details.items[0].timing, 400);
assert.equal(results.details.items[9].timing, 4000);
const extrapolatedFrames = new Set(results.details.items.slice(3).map(f => f.data));
return ScreenshotThumbnailsAudit.audit(artifacts, {settings}).then(results => {
assert.equal(results.details.items[0].timing, 158);
assert.equal(results.details.items[9].timing, 1582);

// last 5 frames should be equal to the last real frame
const extrapolatedFrames = new Set(results.details.items.slice(5).map(f => f.data));
assert.ok(results.details.items[9].data.length > 100, 'did not have last frame');
assert.ok(extrapolatedFrames.size === 1, 'did not extrapolate last frame');
});
});

it('should scale the timeline to TTCI', () => {
it('should not scale the timeline to TTCI when simulate', () => {
const settings = {throttlingMethod: 'simulate'};
const artifacts = Object.assign({
traces: {defaultPass: pwaTrace},
}, computedArtifacts);
computedArtifacts.requestConsistentlyInteractive = () => ({timing: 20000});

ttfiReturn = Promise.resolve({rawValue: 8000});
ttciReturn = Promise.resolve({rawValue: 20000});
return ScreenshotThumbnailsAudit.audit(artifacts).then(results => {
assert.equal(results.details.items[0].timing, 2000);
assert.equal(results.details.items[9].timing, 20000);
const extrapolatedFrames = new Set(results.details.items.map(f => f.data));
assert.ok(results.details.items[9].data.length > 100, 'did not have last frame');
assert.ok(extrapolatedFrames.size === 1, 'did not extrapolate last frame');
return ScreenshotThumbnailsAudit.audit(artifacts, {settings}).then(results => {
assert.equal(results.details.items[0].timing, 82);
assert.equal(results.details.items[9].timing, 818);
});
});

it('should handle nonsense times', async () => {
const settings = {throttlingMethod: 'simulate'};
const artifacts = {
traces: {},
requestSpeedline: () => ({frames: [], complete: false, beginning: -1}),
requestConsistentlyInteractive: () => ({timing: NaN}),
};

try {
await ScreenshotThumbnailsAudit.audit(artifacts, {settings});
assert.fail('should have thrown');
} catch (err) {
assert.equal(err.message, 'INVALID_SPEEDLINE');
}
});
});

0 comments on commit 67947e8

Please sign in to comment.