core(screenshots): align filmstrip to observed metrics (#4965)
This commit is contained in:
Родитель
9ea3c17188
Коммит
67947e8a0b
|
@ -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;
|
||||
|
@ -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'],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
const speedline = await artifacts.requestSpeedline(trace);
|
||||
|
||||
for (let i = 1; i <= NUMBER_OF_THUMBNAILS; i++) {
|
||||
const targetTimestamp = speedline.beginning + timelineEnd * i / NUMBER_OF_THUMBNAILS;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let frameForTimestamp = null;
|
||||
if (i === NUMBER_OF_THUMBNAILS) {
|
||||
frameForTimestamp = analyzedFrames[analyzedFrames.length - 1];
|
||||
} else {
|
||||
analyzedFrames.forEach(frame => {
|
||||
if (frame.getTimeStamp() <= targetTimestamp) {
|
||||
frameForTimestamp = frame;
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
const imageData = frameForTimestamp.getParsedImage();
|
||||
const thumbnailImageData = ScreenshotThumbnails.scaleImageToThumbnail(imageData);
|
||||
const base64Data =
|
||||
cachedThumbnails.get(frameForTimestamp) ||
|
||||
jpeg.encode(thumbnailImageData, 90).data.toString('base64');
|
||||
if (!analyzedFrames.length || !Number.isFinite(timelineEnd)) {
|
||||
throw new LHError(LHError.errors.INVALID_SPEEDLINE);
|
||||
}
|
||||
|
||||
cachedThumbnails.set(frameForTimestamp, base64Data);
|
||||
thumbnails.push({
|
||||
timing: Math.round(targetTimestamp - speedline.beginning),
|
||||
timestamp: targetTimestamp * 1000,
|
||||
data: base64Data,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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`);
|
||||
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче