UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

193 lines (172 loc) • 7.22 kB
/** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {makeComputedArtifact} from '../computed-artifact.js'; import {NavigationMetric} from './navigation-metric.js'; import {LanternInteractive} from './lantern-interactive.js'; import {NetworkMonitor} from '../../gather/driver/network-monitor.js'; import {TraceProcessor} from '../../lib/tracehouse/trace-processor.js'; import {LighthouseError} from '../../lib/lh-error.js'; const REQUIRED_QUIET_WINDOW = 5000; const ALLOWED_CONCURRENT_REQUESTS = 2; /** * @fileoverview Computes "Time To Interactive", the time at which the page has loaded critical * resources and is mostly idle. * @see https://docs.google.com/document/d/1yE4YWsusi5wVXrnwhR61j-QyjK9tzENIzfxrCjA1NAk/edit#heading=h.yozfsuqcgpc4 */ class Interactive extends NavigationMetric { /** * Finds all time periods where the number of inflight requests is less than or equal to the * number of allowed concurrent requests (2). * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords * @param {{timestamps: {traceEnd: number}}} processedNavigation * @return {Array<TimePeriod>} */ static _findNetworkQuietPeriods(networkRecords, processedNavigation) { const traceEndTsInMs = processedNavigation.timestamps.traceEnd / 1000; // Ignore records that failed, never finished, or were POST/PUT/etc. const filteredNetworkRecords = networkRecords.filter(record => { return record.finished && record.requestMethod === 'GET' && !record.failed && // Consider network records that had 4xx/5xx status code as "failed" record.statusCode < 400; }); return NetworkMonitor.findNetworkQuietPeriods(filteredNetworkRecords, ALLOWED_CONCURRENT_REQUESTS, traceEndTsInMs); } /** * Finds all time periods where there are no long tasks. * @param {Array<TimePeriod>} longTasks * @param {{timestamps: {timeOrigin: number, traceEnd: number}}} processedNavigation * @return {Array<TimePeriod>} */ static _findCPUQuietPeriods(longTasks, processedNavigation) { const timeOriginTsInMs = processedNavigation.timestamps.timeOrigin / 1000; const traceEndTsInMs = processedNavigation.timestamps.traceEnd / 1000; if (longTasks.length === 0) { return [{start: 0, end: traceEndTsInMs}]; } /** @type {Array<TimePeriod>} */ const quietPeriods = []; longTasks.forEach((task, index) => { if (index === 0) { quietPeriods.push({ start: 0, end: task.start + timeOriginTsInMs, }); } if (index === longTasks.length - 1) { quietPeriods.push({ start: task.end + timeOriginTsInMs, end: traceEndTsInMs, }); } else { quietPeriods.push({ start: task.end + timeOriginTsInMs, end: longTasks[index + 1].start + timeOriginTsInMs, }); } }); return quietPeriods; } /** * Finds the first time period where a network quiet period and a CPU quiet period overlap. * @param {Array<TimePeriod>} longTasks * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords * @param {LH.Artifacts.ProcessedNavigation} processedNavigation * @return {{cpuQuietPeriod: TimePeriod, networkQuietPeriod: TimePeriod, cpuQuietPeriods: Array<TimePeriod>, networkQuietPeriods: Array<TimePeriod>}} */ static findOverlappingQuietPeriods(longTasks, networkRecords, processedNavigation) { const FcpTsInMs = processedNavigation.timestamps.firstContentfulPaint / 1000; /** @type {function(TimePeriod):boolean} */ const isLongEnoughQuietPeriod = period => period.end > FcpTsInMs + REQUIRED_QUIET_WINDOW && period.end - period.start >= REQUIRED_QUIET_WINDOW; const networkQuietPeriods = this._findNetworkQuietPeriods(networkRecords, processedNavigation) .filter(isLongEnoughQuietPeriod); const cpuQuietPeriods = this._findCPUQuietPeriods(longTasks, processedNavigation) .filter(isLongEnoughQuietPeriod); const cpuQueue = cpuQuietPeriods.slice(); const networkQueue = networkQuietPeriods.slice(); // We will check for a CPU quiet period contained within a Network quiet period or vice-versa let cpuCandidate = cpuQueue.shift(); let networkCandidate = networkQueue.shift(); while (cpuCandidate && networkCandidate) { if (cpuCandidate.start >= networkCandidate.start) { // CPU starts later than network, window must be contained by network or we check the next if (networkCandidate.end >= cpuCandidate.start + REQUIRED_QUIET_WINDOW) { return { cpuQuietPeriod: cpuCandidate, networkQuietPeriod: networkCandidate, cpuQuietPeriods, networkQuietPeriods, }; } else { networkCandidate = networkQueue.shift(); } } else { // Network starts later than CPU, window must be contained by CPU or we check the next if (cpuCandidate.end >= networkCandidate.start + REQUIRED_QUIET_WINDOW) { return { cpuQuietPeriod: cpuCandidate, networkQuietPeriod: networkCandidate, cpuQuietPeriods, networkQuietPeriods, }; } else { cpuCandidate = cpuQueue.shift(); } } } throw new LighthouseError( cpuCandidate ? LighthouseError.errors.NO_TTI_NETWORK_IDLE_PERIOD : LighthouseError.errors.NO_TTI_CPU_IDLE_PERIOD ); } /** * @param {LH.Artifacts.NavigationMetricComputationData} data * @param {LH.Artifacts.ComputedContext} context * @return {Promise<LH.Artifacts.LanternMetric>} */ static computeSimulatedMetric(data, context) { const metricData = NavigationMetric.getMetricComputationInput(data); return LanternInteractive.request(metricData, context); } /** * @param {LH.Artifacts.NavigationMetricComputationData} data * @return {Promise<LH.Artifacts.Metric>} */ static computeObservedMetric(data) { const {processedTrace, processedNavigation, networkRecords} = data; if (!processedNavigation.timestamps.domContentLoaded) { throw new LighthouseError(LighthouseError.errors.NO_DCL); } const longTasks = TraceProcessor.getMainThreadTopLevelEvents(processedTrace) .filter(event => event.duration >= 50); const quietPeriodInfo = Interactive.findOverlappingQuietPeriods( longTasks, networkRecords, processedNavigation ); const cpuQuietPeriod = quietPeriodInfo.cpuQuietPeriod; const timestamp = Math.max( cpuQuietPeriod.start, processedNavigation.timestamps.firstContentfulPaint / 1000, processedNavigation.timestamps.domContentLoaded / 1000 ) * 1000; const timing = (timestamp - processedNavigation.timestamps.timeOrigin) / 1000; return Promise.resolve({timing, timestamp}); } } const InteractiveComputed = makeComputedArtifact( Interactive, ['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL', 'SourceMaps', 'HostDPR'] ); export {InteractiveComputed as Interactive}; /** * @typedef TimePeriod * @property {number} start In ms. * @property {number} end In ms. */