fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,15 @@
/**
* Decorate computableArtifact with a caching `request()` method which will
* automatically call `computableArtifact.compute_()` under the hood.
* @template {{name: string, compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>}} C
* @template {Array<keyof LH.Util.FirstParamType<C['compute_']>>} K
* @param {C} computableArtifact
* @param {(K & ([keyof LH.Util.FirstParamType<C['compute_']>] extends [K[number]] ? unknown : never)) | null} keys List of properties of `dependencies` used by `compute_`; other properties are filtered out. Use `null` to allow all properties. Ensures that only required properties are used for caching result.
*/
export function makeComputedArtifact<C extends {
name: string;
compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>;
}, K extends (keyof import("../../types/utility-types.js").default.FirstParamType<C["compute_"]>)[]>(computableArtifact: C, keys: (K & ([keyof import("../../types/utility-types.js").default.FirstParamType<C["compute_"]>] extends [K[number]] ? unknown : never)) | null): C & {
request: (dependencies: import("../../types/utility-types.js").default.FirstParamType<C["compute_"]>, context: LH.Artifacts.ComputedContext) => ReturnType<C["compute_"]>;
};
//# sourceMappingURL=computed-artifact.d.ts.map

View File

@@ -0,0 +1,62 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import log from 'lighthouse-logger';
import {ArbitraryEqualityMap} from '../lib/arbitrary-equality-map.js';
/**
* Decorate computableArtifact with a caching `request()` method which will
* automatically call `computableArtifact.compute_()` under the hood.
* @template {{name: string, compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>}} C
* @template {Array<keyof LH.Util.FirstParamType<C['compute_']>>} K
* @param {C} computableArtifact
* @param {(K & ([keyof LH.Util.FirstParamType<C['compute_']>] extends [K[number]] ? unknown : never)) | null} keys List of properties of `dependencies` used by `compute_`; other properties are filtered out. Use `null` to allow all properties. Ensures that only required properties are used for caching result.
*/
function makeComputedArtifact(computableArtifact, keys) {
// tsc (3.1) has more difficulty with template inter-references in jsdoc, so
// give types to params and return value the long way, essentially recreating
// polymorphic-this behavior for C.
/**
* Return an automatically cached result from the computed artifact.
* @param {LH.Util.FirstParamType<C['compute_']>} dependencies
* @param {LH.Artifacts.ComputedContext} context
* @return {ReturnType<C['compute_']>}
*/
const request = (dependencies, context) => {
const pickedDependencies = keys ?
Object.fromEntries(keys.map(key => [key, dependencies[key]])) :
dependencies;
// NOTE: break immutability solely for this caching-controller function.
const computedCache = /** @type {Map<string, ArbitraryEqualityMap>} */ (context.computedCache);
const computedName = computableArtifact.name;
const cache = computedCache.get(computedName) || new ArbitraryEqualityMap();
computedCache.set(computedName, cache);
/** @type {ReturnType<C['compute_']>|undefined} */
const computed = cache.get(pickedDependencies);
if (computed) {
return computed;
}
const status = {msg: `Computing artifact: ${computedName}`, id: `lh:computed:${computedName}`};
log.time(status, 'verbose');
const artifactPromise = /** @type {ReturnType<C['compute_']>} */
(computableArtifact.compute_(pickedDependencies, context));
cache.set(pickedDependencies, artifactPromise);
artifactPromise.then(() => log.timeEnd(status)).catch(() => log.timeEnd(status));
return artifactPromise;
};
return Object.assign(computableArtifact, {request});
}
export {makeComputedArtifact};

View File

@@ -0,0 +1,40 @@
export { CriticalRequestChainsComputed as CriticalRequestChains };
declare const CriticalRequestChainsComputed: typeof CriticalRequestChains & {
request: (dependencies: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
trace: LH.Trace;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.CriticalRequestNode>;
};
declare class CriticalRequestChains {
/**
* For now, we use network priorities as a proxy for "render-blocking"/critical-ness.
* It's imperfect, but there is not a higher-fidelity signal available yet.
* @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc
* @param {LH.Artifacts.NetworkRequest} request
* @param {LH.Artifacts.NetworkRequest} mainResource
* @return {boolean}
*/
static isCritical(request: LH.Artifacts.NetworkRequest, mainResource: LH.Artifacts.NetworkRequest): boolean;
/**
* Create a tree of critical requests.
* @param {LH.Artifacts.NetworkRequest} mainResource
* @param {LH.Gatherer.Simulation.GraphNode} graph
* @return {LH.Artifacts.CriticalRequestNode}
*/
static extractChainsFromGraph(mainResource: LH.Artifacts.NetworkRequest, graph: LH.Gatherer.Simulation.GraphNode): LH.Artifacts.CriticalRequestNode;
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog, trace: LH.Trace}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.CriticalRequestNode>}
*/
static compute_(data: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
trace: LH.Trace;
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.CriticalRequestNode>;
}
import { NetworkRequest } from '../lib/network-request.js';
//# sourceMappingURL=critical-request-chains.d.ts.map

View File

@@ -0,0 +1,143 @@
/**
* @license Copyright 2016 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRequest} from '../lib/network-request.js';
import {MainResource} from './main-resource.js';
import {PageDependencyGraph} from './page-dependency-graph.js';
class CriticalRequestChains {
/**
* For now, we use network priorities as a proxy for "render-blocking"/critical-ness.
* It's imperfect, but there is not a higher-fidelity signal available yet.
* @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc
* @param {LH.Artifacts.NetworkRequest} request
* @param {LH.Artifacts.NetworkRequest} mainResource
* @return {boolean}
*/
static isCritical(request, mainResource) {
if (!mainResource) {
throw new Error('mainResource not provided');
}
// The main resource is always critical.
if (request.requestId === mainResource.requestId) return true;
// Treat any preloaded resource as non-critical
if (request.isLinkPreload) {
return false;
}
// Whenever a request is a redirect, we don't know if it's critical until we resolve the final
// destination. At that point we can assign all the properties (priority, resourceType) of the
// final request back to the redirect(s) that led to it.
// See https://github.com/GoogleChrome/lighthouse/pull/6704
while (request.redirectDestination) {
request = request.redirectDestination;
}
// Iframes are considered High Priority but they are not render blocking
const isIframe = request.resourceType === NetworkRequest.TYPES.Document &&
request.frameId !== mainResource.frameId;
// XHRs are fetched at High priority, but we exclude them, as they are unlikely to be critical
// Images are also non-critical.
// Treat any missed images, primarily favicons, as non-critical resources
/** @type {Array<LH.Crdp.Network.ResourceType>} */
const nonCriticalResourceTypes = [
NetworkRequest.TYPES.Image,
NetworkRequest.TYPES.XHR,
NetworkRequest.TYPES.Fetch,
NetworkRequest.TYPES.EventSource,
];
if (nonCriticalResourceTypes.includes(request.resourceType || 'Other') ||
isIframe ||
request.mimeType && request.mimeType.startsWith('image/')) {
return false;
}
// Requests that have no initiatorRequest are typically ambiguous late-load assets.
// Even on the off chance they were important, we don't have any parent to display for them.
if (!request.initiatorRequest) return false;
return ['VeryHigh', 'High', 'Medium'].includes(request.priority);
}
/**
* Create a tree of critical requests.
* @param {LH.Artifacts.NetworkRequest} mainResource
* @param {LH.Gatherer.Simulation.GraphNode} graph
* @return {LH.Artifacts.CriticalRequestNode}
*/
static extractChainsFromGraph(mainResource, graph) {
/** @type {LH.Artifacts.CriticalRequestNode} */
const rootNode = {};
/**
* @param {LH.Artifacts.NetworkRequest[]} path
*/
function addChain(path) {
let currentNode = rootNode;
for (const record of path) {
if (!currentNode[record.requestId]) {
currentNode[record.requestId] = {
request: record,
children: {},
};
}
currentNode = currentNode[record.requestId].children;
}
}
// By default `traverse` will discover nodes in BFS-order regardless of dependencies, but
// here we need traversal in a topological sort order. We'll visit a node only when its
// dependencies have been met.
const seenNodes = new Set();
/** @param {LH.Gatherer.Simulation.GraphNode} node */
function getNextNodes(node) {
return node.getDependents().filter(n => n.getDependencies().every(d => seenNodes.has(d)));
}
graph.traverse((node, traversalPath) => {
seenNodes.add(node);
if (node.type !== 'network') return;
if (!CriticalRequestChains.isCritical(node.record, mainResource)) return;
const networkPath = traversalPath
.filter(/** @return {n is LH.Gatherer.Simulation.GraphNetworkNode} */
n => n.type === 'network')
.reverse()
.map(node => node.record);
// Ignore if some ancestor is not a critical request.
if (networkPath.some(r => !CriticalRequestChains.isCritical(r, mainResource))) return;
// Ignore non-network things (like data urls).
if (NetworkRequest.isNonNetworkRequest(node.record)) return;
addChain(networkPath);
}, getNextNodes);
return rootNode;
}
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog, trace: LH.Trace}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.CriticalRequestNode>}
*/
static async compute_(data, context) {
const mainResource = await MainResource.request(data, context);
const graph = await PageDependencyGraph.request(data, context);
return CriticalRequestChains.extractChainsFromGraph(mainResource, graph);
}
}
const CriticalRequestChainsComputed =
makeComputedArtifact(CriticalRequestChains, ['URL', 'devtoolsLog', 'trace']);
export {CriticalRequestChainsComputed as CriticalRequestChains};

View File

@@ -0,0 +1,32 @@
export { DocumentUrlsComputed as DocumentUrls };
declare const DocumentUrlsComputed: typeof DocumentUrls & {
request: (dependencies: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<{
requestedUrl: string;
mainDocumentUrl: string;
}>;
};
/**
* @fileoverview Compute the navigation specific URLs `requestedUrl` and `mainDocumentUrl` in situations where
* the `URL` artifact is not present. This is not a drop-in replacement for `URL` but can be helpful in situations
* where getting the `URL` artifact is difficult.
*/
declare class DocumentUrls {
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{requestedUrl: string, mainDocumentUrl: string}>}
*/
static compute_(data: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: LH.Artifacts.ComputedContext): Promise<{
requestedUrl: string;
mainDocumentUrl: string;
}>;
}
//# sourceMappingURL=document-urls.d.ts.map

53
node_modules/lighthouse/core/computed/document-urls.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
/**
* @license Copyright 2023 The Lighthouse Authors. 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.
*/
import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecords} from './network-records.js';
import {ProcessedTrace} from './processed-trace.js';
/**
* @fileoverview Compute the navigation specific URLs `requestedUrl` and `mainDocumentUrl` in situations where
* the `URL` artifact is not present. This is not a drop-in replacement for `URL` but can be helpful in situations
* where getting the `URL` artifact is difficult.
*/
class DocumentUrls {
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{requestedUrl: string, mainDocumentUrl: string}>}
*/
static async compute_(data, context) {
const processedTrace = await ProcessedTrace.request(data.trace, context);
const networkRecords = await NetworkRecords.request(data.devtoolsLog, context);
const mainFrameId = processedTrace.mainFrameInfo.frameId;
/** @type {string|undefined} */
let requestedUrl;
/** @type {string|undefined} */
let mainDocumentUrl;
for (const event of data.devtoolsLog) {
if (event.method === 'Page.frameNavigated' && event.params.frame.id === mainFrameId) {
const {url} = event.params.frame;
// Only set requestedUrl on the first main frame navigation.
if (!requestedUrl) requestedUrl = url;
mainDocumentUrl = url;
}
}
if (!requestedUrl || !mainDocumentUrl) throw new Error('No main frame navigations found');
const initialRequest = NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
if (initialRequest?.redirects?.length) requestedUrl = initialRequest.redirects[0].url;
return {requestedUrl, mainDocumentUrl};
}
}
const DocumentUrlsComputed = makeComputedArtifact(DocumentUrls, ['devtoolsLog', 'trace']);
export {DocumentUrlsComputed as DocumentUrls};

View File

@@ -0,0 +1,42 @@
export { EntityClassificationComputed as EntityClassification };
export type EntityCache = Map<string, LH.Artifacts.Entity>;
declare const EntityClassificationComputed: typeof EntityClassification & {
request: (dependencies: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.EntityClassification>;
};
/** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
declare class EntityClassification {
/**
* @param {EntityCache} entityCache
* @param {string} url
* @param {string=} extensionName
* @return {LH.Artifacts.Entity}
*/
static makeupChromeExtensionEntity_(entityCache: EntityCache, url: string, extensionName?: string | undefined): LH.Artifacts.Entity;
/**
* @param {EntityCache} entityCache
* @param {string} url
* @return {LH.Artifacts.Entity | undefined}
*/
static _makeUpAnEntity(entityCache: EntityCache, url: string): LH.Artifacts.Entity | undefined;
/**
* Preload Chrome extensions found in the devtoolsLog into cache.
* @param {EntityCache} entityCache
* @param {LH.DevtoolsLog} devtoolsLog
*/
static _preloadChromeExtensionsToCache(entityCache: EntityCache, devtoolsLog: import("../index.js").DevtoolsLog): void;
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.EntityClassification>}
*/
static compute_(data: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.EntityClassification>;
}
//# sourceMappingURL=entity-classification.d.ts.map

View File

@@ -0,0 +1,157 @@
/**
* @license Copyright 2022 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecords} from './network-records.js';
import {Util} from '../../shared/util.js';
import UrlUtils from '../lib/url-utils.js';
import thirdPartyWeb from '../lib/third-party-web.js';
/** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
class EntityClassification {
/**
* @param {EntityCache} entityCache
* @param {string} url
* @param {string=} extensionName
* @return {LH.Artifacts.Entity}
*/
static makeupChromeExtensionEntity_(entityCache, url, extensionName) {
const origin = Util.getChromeExtensionOrigin(url);
const host = new URL(origin).host;
const name = extensionName || host;
const cachedEntity = entityCache.get(origin);
if (cachedEntity) return cachedEntity;
const chromeExtensionEntity = {
name,
company: name,
category: 'Chrome Extension',
homepage: 'https://chromewebstore.google.com/detail/' + host,
categories: [],
domains: [],
averageExecutionTime: 0,
totalExecutionTime: 0,
totalOccurrences: 0,
};
entityCache.set(origin, chromeExtensionEntity);
return chromeExtensionEntity;
}
/**
* @param {EntityCache} entityCache
* @param {string} url
* @return {LH.Artifacts.Entity | undefined}
*/
static _makeUpAnEntity(entityCache, url) {
if (!UrlUtils.isValid(url)) return;
const parsedUrl = Util.createOrReturnURL(url);
if (parsedUrl.protocol === 'chrome-extension:') {
return EntityClassification.makeupChromeExtensionEntity_(entityCache, url);
}
// Make up an entity only for valid http/https URLs.
if (!parsedUrl.protocol.startsWith('http')) return;
const rootDomain = Util.getRootDomain(url);
if (!rootDomain) return;
if (entityCache.has(rootDomain)) return entityCache.get(rootDomain);
const unrecognizedEntity = {
name: rootDomain,
company: rootDomain,
category: '',
categories: [],
domains: [rootDomain],
averageExecutionTime: 0,
totalExecutionTime: 0,
totalOccurrences: 0,
isUnrecognized: true,
};
entityCache.set(rootDomain, unrecognizedEntity);
return unrecognizedEntity;
}
/**
* Preload Chrome extensions found in the devtoolsLog into cache.
* @param {EntityCache} entityCache
* @param {LH.DevtoolsLog} devtoolsLog
*/
static _preloadChromeExtensionsToCache(entityCache, devtoolsLog) {
for (const entry of devtoolsLog.values()) {
if (entry.method !== 'Runtime.executionContextCreated') continue;
const origin = entry.params.context.origin;
if (!origin.startsWith('chrome-extension:')) continue;
if (entityCache.has(origin)) continue;
EntityClassification.makeupChromeExtensionEntity_(entityCache, origin,
entry.params.context.name);
}
}
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.EntityClassification>}
*/
static async compute_(data, context) {
const networkRecords = await NetworkRecords.request(data.devtoolsLog, context);
/** @type {EntityCache} */
const madeUpEntityCache = new Map();
/** @type {Map<string, LH.Artifacts.Entity>} */
const entityByUrl = new Map();
/** @type {Map<LH.Artifacts.Entity, Set<string>>} */
const urlsByEntity = new Map();
EntityClassification._preloadChromeExtensionsToCache(madeUpEntityCache, data.devtoolsLog);
for (const record of networkRecords) {
const {url} = record;
if (entityByUrl.has(url)) continue;
const entity = thirdPartyWeb.getEntity(url) ||
EntityClassification._makeUpAnEntity(madeUpEntityCache, url);
if (!entity) continue;
const entityURLs = urlsByEntity.get(entity) || new Set();
entityURLs.add(url);
urlsByEntity.set(entity, entityURLs);
entityByUrl.set(url, entity);
}
// When available, first party identification will be done via
// `mainDocumentUrl` (for navigations), and falls back to `finalDisplayedUrl` (for timespan/snapshot).
// See https://github.com/GoogleChrome/lighthouse/issues/13706
const firstPartyUrl = data.URL.mainDocumentUrl || data.URL.finalDisplayedUrl;
const firstParty = thirdPartyWeb.getEntity(firstPartyUrl) ||
EntityClassification._makeUpAnEntity(madeUpEntityCache, firstPartyUrl);
/**
* Convenience function to check if a URL belongs to first party.
* @param {string} url
* @return {boolean}
*/
function isFirstParty(url) {
const entityUrl = entityByUrl.get(url);
return entityUrl === firstParty;
}
return {
entityByUrl,
urlsByEntity,
firstParty,
isFirstParty,
};
}
}
const EntityClassificationComputed = makeComputedArtifact(EntityClassification,
['URL', 'devtoolsLog']);
export {EntityClassificationComputed as EntityClassification};

View File

@@ -0,0 +1,24 @@
export { ImageRecordsComputed as ImageRecords };
declare const ImageRecordsComputed: typeof ImageRecords & {
request: (dependencies: {
ImageElements: LH.Artifacts['ImageElements'];
networkRecords: LH.Artifacts.NetworkRequest[];
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.ImageElementRecord[]>;
};
declare class ImageRecords {
/**
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
*/
static indexNetworkRecords(networkRecords: LH.Artifacts.NetworkRequest[]): Record<string, import("../lib/network-request.js").NetworkRequest>;
/**
* @param {{ImageElements: LH.Artifacts['ImageElements'], networkRecords: LH.Artifacts.NetworkRequest[]}} data
* @return {Promise<LH.Artifacts.ImageElementRecord[]>}
*/
static compute_(data: {
ImageElements: LH.Artifacts['ImageElements'];
networkRecords: LH.Artifacts.NetworkRequest[];
}): Promise<LH.Artifacts.ImageElementRecord[]>;
}
//# sourceMappingURL=image-records.d.ts.map

64
node_modules/lighthouse/core/computed/image-records.js generated vendored Normal file
View File

@@ -0,0 +1,64 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
import UrlUtils from '../lib/url-utils.js';
import {makeComputedArtifact} from './computed-artifact.js';
class ImageRecords {
/**
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
*/
static indexNetworkRecords(networkRecords) {
return networkRecords.reduce((map, record) => {
// An image response in newer formats is sometimes incorrectly marked as "application/octet-stream",
// so respect the extension too.
const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
// The network record is only valid for size information if it finished with a successful status
// code that indicates a complete image response.
if (isImage && record.finished && record.statusCode === 200) {
map[record.url] = record;
}
return map;
}, /** @type {Record<string, LH.Artifacts.NetworkRequest>} */ ({}));
}
/**
* @param {{ImageElements: LH.Artifacts['ImageElements'], networkRecords: LH.Artifacts.NetworkRequest[]}} data
* @return {Promise<LH.Artifacts.ImageElementRecord[]>}
*/
static async compute_(data) {
const indexedNetworkRecords = ImageRecords.indexNetworkRecords(data.networkRecords);
/** @type {LH.Artifacts.ImageElementRecord[]} */
const imageRecords = [];
for (const element of data.ImageElements) {
const networkRecord = indexedNetworkRecords[element.src];
const mimeType = networkRecord?.mimeType;
// Don't change the guessed mime type if no mime type was found.
imageRecords.push({
...element,
mimeType: mimeType ? mimeType : UrlUtils.guessMimeType(element.src),
});
}
// Sort (in-place) as largest images descending.
imageRecords.sort((a, b) => {
const aRecord = indexedNetworkRecords[a.src] || {};
const bRecord = indexedNetworkRecords[b.src] || {};
return bRecord.resourceSize - aRecord.resourceSize;
});
return imageRecords;
}
}
const ImageRecordsComputed =
makeComputedArtifact(ImageRecords, ['ImageElements', 'networkRecords']);
export {ImageRecordsComputed as ImageRecords};

13
node_modules/lighthouse/core/computed/js-bundles.d.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
export { JSBundlesComputed as JSBundles };
declare const JSBundlesComputed: typeof JSBundles & {
request: (dependencies: Pick<import("../index.js").Artifacts, "Scripts" | "SourceMaps">, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.Bundle[]>;
};
declare class JSBundles {
/**
* @param {Pick<LH.Artifacts, 'SourceMaps'|'Scripts'>} artifacts
*/
static compute_(artifacts: Pick<LH.Artifacts, 'SourceMaps' | 'Scripts'>): Promise<import("../index.js").Artifacts.Bundle[]>;
}
//# sourceMappingURL=js-bundles.d.ts.map

120
node_modules/lighthouse/core/computed/js-bundles.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
/**
* @license Copyright 2020 The Lighthouse Authors. 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.
*/
import log from 'lighthouse-logger';
import {makeComputedArtifact} from './computed-artifact.js';
import SDK from '../lib/cdt/SDK.js';
/**
* Calculate the number of bytes contributed by each source file
* @param {LH.Artifacts.Bundle['map']} map
* @param {number} contentLength
* @param {string} content
* @return {LH.Artifacts.Bundle['sizes']}
*/
function computeGeneratedFileSizes(map, contentLength, content) {
const lines = content.split('\n');
/** @type {Record<string, number>} */
const files = {};
const totalBytes = contentLength;
let unmappedBytes = totalBytes;
// @ts-expect-error: This function is added in SDK.js. This will eventually be added to CDT.
map.computeLastGeneratedColumns();
for (const mapping of map.mappings()) {
const source = mapping.sourceURL;
const lineNum = mapping.lineNumber;
const colNum = mapping.columnNumber;
const lastColNum = mapping.lastColumnNumber;
// Webpack sometimes emits null mappings.
// https://github.com/mozilla/source-map/pull/303
if (!source) continue;
// Lines and columns are zero-based indices. Visually, lines are shown as a 1-based index.
const line = lines[lineNum];
if (line === null || line === undefined) {
const errorMessage = `${map.url()} mapping for line out of bounds: ${lineNum + 1}`;
log.error('JSBundles', errorMessage);
return {errorMessage};
}
if (colNum > line.length) {
const errorMessage =
`${map.url()} mapping for column out of bounds: ${lineNum + 1}:${colNum}`;
log.error('JSBundles', errorMessage);
return {errorMessage};
}
let mappingLength = 0;
if (lastColNum !== undefined) {
if (lastColNum > line.length) {
// eslint-disable-next-line max-len
const errorMessage =
`${map.url()} mapping for last column out of bounds: ${lineNum + 1}:${lastColNum}`;
log.error('JSBundles', errorMessage);
return {errorMessage};
}
mappingLength = lastColNum - colNum;
} else {
// Add +1 to account for the newline.
mappingLength = line.length - colNum + 1;
}
files[source] = (files[source] || 0) + mappingLength;
unmappedBytes -= mappingLength;
}
return {
files,
unmappedBytes,
totalBytes,
};
}
class JSBundles {
/**
* @param {Pick<LH.Artifacts, 'SourceMaps'|'Scripts'>} artifacts
*/
static async compute_(artifacts) {
const {SourceMaps, Scripts} = artifacts;
/** @type {LH.Artifacts.Bundle[]} */
const bundles = [];
// Collate map and script, compute file sizes.
for (const SourceMap of SourceMaps) {
if (!SourceMap.map) continue;
const {scriptId, map: rawMap} = SourceMap;
if (!rawMap.mappings) continue;
const script = Scripts.find(s => s.scriptId === scriptId);
if (!script) continue;
const compiledUrl = SourceMap.scriptUrl || 'compiled.js';
const mapUrl = SourceMap.sourceMapUrl || 'compiled.js.map';
const map = new SDK.SourceMap(compiledUrl, mapUrl, rawMap);
const sizes = computeGeneratedFileSizes(map, script.length || 0, script.content || '');
const bundle = {
rawMap,
script,
map,
sizes,
};
bundles.push(bundle);
}
return bundles;
}
}
const JSBundlesComputed = makeComputedArtifact(JSBundles, ['Scripts', 'SourceMaps']);
export {JSBundlesComputed as JSBundles};

View File

@@ -0,0 +1,26 @@
export { LCPImageRecordComputed as LCPImageRecord };
declare const LCPImageRecordComputed: typeof LCPImageRecord & {
request: (dependencies: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
};
/**
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
* was a pseudo-element associated with a node containing a smaller background-image.
*/
declare class LCPImageRecord {
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
*/
static compute_(data: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
}
//# sourceMappingURL=lcp-image-record.d.ts.map

View File

@@ -0,0 +1,74 @@
/**
* @license Copyright 2023 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecords} from './network-records.js';
import {ProcessedNavigation} from './processed-navigation.js';
import {LighthouseError} from '../lib/lh-error.js';
/**
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
* was a pseudo-element associated with a node containing a smaller background-image.
*/
class LCPImageRecord {
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
*/
static async compute_(data, context) {
const {trace, devtoolsLog} = data;
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const processedNavigation = await ProcessedNavigation.request(trace, context);
if (processedNavigation.timings.largestContentfulPaint === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
// Use main-frame-only LCP to match the metric value.
const lcpEvent = processedNavigation.largestContentfulPaintEvt;
if (!lcpEvent) return;
const lcpImagePaintEvent = trace.traceEvents.filter(e => {
return e.name === 'LargestImagePaint::Candidate' &&
e.args.frame === lcpEvent.args.frame &&
e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
e.args.data?.size === lcpEvent.args.data?.size;
// Get last candidate, in case there was more than one.
}).sort((a, b) => b.ts - a.ts)[0];
const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
if (!lcpUrl) return;
const candidates = networkRecords.filter(record => {
return record.url === lcpUrl &&
record.finished &&
// Same frame as LCP trace event.
record.frameId === lcpImagePaintEvent.args.frame &&
record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
}).map(record => {
// Follow any redirects to find the real image request.
while (record.redirectDestination) {
record = record.redirectDestination;
}
return record;
}).filter(record => {
// Don't select if also loaded by some other means (xhr, etc). `resourceType`
// isn't set on redirect _sources_, so have to check after following redirects.
return record.resourceType === 'Image';
});
// If there are still multiple candidates, at this point it appears the page
// simply made multiple requests for the image. The first loaded is the best
// guess of the request that made the image available for use.
return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
}
}
const LCPImageRecordComputed = makeComputedArtifact(LCPImageRecord, ['devtoolsLog', 'trace']);
export {LCPImageRecordComputed as LCPImageRecord};

View File

@@ -0,0 +1,28 @@
export { LoadSimulatorComputed as LoadSimulator };
declare const LoadSimulatorComputed: typeof LoadSimulator & {
request: (dependencies: {
devtoolsLog: import("../index.js").DevtoolsLog;
settings: LH.Audit.Context['settings'];
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<Simulator>;
};
declare class LoadSimulator {
/**
* @param {{devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Simulator>}
*/
static compute_(data: {
devtoolsLog: import("../index.js").DevtoolsLog;
settings: LH.Audit.Context['settings'];
}, context: LH.Artifacts.ComputedContext): Promise<Simulator>;
/**
* @param {LH.Artifacts.NetworkAnalysis} networkAnalysis
* @return {LH.PrecomputedLanternData}
*/
static convertAnalysisToSaveableLanternData(networkAnalysis: LH.Artifacts.NetworkAnalysis): LH.PrecomputedLanternData;
}
import { Simulator } from '../lib/dependency-graph/simulator/simulator.js';
import { NetworkAnalysis } from './network-analysis.js';
//# sourceMappingURL=load-simulator.d.ts.map

View File

@@ -0,0 +1,92 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import * as constants from '../config/constants.js';
import {Simulator} from '../lib/dependency-graph/simulator/simulator.js';
import {NetworkAnalysis} from './network-analysis.js';
class LoadSimulator {
/**
* @param {{devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Simulator>}
*/
static async compute_(data, context) {
const {throttlingMethod, throttling, precomputedLanternData} = data.settings;
const networkAnalysis = await NetworkAnalysis.request(data.devtoolsLog, context);
/** @type {LH.Gatherer.Simulation.Options} */
const options = {
additionalRttByOrigin: networkAnalysis.additionalRttByOrigin,
serverResponseTimeByOrigin: networkAnalysis.serverResponseTimeByOrigin,
observedThroughput: networkAnalysis.throughput,
};
// If we have precomputed lantern data, overwrite our observed estimates and use precomputed instead
// for increased stability.
if (precomputedLanternData) {
options.additionalRttByOrigin = new Map(Object.entries(
precomputedLanternData.additionalRttByOrigin));
options.serverResponseTimeByOrigin = new Map(Object.entries(
precomputedLanternData.serverResponseTimeByOrigin));
}
switch (throttlingMethod) {
case 'provided':
options.rtt = networkAnalysis.rtt;
options.throughput = networkAnalysis.throughput;
options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'devtools':
if (throttling) {
options.rtt =
throttling.requestLatencyMs / constants.throttling.DEVTOOLS_RTT_ADJUSTMENT_FACTOR;
options.throughput =
throttling.downloadThroughputKbps * 1024 /
constants.throttling.DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR;
}
options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'simulate':
if (throttling) {
options.rtt = throttling.rttMs;
options.throughput = throttling.throughputKbps * 1024;
options.cpuSlowdownMultiplier = throttling.cpuSlowdownMultiplier;
}
break;
default:
// intentionally fallback to simulator defaults
break;
}
return new Simulator(options);
}
/**
* @param {LH.Artifacts.NetworkAnalysis} networkAnalysis
* @return {LH.PrecomputedLanternData}
*/
static convertAnalysisToSaveableLanternData(networkAnalysis) {
/** @type {LH.PrecomputedLanternData} */
const lanternData = {additionalRttByOrigin: {}, serverResponseTimeByOrigin: {}};
for (const [origin, value] of networkAnalysis.additionalRttByOrigin.entries()) {
if (origin.startsWith('http')) lanternData.additionalRttByOrigin[origin] = value;
}
for (const [origin, value] of networkAnalysis.serverResponseTimeByOrigin.entries()) {
if (origin.startsWith('http')) lanternData.serverResponseTimeByOrigin[origin] = value;
}
return lanternData;
}
}
const LoadSimulatorComputed = makeComputedArtifact(LoadSimulator, ['devtoolsLog', 'settings']);
export {LoadSimulatorComputed as LoadSimulator};

View File

@@ -0,0 +1,25 @@
export { MainResourceComputed as MainResource };
declare const MainResourceComputed: typeof MainResource & {
request: (dependencies: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../lib/network-request.js").NetworkRequest>;
};
/**
* @fileoverview This artifact identifies the main resource on the page. Current solution assumes
* that the main resource is the first non-rediected one.
*/
declare class MainResource {
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkRequest>}
*/
static compute_(data: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest>;
}
//# sourceMappingURL=main-resource.d.ts.map

35
node_modules/lighthouse/core/computed/main-resource.js generated vendored Normal file
View File

@@ -0,0 +1,35 @@
/**
* @license Copyright 2017 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
import {NetworkRecords} from './network-records.js';
/**
* @fileoverview This artifact identifies the main resource on the page. Current solution assumes
* that the main resource is the first non-rediected one.
*/
class MainResource {
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkRequest>}
*/
static async compute_(data, context) {
const {mainDocumentUrl} = data.URL;
if (!mainDocumentUrl) throw new Error('mainDocumentUrl must exist to get the main resource');
const requests = await NetworkRecords.request(data.devtoolsLog, context);
const mainResource = NetworkAnalyzer.findResourceForUrl(requests, mainDocumentUrl);
if (!mainResource) {
throw new Error('Unable to identify the main resource');
}
return mainResource;
}
}
const MainResourceComputed = makeComputedArtifact(MainResource, ['URL', 'devtoolsLog']);
export {MainResourceComputed as MainResource};

View File

@@ -0,0 +1,15 @@
export { MainThreadTasksComputed as MainThreadTasks };
declare const MainThreadTasksComputed: typeof MainThreadTasks & {
request: (dependencies: import("../index.js").Trace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../lib/tracehouse/main-thread-tasks.js").TaskNode[]>;
};
declare class MainThreadTasks {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Array<LH.Artifacts.TaskNode>>}
*/
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<Array<LH.Artifacts.TaskNode>>;
}
//# sourceMappingURL=main-thread-tasks.d.ts.map

View File

@@ -0,0 +1,25 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {MainThreadTasks as MainThreadTasks_} from '../lib/tracehouse/main-thread-tasks.js';
import {ProcessedTrace} from './processed-trace.js';
class MainThreadTasks {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Array<LH.Artifacts.TaskNode>>}
*/
static async compute_(trace, context) {
const {mainThreadEvents, frames, timestamps} = await ProcessedTrace.request(trace, context);
return MainThreadTasks_.getMainThreadTasks(mainThreadEvents, frames, timestamps.traceEnd,
timestamps.timeOrigin);
}
}
const MainThreadTasksComputed = makeComputedArtifact(MainThreadTasks, null);
export {MainThreadTasksComputed as MainThreadTasks};

View File

@@ -0,0 +1,24 @@
export { ManifestValuesComputed as ManifestValues };
declare const ManifestValuesComputed: typeof ManifestValues & {
request: (dependencies: Pick<import("../index.js").Artifacts, "InstallabilityErrors" | "WebAppManifest">, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.ManifestValues>;
};
declare class ManifestValues {
/** @typedef {(val: NonNullable<LH.Artifacts.Manifest['value']>, errors: LH.Artifacts.InstallabilityErrors['errors']) => boolean} Validator */
/**
* @return {Array<{id: LH.Artifacts.ManifestValueCheckID, failureText: string, validate: Validator}>}
*/
static get manifestChecks(): {
id: LH.Artifacts.ManifestValueCheckID;
failureText: string;
validate: (val: NonNullable<LH.Artifacts.Manifest['value']>, errors: LH.Artifacts.InstallabilityErrors['errors']) => boolean;
}[];
/**
* Returns results of all manifest checks
* @param {Pick<LH.Artifacts, 'WebAppManifest'|'InstallabilityErrors'>} Manifest
* @return {Promise<LH.Artifacts.ManifestValues>}
*/
static compute_({ WebAppManifest, InstallabilityErrors }: Pick<LH.Artifacts, 'WebAppManifest' | 'InstallabilityErrors'>): Promise<LH.Artifacts.ManifestValues>;
}
//# sourceMappingURL=manifest-values.d.ts.map

View File

@@ -0,0 +1,136 @@
/**
* @license Copyright 2016 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import * as icons from '../lib/icons.js';
const PWA_DISPLAY_VALUES = ['minimal-ui', 'fullscreen', 'standalone'];
// Historically, Chrome recommended 12 chars as the maximum short_name length to prevent truncation.
// For more discussion, see https://github.com/GoogleChrome/lighthouse/issues/69 and https://developer.chrome.com/apps/manifest/name#short_name
const SUGGESTED_SHORTNAME_LENGTH = 12;
class ManifestValues {
/** @typedef {(val: NonNullable<LH.Artifacts.Manifest['value']>, errors: LH.Artifacts.InstallabilityErrors['errors']) => boolean} Validator */
/**
* @return {Array<{id: LH.Artifacts.ManifestValueCheckID, failureText: string, validate: Validator}>}
*/
static get manifestChecks() {
return [
{
id: 'hasStartUrl',
failureText: 'Manifest does not contain a `start_url`',
validate: manifestValue => !!manifestValue.start_url.value,
},
{
id: 'hasIconsAtLeast144px',
failureText: 'Manifest does not have a PNG icon of at least 144px',
validate: manifestValue => icons.doExist(manifestValue) &&
icons.pngSizedAtLeast(144, manifestValue).length > 0,
},
{
id: 'hasIconsAtLeast512px',
failureText: 'Manifest does not have a PNG icon of at least 512px',
validate: manifestValue => icons.doExist(manifestValue) &&
icons.pngSizedAtLeast(512, manifestValue).length > 0,
},
{
id: 'fetchesIcon',
failureText: 'Manifest icon failed to be fetched',
validate: (manifestValue, errors) => {
const failedToFetchIconErrorIds = [
'cannot-download-icon',
'no-icon-available',
];
return icons.doExist(manifestValue) &&
!errors.some(error => failedToFetchIconErrorIds.includes(error.errorId));
},
},
{
id: 'hasPWADisplayValue',
failureText: 'Manifest\'s `display` value is not one of: ' + PWA_DISPLAY_VALUES.join(' | '),
validate: manifestValue => PWA_DISPLAY_VALUES.includes(manifestValue.display.value),
},
{
id: 'hasBackgroundColor',
failureText: 'Manifest does not have `background_color`',
validate: manifestValue => !!manifestValue.background_color.value,
},
{
id: 'hasThemeColor',
failureText: 'Manifest does not have `theme_color`',
validate: manifestValue => !!manifestValue.theme_color.value,
},
{
id: 'hasShortName',
failureText: 'Manifest does not have `short_name`',
validate: manifestValue => !!manifestValue.short_name.value,
},
{
id: 'shortNameLength',
failureText: `Manifest's \`short_name\` is too long (>${SUGGESTED_SHORTNAME_LENGTH} ` +
`characters) to be displayed on a homescreen without truncation`,
// Pass if there's no short_name. Don't want to report a non-existent string is too long
validate: manifestValue => !!manifestValue.short_name.value &&
manifestValue.short_name.value.length <= SUGGESTED_SHORTNAME_LENGTH,
},
{
id: 'hasName',
failureText: 'Manifest does not have `name`',
validate: manifestValue => !!manifestValue.name.value,
},
{
id: 'hasMaskableIcon',
failureText: 'Manifest does not have at least one icon that is maskable',
validate: ManifestValue => icons.doExist(ManifestValue) &&
icons.containsMaskableIcon(ManifestValue),
},
];
}
/**
* Returns results of all manifest checks
* @param {Pick<LH.Artifacts, 'WebAppManifest'|'InstallabilityErrors'>} Manifest
* @return {Promise<LH.Artifacts.ManifestValues>}
*/
static async compute_({WebAppManifest, InstallabilityErrors}) {
// if the manifest isn't there or is invalid json, we report that and bail
if (WebAppManifest === null) {
return {
isParseFailure: true,
parseFailureReason: 'No manifest was fetched',
allChecks: [],
};
}
const manifestValue = WebAppManifest.value;
if (manifestValue === undefined) {
return {
isParseFailure: true,
parseFailureReason: 'Manifest failed to parse as valid JSON',
allChecks: [],
};
}
// manifest is valid, so do the rest of the checks
const remainingChecks = ManifestValues.manifestChecks.map(item => {
return {
id: item.id,
failureText: item.failureText,
passing: item.validate(manifestValue, InstallabilityErrors.errors),
};
});
return {
isParseFailure: false,
allChecks: remainingChecks,
};
}
}
const ManifestValuesComputed =
makeComputedArtifact(ManifestValues, ['InstallabilityErrors', 'WebAppManifest']);
export {ManifestValuesComputed as ManifestValues};

View File

@@ -0,0 +1,46 @@
export { CumulativeLayoutShiftComputed as CumulativeLayoutShift };
export type LayoutShiftEvent = {
ts: number;
isMainFrame: boolean;
weightedScore: number;
impactedNodes?: LH.Artifacts.TraceImpactedNode[];
};
declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
request: (dependencies: import("../../index.js").Trace, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<{
cumulativeLayoutShift: number;
cumulativeLayoutShiftMainFrame: number;
}>;
};
declare class CumulativeLayoutShift {
/**
* Returns all LayoutShift events that had no recent input.
* Only a `weightedScore` per event is returned. For non-main-frame events, this is
* the only score that matters. For main-frame events, `weighted_score_delta === score`.
* @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {Array<LayoutShiftEvent>}
*/
static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
/**
* Calculates cumulative layout shifts per cluster (session) of LayoutShift
* events -- where a new cluster is created when there's a gap of more than
* 1000ms since the last LayoutShift event or the cluster is greater than
* 5000ms long -- and returns the max LayoutShift score found.
* @param {Array<LayoutShiftEvent>} layoutShiftEvents
* @return {number}
*/
static calculate(layoutShiftEvents: Array<LayoutShiftEvent>): number;
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
*/
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
cumulativeLayoutShift: number;
cumulativeLayoutShiftMainFrame: number;
}>;
}
import { ProcessedTrace } from '../processed-trace.js';
//# sourceMappingURL=cumulative-layout-shift.d.ts.map

View File

@@ -0,0 +1,124 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {ProcessedTrace} from '../processed-trace.js';
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */
const RECENT_INPUT_WINDOW = 500;
class CumulativeLayoutShift {
/**
* Returns all LayoutShift events that had no recent input.
* Only a `weightedScore` per event is returned. For non-main-frame events, this is
* the only score that matters. For main-frame events, `weighted_score_delta === score`.
* @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {Array<LayoutShiftEvent>}
*/
static getLayoutShiftEvents(processedTrace) {
const layoutShiftEvents = [];
// Chromium will set `had_recent_input` if there was recent user input, which
// skips shift events from contributing to CLS. This flag is also set when
// Lighthouse changes the emulation size. This results in the first few shift
// events having `had_recent_input` set, so ignore it for those events.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974.
let mustRespectHadRecentInput = false;
// Even if emulation was applied before navigating, Chrome will issue a viewport
// change event after a navigation starts which is treated as an interaction when
// deciding the `had_recent_input` flag. Anything within 500ms of this event should
// always be counted for CLS regardless of the `had_recent_input` flag.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1302667
let viewportChangeTs = processedTrace.timestamps.timeOrigin;
const firstViewportEvent = processedTrace.frameEvents.find(event => event.name === 'viewport');
if (firstViewportEvent) {
viewportChangeTs = firstViewportEvent.ts;
}
for (const event of processedTrace.frameTreeEvents) {
if (event.name !== 'LayoutShift' ||
!event.args.data ||
event.args.data.is_main_frame === undefined) {
continue;
}
// For all-frames CLS calculation, we rely on `weighted_score_delta`
// All layout shift events should have this since M90: https://crbug.com/1173139
if (event.args.data.weighted_score_delta === undefined) {
throw new Error('CLS missing weighted_score_delta');
}
if (event.args.data.had_recent_input) {
const timing = (event.ts - viewportChangeTs) / 1000;
if (timing > RECENT_INPUT_WINDOW || mustRespectHadRecentInput) continue;
} else {
mustRespectHadRecentInput = true;
}
layoutShiftEvents.push({
ts: event.ts,
isMainFrame: event.args.data.is_main_frame,
weightedScore: event.args.data.weighted_score_delta,
impactedNodes: event.args.data.impacted_nodes,
});
}
return layoutShiftEvents;
}
/**
* Calculates cumulative layout shifts per cluster (session) of LayoutShift
* events -- where a new cluster is created when there's a gap of more than
* 1000ms since the last LayoutShift event or the cluster is greater than
* 5000ms long -- and returns the max LayoutShift score found.
* @param {Array<LayoutShiftEvent>} layoutShiftEvents
* @return {number}
*/
static calculate(layoutShiftEvents) {
const gapMicroseconds = 1_000_000;
const limitMicroseconds = 5_000_000;
let maxScore = 0;
let currentClusterScore = 0;
let firstTs = Number.NEGATIVE_INFINITY;
let prevTs = Number.NEGATIVE_INFINITY;
for (const event of layoutShiftEvents) {
if (event.ts - firstTs > limitMicroseconds || event.ts - prevTs > gapMicroseconds) {
firstTs = event.ts;
currentClusterScore = 0;
}
prevTs = event.ts;
currentClusterScore += event.weightedScore;
maxScore = Math.max(maxScore, currentClusterScore);
}
return maxScore;
}
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
*/
static async compute_(trace, context) {
const processedTrace = await ProcessedTrace.request(trace, context);
const allFrameShiftEvents =
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
return {
cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents),
cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents),
};
}
}
const CumulativeLayoutShiftComputed = makeComputedArtifact(CumulativeLayoutShift, null);
export {CumulativeLayoutShiftComputed as CumulativeLayoutShift};

View File

@@ -0,0 +1,19 @@
export { FirstContentfulPaintAllFramesComputed as FirstContentfulPaintAllFrames };
declare const FirstContentfulPaintAllFramesComputed: typeof FirstContentfulPaintAllFrames & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class FirstContentfulPaintAllFrames extends NavigationMetric {
/**
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=first-contentful-paint-all-frames.d.ts.map

View File

@@ -0,0 +1,37 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
class FirstContentfulPaintAllFrames extends NavigationMetric {
/**
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric() {
// TODO: Add support for all frames in lantern.
throw new Error('FCP All Frames not implemented in lantern');
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {processedNavigation} = data;
return {
timing: processedNavigation.timings.firstContentfulPaintAllFrames,
timestamp: processedNavigation.timestamps.firstContentfulPaintAllFrames,
};
}
}
const FirstContentfulPaintAllFramesComputed = makeComputedArtifact(
FirstContentfulPaintAllFrames,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {FirstContentfulPaintAllFramesComputed as FirstContentfulPaintAllFrames};

View File

@@ -0,0 +1,21 @@
export { FirstContentfulPaintComputed as FirstContentfulPaint };
declare const FirstContentfulPaintComputed: typeof FirstContentfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class FirstContentfulPaint extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=first-contentful-paint.d.ts.map

View File

@@ -0,0 +1,40 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
class FirstContentfulPaint extends NavigationMetric {
/**
* @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 LanternFirstContentfulPaint.request(metricData, context);
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {processedNavigation} = data;
return {
timing: processedNavigation.timings.firstContentfulPaint,
timestamp: processedNavigation.timestamps.firstContentfulPaint,
};
}
}
const FirstContentfulPaintComputed = makeComputedArtifact(
FirstContentfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {FirstContentfulPaintComputed as FirstContentfulPaint};

View File

@@ -0,0 +1,21 @@
export { FirstMeaningfulPaintComputed as FirstMeaningfulPaint };
declare const FirstMeaningfulPaintComputed: typeof FirstMeaningfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class FirstMeaningfulPaint extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=first-meaningful-paint.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {LanternFirstMeaningfulPaint} from './lantern-first-meaningful-paint.js';
class FirstMeaningfulPaint extends NavigationMetric {
/**
* @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 LanternFirstMeaningfulPaint.request(metricData, context);
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {processedNavigation} = data;
if (processedNavigation.timings.firstMeaningfulPaint === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_FMP);
}
return {
timing: processedNavigation.timings.firstMeaningfulPaint,
timestamp: processedNavigation.timestamps.firstMeaningfulPaint,
};
}
}
const FirstMeaningfulPaintComputed = makeComputedArtifact(
FirstMeaningfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {FirstMeaningfulPaintComputed as FirstMeaningfulPaint};

View File

@@ -0,0 +1,67 @@
export { InteractiveComputed as Interactive };
export type TimePeriod = {
start: number;
end: number;
};
declare const InteractiveComputed: typeof Interactive & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
/**
* @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
*/
declare 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: Array<LH.Artifacts.NetworkRequest>, processedNavigation: {
timestamps: {
traceEnd: number;
};
}): Array<TimePeriod>;
/**
* 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: Array<TimePeriod>, processedNavigation: {
timestamps: {
timeOrigin: number;
traceEnd: number;
};
}): Array<TimePeriod>;
/**
* 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: Array<TimePeriod>, networkRecords: Array<LH.Artifacts.NetworkRequest>, processedNavigation: LH.Artifacts.ProcessedNavigation): {
cpuQuietPeriod: TimePeriod;
networkQuietPeriod: TimePeriod;
cpuQuietPeriods: Array<TimePeriod>;
networkQuietPeriods: Array<TimePeriod>;
};
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=interactive.d.ts.map

View File

@@ -0,0 +1,192 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
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']
);
export {InteractiveComputed as Interactive};
/**
* @typedef TimePeriod
* @property {number} start
* @property {number} end
*/

View File

@@ -0,0 +1,48 @@
export { LanternFirstContentfulPaintComputed as LanternFirstContentfulPaint };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
export type CPUNode = import('../../lib/dependency-graph/cpu-node').CPUNode;
export type NetworkNode = import('../../lib/dependency-graph/network-node').NetworkNode;
declare const LanternFirstContentfulPaintComputed: typeof LanternFirstContentfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
/** @typedef {import('../../lib/dependency-graph/cpu-node').CPUNode} CPUNode */
/** @typedef {import('../../lib/dependency-graph/network-node').NetworkNode} NetworkNode */
declare class LanternFirstContentfulPaint extends LanternMetric {
/**
* This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
* *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
* It also computes the set of corresponding CPU node ids that were needed for the paint at the
* given timestamp.
*
* @param {Node} graph
* @param {number} filterTimestamp The timestamp used to filter out tasks that occured after our
* paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
* @param {function(NetworkNode):boolean} blockingScriptFilter The function that determines which scripts
* should be considered *possibly* render-blocking.
* @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
* should also be included in our blocking node IDs set.
* @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, blockingCpuNodeIds: Set<string>}}
*/
static getBlockingNodeData(graph: Node, filterTimestamp: number, blockingScriptFilter: (arg0: NetworkNode) => boolean, extraBlockingCpuNodesToIncludeFilter?: ((arg0: CPUNode) => boolean) | undefined): {
definitelyNotRenderBlockingScriptUrls: Set<string>;
blockingCpuNodeIds: Set<string>;
};
/**
* This function computes the graph required for the first paint of interest.
*
* @param {Node} dependencyGraph
* @param {number} paintTs The timestamp used to filter out tasks that occured after our
* paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
* @param {function(NetworkNode):boolean} blockingResourcesFilter The function that determines which resources
* should be considered *possibly* render-blocking.
* @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
* should also be included in our blocking node IDs set.
* @return {Node}
*/
static getFirstPaintBasedGraph(dependencyGraph: Node, paintTs: number, blockingResourcesFilter: (arg0: NetworkNode) => boolean, extraBlockingCpuNodesToIncludeFilter?: ((arg0: CPUNode) => boolean) | undefined): Node;
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-first-contentful-paint.d.ts.map

View File

@@ -0,0 +1,204 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
/** @typedef {import('../../lib/dependency-graph/cpu-node').CPUNode} CPUNode */
/** @typedef {import('../../lib/dependency-graph/network-node').NetworkNode} NetworkNode */
class LanternFirstContentfulPaint extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
* *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
* It also computes the set of corresponding CPU node ids that were needed for the paint at the
* given timestamp.
*
* @param {Node} graph
* @param {number} filterTimestamp The timestamp used to filter out tasks that occured after our
* paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
* @param {function(NetworkNode):boolean} blockingScriptFilter The function that determines which scripts
* should be considered *possibly* render-blocking.
* @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
* should also be included in our blocking node IDs set.
* @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, blockingCpuNodeIds: Set<string>}}
*/
static getBlockingNodeData(
graph,
filterTimestamp,
blockingScriptFilter,
extraBlockingCpuNodesToIncludeFilter
) {
/** @type {Map<string, CPUNode>} A map of blocking script URLs to the earliest EvaluateScript task node that executed them. */
const scriptUrlToNodeMap = new Map();
/** @type {Array<CPUNode>} */
const cpuNodes = [];
graph.traverse(node => {
if (node.type === BaseNode.TYPES.CPU) {
// A task is *possibly* render blocking if it *started* before filterTimestamp.
// We use startTime here because the paint event can be *inside* the task that was render blocking.
if (node.startTime <= filterTimestamp) cpuNodes.push(node);
// Build our script URL map to find the earliest EvaluateScript task node.
const scriptUrls = node.getEvaluateScriptURLs();
for (const url of scriptUrls) {
// Use the earliest CPU node we find.
const existing = scriptUrlToNodeMap.get(url) || node;
scriptUrlToNodeMap.set(url, node.startTime < existing.startTime ? node : existing);
}
}
});
cpuNodes.sort((a, b) => a.startTime - b.startTime);
// A script is *possibly* render blocking if it finished loading before filterTimestamp.
const possiblyRenderBlockingScriptUrls = LanternMetric.getScriptUrls(graph, node => {
return node.endTime <= filterTimestamp && blockingScriptFilter(node);
});
// A script is *definitely not* render blocking if its EvaluateScript task started after filterTimestamp.
/** @type {Set<string>} */
const definitelyNotRenderBlockingScriptUrls = new Set();
/** @type {Set<string>} */
const blockingCpuNodeIds = new Set();
for (const url of possiblyRenderBlockingScriptUrls) {
// Lookup the CPU node that had the earliest EvaluateScript for this URL.
const cpuNodeForUrl = scriptUrlToNodeMap.get(url);
// If we can't find it at all, we can't conclude anything, so just skip it.
if (!cpuNodeForUrl) continue;
// If we found it and it was in our `cpuNodes` set that means it finished before filterTimestamp, so it really is render-blocking.
if (cpuNodes.includes(cpuNodeForUrl)) {
blockingCpuNodeIds.add(cpuNodeForUrl.id);
continue;
}
// We couldn't find the evaluate script in the set of CPU nodes that ran before our paint, so
// it must not have been necessary for the paint.
definitelyNotRenderBlockingScriptUrls.add(url);
}
// The first layout, first paint, and first ParseHTML are almost always necessary for first paint,
// so we always include those CPU nodes.
const firstLayout = cpuNodes.find(node => node.didPerformLayout());
if (firstLayout) blockingCpuNodeIds.add(firstLayout.id);
const firstPaint = cpuNodes.find(node => node.childEvents.some(e => e.name === 'Paint'));
if (firstPaint) blockingCpuNodeIds.add(firstPaint.id);
const firstParse = cpuNodes.find(node => node.childEvents.some(e => e.name === 'ParseHTML'));
if (firstParse) blockingCpuNodeIds.add(firstParse.id);
// If a CPU filter was passed in, we also want to include those extra nodes.
if (extraBlockingCpuNodesToIncludeFilter) {
cpuNodes
.filter(extraBlockingCpuNodesToIncludeFilter)
.forEach(node => blockingCpuNodeIds.add(node.id));
}
return {
definitelyNotRenderBlockingScriptUrls,
blockingCpuNodeIds,
};
}
/**
* This function computes the graph required for the first paint of interest.
*
* @param {Node} dependencyGraph
* @param {number} paintTs The timestamp used to filter out tasks that occured after our
* paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
* @param {function(NetworkNode):boolean} blockingResourcesFilter The function that determines which resources
* should be considered *possibly* render-blocking.
* @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
* should also be included in our blocking node IDs set.
* @return {Node}
*/
static getFirstPaintBasedGraph(
dependencyGraph,
paintTs,
blockingResourcesFilter,
extraBlockingCpuNodesToIncludeFilter
) {
const {
definitelyNotRenderBlockingScriptUrls,
blockingCpuNodeIds,
} = this.getBlockingNodeData(
dependencyGraph,
paintTs,
blockingResourcesFilter,
extraBlockingCpuNodesToIncludeFilter
);
return dependencyGraph.cloneWithRelationships(node => {
if (node.type === BaseNode.TYPES.NETWORK) {
// Exclude all nodes that ended after paintTs (except for the main document which we always consider necessary)
// endTime is negative if request does not finish, make sure startTime isn't after paintTs in this case.
const endedAfterPaint = node.endTime > paintTs || node.startTime > paintTs;
if (endedAfterPaint && !node.isMainDocument()) return false;
const url = node.record.url;
// If the URL definitely wasn't render-blocking then we filter it out.
if (definitelyNotRenderBlockingScriptUrls.has(url)) {
return false;
}
return blockingResourcesFilter(node);
} else {
// If it's a CPU node, just check if it was blocking.
return blockingCpuNodeIds.has(node.id);
}
});
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) {
return this.getFirstPaintBasedGraph(
dependencyGraph,
processedNavigation.timestamps.firstContentfulPaint,
// In the optimistic graph we exclude resources that appeared to be render blocking but were
// initiated by a script. While they typically have a very high importance and tend to have a
// significant impact on the page's content, these resources don't technically block rendering.
node => node.hasRenderBlockingPriority() && node.initiatorType !== 'script'
);
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) {
return this.getFirstPaintBasedGraph(
dependencyGraph,
processedNavigation.timestamps.firstContentfulPaint,
node => node.hasRenderBlockingPriority()
);
}
}
const LanternFirstContentfulPaintComputed = makeComputedArtifact(
LanternFirstContentfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternFirstContentfulPaintComputed as LanternFirstContentfulPaint};

View File

@@ -0,0 +1,12 @@
export { LanternFirstMeaningfulPaintComputed as LanternFirstMeaningfulPaint };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternFirstMeaningfulPaintComputed: typeof LanternFirstMeaningfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
declare class LanternFirstMeaningfulPaint extends LanternMetric {
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-first-meaningful-paint.d.ts.map

View File

@@ -0,0 +1,83 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
class LanternFirstMeaningfulPaint extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) {
const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
if (!fmp) {
throw new LighthouseError(LighthouseError.errors.NO_FMP);
}
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(
dependencyGraph,
fmp,
// See LanternFirstContentfulPaint's getOptimisticGraph implementation for a longer description
// of why we exclude script initiated resources here.
node => node.hasRenderBlockingPriority() && node.initiatorType !== 'script'
);
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) {
const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
if (!fmp) {
throw new LighthouseError(LighthouseError.errors.NO_FMP);
}
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(
dependencyGraph,
fmp,
node => node.hasRenderBlockingPriority(),
// For pessimistic FMP we'll include *all* layout nodes
node => node.didPerformLayout()
);
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const fcpResult = await LanternFirstContentfulPaint.request(data, context);
const metricResult = await this.computeMetricWithGraphs(data, context);
metricResult.timing = Math.max(metricResult.timing, fcpResult.timing);
return metricResult;
}
}
const LanternFirstMeaningfulPaintComputed = makeComputedArtifact(
LanternFirstMeaningfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternFirstMeaningfulPaintComputed as LanternFirstMeaningfulPaint};

View File

@@ -0,0 +1,31 @@
export { LanternInteractiveComputed as LanternInteractive };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternInteractiveComputed: typeof LanternInteractive & {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
declare class LanternInteractive extends LanternMetric {
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph: Node): Node;
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph: Node): Node;
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @return {number}
*/
static getLastLongTaskEndTime(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], duration?: number): number;
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-interactive.d.ts.map

View File

@@ -0,0 +1,113 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
import {NetworkRequest} from '../../lib/network-request.js';
import {LanternFirstMeaningfulPaint} from './lantern-first-meaningful-paint.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
// Any CPU task of 20 ms or more will end up being a critical long task on mobile
const CRITICAL_LONG_TASK_THRESHOLD = 20;
class LanternInteractive extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph) {
// Adjust the critical long task threshold for microseconds
const minimumCpuTaskDuration = CRITICAL_LONG_TASK_THRESHOLD * 1000;
return dependencyGraph.cloneWithRelationships(node => {
// Include everything that might be a long task
if (node.type === BaseNode.TYPES.CPU) {
return node.event.dur > minimumCpuTaskDuration;
}
// Include all scripts and high priority requests, exclude all images
const isImage = node.record.resourceType === NetworkRequest.TYPES.Image;
const isScript = node.record.resourceType === NetworkRequest.TYPES.Script;
return (
!isImage &&
(isScript ||
node.record.priority === 'High' ||
node.record.priority === 'VeryHigh')
);
});
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @param {import('./lantern-metric.js').Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult, extras) {
if (!extras.fmpResult) throw new Error('missing fmpResult');
const lastTaskAt = LanternInteractive.getLastLongTaskEndTime(simulationResult.nodeTimings);
const minimumTime = extras.optimistic
? extras.fmpResult.optimisticEstimate.timeInMs
: extras.fmpResult.pessimisticEstimate.timeInMs;
return {
timeInMs: Math.max(minimumTime, lastTaskAt),
nodeTimings: simulationResult.nodeTimings,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const fmpResult = await LanternFirstMeaningfulPaint.request(data, context);
const metricResult = await this.computeMetricWithGraphs(data, context, {fmpResult});
metricResult.timing = Math.max(metricResult.timing, fmpResult.timing);
return metricResult;
}
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @return {number}
*/
static getLastLongTaskEndTime(nodeTimings, duration = 50) {
return Array.from(nodeTimings.entries())
.filter(([node, timing]) => {
if (node.type !== BaseNode.TYPES.CPU) return false;
return timing.duration > duration;
})
.map(([_, timing]) => timing.endTime)
.reduce((max, x) => Math.max(max || 0, x || 0), 0);
}
}
const LanternInteractiveComputed = makeComputedArtifact(
LanternInteractive,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternInteractiveComputed as LanternInteractive};

View File

@@ -0,0 +1,25 @@
export { LanternLargestContentfulPaintComputed as LanternLargestContentfulPaint };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternLargestContentfulPaintComputed: typeof LanternLargestContentfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
declare class LanternLargestContentfulPaint extends LanternMetric {
/**
* Low priority image nodes are usually offscreen and very unlikely to be the
* resource that is required for LCP. Our LCP graphs include everything except for these images.
*
* @param {Node} node
* @return {boolean}
*/
static isNotLowPriorityImageNode(node: Node): boolean;
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult: LH.Gatherer.Simulation.Result): LH.Gatherer.Simulation.Result;
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-largest-contentful-paint.d.ts.map

View File

@@ -0,0 +1,111 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
class LanternLargestContentfulPaint extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* Low priority image nodes are usually offscreen and very unlikely to be the
* resource that is required for LCP. Our LCP graphs include everything except for these images.
*
* @param {Node} node
* @return {boolean}
*/
static isNotLowPriorityImageNode(node) {
if (node.type !== 'network') return true;
const isImage = node.record.resourceType === 'Image';
const isLowPriority = node.record.priority === 'Low' || node.record.priority === 'VeryLow';
return !isImage || !isLowPriority;
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) {
const lcp = processedNavigation.timestamps.largestContentfulPaint;
if (!lcp) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(
dependencyGraph,
lcp,
LanternLargestContentfulPaint.isNotLowPriorityImageNode
);
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) {
const lcp = processedNavigation.timestamps.largestContentfulPaint;
if (!lcp) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(
dependencyGraph,
lcp,
_ => true,
// For pessimistic LCP we'll include *all* layout nodes
node => node.didPerformLayout()
);
}
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult) {
const nodeTimesNotOffscreenImages = Array.from(simulationResult.nodeTimings.entries())
.filter(entry => LanternLargestContentfulPaint.isNotLowPriorityImageNode(entry[0]))
.map(entry => entry[1].endTime);
return {
timeInMs: Math.max(...nodeTimesNotOffscreenImages),
nodeTimings: simulationResult.nodeTimings,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const fcpResult = await LanternFirstContentfulPaint.request(data, context);
const metricResult = await this.computeMetricWithGraphs(data, context);
metricResult.timing = Math.max(metricResult.timing, fcpResult.timing);
return metricResult;
}
}
const LanternLargestContentfulPaintComputed = makeComputedArtifact(
LanternLargestContentfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternLargestContentfulPaintComputed as LanternLargestContentfulPaint};

View File

@@ -0,0 +1,30 @@
export { LanternMaxPotentialFIDComputed as LanternMaxPotentialFID };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternMaxPotentialFIDComputed: typeof LanternMaxPotentialFID & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
declare class LanternMaxPotentialFID extends LanternMetric {
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph: Node): Node;
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph: Node): Node;
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} fcpTimeInMs
* @return {Array<{duration: number}>}
*/
static getTimingsAfterFCP(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], fcpTimeInMs: number): Array<{
duration: number;
}>;
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-max-potential-fid.d.ts.map

View File

@@ -0,0 +1,93 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
class LanternMaxPotentialFID extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {LH.Gatherer.Simulation.Result} simulation
* @param {import('./lantern-metric.js').Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulation, extras) {
if (!extras.fcpResult) throw new Error('missing fcpResult');
// Intentionally use the opposite FCP estimate, a more pessimistic FCP means that more tasks
// are excluded from the FID computation, so a higher FCP means lower FID for same work.
const fcpTimeInMs = extras.optimistic
? extras.fcpResult.pessimisticEstimate.timeInMs
: extras.fcpResult.optimisticEstimate.timeInMs;
const timings = LanternMaxPotentialFID.getTimingsAfterFCP(
simulation.nodeTimings,
fcpTimeInMs
);
return {
timeInMs: Math.max(...timings.map(timing => timing.duration), 16),
nodeTimings: simulation.nodeTimings,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const fcpResult = await LanternFirstContentfulPaint.request(data, context);
return super.computeMetricWithGraphs(data, context, {fcpResult});
}
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} fcpTimeInMs
* @return {Array<{duration: number}>}
*/
static getTimingsAfterFCP(nodeTimings, fcpTimeInMs) {
return Array.from(nodeTimings.entries())
.filter(([node, timing]) => node.type === BaseNode.TYPES.CPU && timing.endTime > fcpTimeInMs)
.map(([_, timing]) => timing);
}
}
const LanternMaxPotentialFIDComputed = makeComputedArtifact(
LanternMaxPotentialFID,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternMaxPotentialFIDComputed as LanternMaxPotentialFID};

View File

@@ -0,0 +1,78 @@
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
export type NetworkNode = import('../../lib/dependency-graph/network-node').NetworkNode;
export type Simulator = import('../../lib/dependency-graph/simulator/simulator').Simulator;
export type Extras = {
optimistic: boolean;
fcpResult?: LH.Artifacts.LanternMetric | undefined;
fmpResult?: LH.Artifacts.LanternMetric | undefined;
interactiveResult?: LH.Artifacts.LanternMetric | undefined;
speedline?: {
speedIndex: number;
} | undefined;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
/** @typedef {import('../../lib/dependency-graph/network-node').NetworkNode} NetworkNode */
/** @typedef {import('../../lib/dependency-graph/simulator/simulator').Simulator} Simulator */
/**
* @typedef Extras
* @property {boolean} optimistic
* @property {LH.Artifacts.LanternMetric=} fcpResult
* @property {LH.Artifacts.LanternMetric=} fmpResult
* @property {LH.Artifacts.LanternMetric=} interactiveResult
* @property {{speedIndex: number}=} speedline
*/
export class LanternMetric {
/**
* @param {Node} dependencyGraph
* @param {function(NetworkNode):boolean=} condition
* @return {Set<string>}
*/
static getScriptUrls(dependencyGraph: Node, condition?: ((arg0: NetworkNode) => boolean) | undefined): Set<string>;
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS(): import("../../../types/gatherer.js").default.Simulation.MetricCoefficients;
/**
* Returns the coefficients, scaled by the throttling settings if needed by the metric.
* Some lantern metrics (speed-index) use components in their estimate that are not
* from the simulator. In this case, we need to adjust the coefficients as the target throttling
* settings change.
*
* @param {number} rttMs
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static getScaledCoefficients(rttMs: number): LH.Gatherer.Simulation.MetricCoefficients;
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph: Node, processedNavigation: LH.Artifacts.ProcessedNavigation): Node;
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph: Node, processedNavigation: LH.Artifacts.ProcessedNavigation): Node;
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @param {Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult: LH.Gatherer.Simulation.Result, extras: Extras): LH.Gatherer.Simulation.Result;
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeMetricWithGraphs(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext, extras?: Omit<Extras, 'optimistic'> | undefined): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
}
import { ProcessedNavigation } from '../processed-navigation.js';
//# sourceMappingURL=lantern-metric.d.ts.map

View File

@@ -0,0 +1,162 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
import {NetworkRequest} from '../../lib/network-request.js';
import {ProcessedNavigation} from '../processed-navigation.js';
import {PageDependencyGraph} from '../page-dependency-graph.js';
import {LoadSimulator} from '../load-simulator.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
/** @typedef {import('../../lib/dependency-graph/network-node').NetworkNode} NetworkNode */
/** @typedef {import('../../lib/dependency-graph/simulator/simulator').Simulator} Simulator */
/**
* @typedef Extras
* @property {boolean} optimistic
* @property {LH.Artifacts.LanternMetric=} fcpResult
* @property {LH.Artifacts.LanternMetric=} fmpResult
* @property {LH.Artifacts.LanternMetric=} interactiveResult
* @property {{speedIndex: number}=} speedline
*/
class LanternMetric {
/**
* @param {Node} dependencyGraph
* @param {function(NetworkNode):boolean=} condition
* @return {Set<string>}
*/
static getScriptUrls(dependencyGraph, condition) {
/** @type {Set<string>} */
const scriptUrls = new Set();
dependencyGraph.traverse(node => {
if (node.type === BaseNode.TYPES.CPU) return;
if (node.record.resourceType !== NetworkRequest.TYPES.Script) return;
if (condition && !condition(node)) return;
scriptUrls.add(node.record.url);
});
return scriptUrls;
}
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
throw new Error('COEFFICIENTS unimplemented!');
}
/**
* Returns the coefficients, scaled by the throttling settings if needed by the metric.
* Some lantern metrics (speed-index) use components in their estimate that are not
* from the simulator. In this case, we need to adjust the coefficients as the target throttling
* settings change.
*
* @param {number} rttMs
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static getScaledCoefficients(rttMs) { // eslint-disable-line no-unused-vars
return this.COEFFICIENTS;
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars
throw new Error('Optimistic graph unimplemented!');
}
/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars
throw new Error('Pessmistic graph unimplemented!');
}
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @param {Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult, extras) { // eslint-disable-line no-unused-vars
return simulationResult;
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
// TODO: remove this fallback when lighthouse-pub-ads plugin can update.
const gatherContext = data.gatherContext || {gatherMode: 'navigation'};
if (gatherContext.gatherMode !== 'navigation') {
throw new Error(`Lantern metrics can only be computed on navigations`);
}
const metricName = this.name.replace('Lantern', '');
const graph = await PageDependencyGraph.request(data, context);
const processedNavigation = await ProcessedNavigation.request(data.trace, context);
const simulator = data.simulator || (await LoadSimulator.request(data, context));
const optimisticGraph = this.getOptimisticGraph(graph, processedNavigation);
const pessimisticGraph = this.getPessimisticGraph(graph, processedNavigation);
/** @type {{flexibleOrdering?: boolean, label?: string}} */
let simulateOptions = {label: `optimistic${metricName}`};
const optimisticSimulation = simulator.simulate(optimisticGraph, simulateOptions);
simulateOptions = {label: `optimisticFlex${metricName}`, flexibleOrdering: true};
const optimisticFlexSimulation = simulator.simulate(optimisticGraph, simulateOptions);
simulateOptions = {label: `pessimistic${metricName}`};
const pessimisticSimulation = simulator.simulate(pessimisticGraph, simulateOptions);
const optimisticEstimate = this.getEstimateFromSimulation(
optimisticSimulation.timeInMs < optimisticFlexSimulation.timeInMs ?
optimisticSimulation : optimisticFlexSimulation, {...extras, optimistic: true}
);
const pessimisticEstimate = this.getEstimateFromSimulation(
pessimisticSimulation,
{...extras, optimistic: false}
);
const coefficients = this.getScaledCoefficients(simulator.rtt);
// Estimates under 1s don't really follow the normal curve fit, minimize the impact of the intercept
const interceptMultiplier = coefficients.intercept > 0 ?
Math.min(1, optimisticEstimate.timeInMs / 1000) : 1;
const timing =
coefficients.intercept * interceptMultiplier +
coefficients.optimistic * optimisticEstimate.timeInMs +
coefficients.pessimistic * pessimisticEstimate.timeInMs;
return {
timing,
optimisticEstimate,
pessimisticEstimate,
optimisticGraph,
pessimisticGraph,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
return this.computeMetricWithGraphs(data, context);
}
}
export {LanternMetric};

View File

@@ -0,0 +1,38 @@
export { LanternSpeedIndexComputed as LanternSpeedIndex };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternSpeedIndexComputed: typeof LanternSpeedIndex & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
declare class LanternSpeedIndex extends LanternMetric {
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph: Node): Node;
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph: Node): Node;
/**
* Approximate speed index using layout events from the simulated node timings.
* The layout-based speed index is the weighted average of the endTime of CPU nodes that contained
* a 'Layout' task. log(duration) is used as the weight to stand for "significance" to the page.
*
* If no layout events can be found or the endTime of a CPU task is too early, FCP is used instead.
*
* This approach was determined after evaluating the accuracy/complexity tradeoff of many
* different methods. Read more in the evaluation doc.
*
* @see https://docs.google.com/document/d/1qJWXwxoyVLVadezIp_Tgdk867G3tDNkkVRvUJSH3K1E/edit#
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} fcpTimeInMs
* @return {number}
*/
static computeLayoutBasedSpeedIndex(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], fcpTimeInMs: number): number;
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-speed-index.d.ts.map

View File

@@ -0,0 +1,150 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
import {Speedline} from '../speedline.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
import {throttling as defaultThrottling} from '../../config/constants.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
class LanternSpeedIndex extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
// Negative intercept is OK because estimate is Math.max(FCP, Speed Index) and
// the optimistic estimate is based on the real observed speed index rather than a real
// lantern graph.
intercept: -250,
optimistic: 1.4,
pessimistic: 0.65,
};
}
/**
* @param {number} rttMs
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static getScaledCoefficients(rttMs) { // eslint-disable-line no-unused-vars
// We want to scale our default coefficients based on the speed of the connection.
// We will linearly interpolate coefficients for the passed-in rttMs based on two pre-determined points:
// 1. Baseline point of 30 ms RTT where Speed Index should be a ~50/50 blend of optimistic/pessimistic.
// 30 ms was based on a typical home WiFi connection's actual RTT.
// Coefficients here follow from the fact that the optimistic estimate should be very close
// to reality at this connection speed and the pessimistic estimate compensates for minor
// connection speed differences.
// 2. Default throttled point of 150 ms RTT where the default coefficients have been determined to be most accurate.
// Coefficients here were determined through thorough analysis and linear regression on the
// lantern test data set. See core/scripts/test-lantern.sh for more detail.
// While the coefficients haven't been analyzed at the interpolated points, it's our current best effort.
const defaultCoefficients = this.COEFFICIENTS;
const defaultRttExcess = defaultThrottling.mobileSlow4G.rttMs - 30;
const multiplier = Math.max((rttMs - 30) / defaultRttExcess, 0);
return {
intercept: defaultCoefficients.intercept * multiplier,
optimistic: 0.5 + (defaultCoefficients.optimistic - 0.5) * multiplier,
pessimistic: 0.5 + (defaultCoefficients.pessimistic - 0.5) * multiplier,
};
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @param {import('./lantern-metric.js').Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulationResult, extras) {
if (!extras.fcpResult) throw new Error('missing fcpResult');
if (!extras.speedline) throw new Error('missing speedline');
const fcpTimeInMs = extras.fcpResult.pessimisticEstimate.timeInMs;
const estimate = extras.optimistic
? extras.speedline.speedIndex
: LanternSpeedIndex.computeLayoutBasedSpeedIndex(simulationResult.nodeTimings, fcpTimeInMs);
return {
timeInMs: estimate,
nodeTimings: simulationResult.nodeTimings,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const speedline = await Speedline.request(data.trace, context);
const fcpResult = await LanternFirstContentfulPaint.request(data, context);
const metricResult = await this.computeMetricWithGraphs(data, context, {
speedline,
fcpResult,
});
metricResult.timing = Math.max(metricResult.timing, fcpResult.timing);
return metricResult;
}
/**
* Approximate speed index using layout events from the simulated node timings.
* The layout-based speed index is the weighted average of the endTime of CPU nodes that contained
* a 'Layout' task. log(duration) is used as the weight to stand for "significance" to the page.
*
* If no layout events can be found or the endTime of a CPU task is too early, FCP is used instead.
*
* This approach was determined after evaluating the accuracy/complexity tradeoff of many
* different methods. Read more in the evaluation doc.
*
* @see https://docs.google.com/document/d/1qJWXwxoyVLVadezIp_Tgdk867G3tDNkkVRvUJSH3K1E/edit#
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} fcpTimeInMs
* @return {number}
*/
static computeLayoutBasedSpeedIndex(nodeTimings, fcpTimeInMs) {
/** @type {Array<{time: number, weight: number}>} */
const layoutWeights = [];
for (const [node, timing] of nodeTimings.entries()) {
if (node.type !== BaseNode.TYPES.CPU) continue;
if (node.childEvents.some(x => x.name === 'Layout')) {
const timingWeight = Math.max(Math.log2(timing.endTime - timing.startTime), 0);
layoutWeights.push({time: timing.endTime, weight: timingWeight});
}
}
const totalWeightedTime = layoutWeights
.map(evt => evt.weight * Math.max(evt.time, fcpTimeInMs))
.reduce((a, b) => a + b, 0);
const totalWeight = layoutWeights.map(evt => evt.weight).reduce((a, b) => a + b, 0);
if (!totalWeight) return fcpTimeInMs;
return totalWeightedTime / totalWeight;
}
}
const LanternSpeedIndexComputed = makeComputedArtifact(
LanternSpeedIndex,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternSpeedIndexComputed as LanternSpeedIndex};

View File

@@ -0,0 +1,31 @@
export { LanternTotalBlockingTimeComputed as LanternTotalBlockingTime };
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
declare const LanternTotalBlockingTimeComputed: typeof LanternTotalBlockingTime & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
};
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
declare class LanternTotalBlockingTime extends LanternMetric {
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph: Node): Node;
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph: Node): Node;
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} minDurationMs
*/
static getTopLevelEvents(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], minDurationMs: number): {
start: number;
end: number;
duration: number;
}[];
}
import { LanternMetric } from './lantern-metric.js';
//# sourceMappingURL=lantern-total-blocking-time.d.ts.map

View File

@@ -0,0 +1,126 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/dependency-graph/base-node.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
import {LanternInteractive} from './lantern-interactive.js';
import {BLOCKING_TIME_THRESHOLD, calculateSumOfBlockingTime} from './tbt-utils.js';
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
class LanternTotalBlockingTime extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph) {
return dependencyGraph;
}
/**
* @param {LH.Gatherer.Simulation.Result} simulation
* @param {import('./lantern-metric.js').Extras} extras
* @return {LH.Gatherer.Simulation.Result}
*/
static getEstimateFromSimulation(simulation, extras) {
if (!extras.fcpResult) throw new Error('missing fcpResult');
if (!extras.interactiveResult) throw new Error('missing interactiveResult');
// Intentionally use the opposite FCP estimate. A pessimistic FCP is higher than equal to an
// optimistic FCP, which means potentially more tasks are excluded from the Total Blocking Time
// computation. So a more pessimistic FCP gives a more optimistic Total Blocking Time for the
// same work.
const fcpTimeInMs = extras.optimistic
? extras.fcpResult.pessimisticEstimate.timeInMs
: extras.fcpResult.optimisticEstimate.timeInMs;
// Similarly, we always have pessimistic TTI >= optimistic TTI. Therefore, picking optimistic
// TTI means our window of interest is smaller and thus potentially more tasks are excluded from
// Total Blocking Time computation, yielding a lower (more optimistic) Total Blocking Time value
// for the same work.
const interactiveTimeMs = extras.optimistic
? extras.interactiveResult.optimisticEstimate.timeInMs
: extras.interactiveResult.pessimisticEstimate.timeInMs;
const minDurationMs = BLOCKING_TIME_THRESHOLD;
const events = LanternTotalBlockingTime.getTopLevelEvents(
simulation.nodeTimings,
minDurationMs
);
return {
timeInMs: calculateSumOfBlockingTime(
events,
fcpTimeInMs,
interactiveTimeMs
),
nodeTimings: simulation.nodeTimings,
};
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async compute_(data, context) {
const fcpResult = await LanternFirstContentfulPaint.request(data, context);
const interactiveResult = await LanternInteractive.request(data, context);
return this.computeMetricWithGraphs(data, context, {fcpResult, interactiveResult});
}
/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @param {number} minDurationMs
*/
static getTopLevelEvents(nodeTimings, minDurationMs) {
/** @type {Array<{start: number, end: number, duration: number}>}
*/
const events = [];
for (const [node, timing] of nodeTimings.entries()) {
if (node.type !== BaseNode.TYPES.CPU) continue;
// Filtering out events below minimum duration.
if (timing.duration < minDurationMs) continue;
events.push({
start: timing.startTime,
end: timing.endTime,
duration: timing.duration,
});
}
return events;
}
}
const LanternTotalBlockingTimeComputed = makeComputedArtifact(
LanternTotalBlockingTime,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LanternTotalBlockingTimeComputed as LanternTotalBlockingTime};

View File

@@ -0,0 +1,20 @@
export { LargestContentfulPaintAllFramesComputed as LargestContentfulPaintAllFrames };
declare const LargestContentfulPaintAllFramesComputed: typeof LargestContentfulPaintAllFrames & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class LargestContentfulPaintAllFrames extends NavigationMetric {
/**
* TODO: Simulate LCP all frames in lantern.
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=largest-contentful-paint-all-frames.d.ts.map

View File

@@ -0,0 +1,45 @@
/**
* @license Copyright 2020 The Lighthouse Authors. 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.
*/
/**
* @fileoverview Computed Largest Contentful Paint (LCP) for all frames.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
class LargestContentfulPaintAllFrames extends NavigationMetric {
/**
* TODO: Simulate LCP all frames in lantern.
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeSimulatedMetric() {
throw new Error('LCP All Frames not implemented in lantern');
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {processedNavigation} = data;
if (processedNavigation.timings.largestContentfulPaintAllFrames === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP_ALL_FRAMES);
}
return {
timing: processedNavigation.timings.largestContentfulPaintAllFrames,
timestamp: processedNavigation.timestamps.largestContentfulPaintAllFrames,
};
}
}
const LargestContentfulPaintAllFramesComputed = makeComputedArtifact(
LargestContentfulPaintAllFrames,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LargestContentfulPaintAllFramesComputed as LargestContentfulPaintAllFrames};

View File

@@ -0,0 +1,21 @@
export { LargestContentfulPaintComputed as LargestContentfulPaint };
declare const LargestContentfulPaintComputed: typeof LargestContentfulPaint & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class LargestContentfulPaint extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=largest-contentful-paint.d.ts.map

View File

@@ -0,0 +1,52 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
/**
* @fileoverview Computed Largest Contentful Paint (LCP), the paint time of the largest in-viewport contentful element
* COMPAT: LCP's trace event was first introduced in m78. We can't surface an LCP for older Chrome versions
* @see https://github.com/WICG/largest-contentful-paint
* @see https://wicg.github.io/largest-contentful-paint/
* @see https://web.dev/lcp
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {LanternLargestContentfulPaint} from './lantern-largest-contentful-paint.js';
class LargestContentfulPaint extends NavigationMetric {
/**
* @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 LanternLargestContentfulPaint.request(metricData, context);
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {processedNavigation} = data;
if (processedNavigation.timings.largestContentfulPaint === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
return {
timing: processedNavigation.timings.largestContentfulPaint,
timestamp: processedNavigation.timestamps.largestContentfulPaint,
};
}
}
const LargestContentfulPaintComputed = makeComputedArtifact(
LargestContentfulPaint,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LargestContentfulPaintComputed as LargestContentfulPaint};

View File

@@ -0,0 +1,23 @@
export { LCPBreakdownComputed as LCPBreakdown };
declare const LCPBreakdownComputed: typeof LCPBreakdown & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<{
ttfb: number;
loadStart?: number | undefined;
loadEnd?: number | undefined;
}>;
};
declare class LCPBreakdown {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{ttfb: number, loadStart?: number, loadEnd?: number}>}
*/
static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
ttfb: number;
loadStart?: number | undefined;
loadEnd?: number | undefined;
}>;
}
//# sourceMappingURL=lcp-breakdown.d.ts.map

View File

@@ -0,0 +1,58 @@
/**
* @license Copyright 2023 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {LargestContentfulPaint} from './largest-contentful-paint.js';
import {ProcessedNavigation} from '../processed-navigation.js';
import {TimeToFirstByte} from './time-to-first-byte.js';
import {LCPImageRecord} from '../lcp-image-record.js';
class LCPBreakdown {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{ttfb: number, loadStart?: number, loadEnd?: number}>}
*/
static async compute_(data, context) {
const processedNavigation = await ProcessedNavigation.request(data.trace, context);
const observedLcp = processedNavigation.timings.largestContentfulPaint;
if (observedLcp === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;
const {timing: ttfb} = await TimeToFirstByte.request(data, context);
const lcpRecord = await LCPImageRecord.request(data, context);
if (!lcpRecord) {
return {ttfb};
}
// Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
const throttleRatio = metricLcp / observedLcp;
const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
const loadStart = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
const loadEnd = Math.max(loadStart, Math.min(unclampedLoadEnd, metricLcp));
return {
ttfb,
loadStart,
loadEnd,
};
}
}
const LCPBreakdownComputed = makeComputedArtifact(
LCPBreakdown,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {LCPBreakdownComputed as LCPBreakdown};

View File

@@ -0,0 +1,21 @@
export { MaxPotentialFIDComputed as MaxPotentialFID };
declare const MaxPotentialFIDComputed: typeof MaxPotentialFID & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class MaxPotentialFID extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=max-potential-fid.d.ts.map

View File

@@ -0,0 +1,45 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LanternMaxPotentialFID} from './lantern-max-potential-fid.js';
import {TraceProcessor} from '../../lib/tracehouse/trace-processor.js';
class MaxPotentialFID extends NavigationMetric {
/**
* @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 LanternMaxPotentialFID.request(metricData, context);
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data) {
const {firstContentfulPaint} = data.processedNavigation.timings;
const events = TraceProcessor.getMainThreadTopLevelEvents(
data.processedTrace,
firstContentfulPaint
).filter(evt => evt.duration >= 1);
return Promise.resolve({
timing: Math.max(...events.map(evt => evt.duration), 16),
});
}
}
const MaxPotentialFIDComputed = makeComputedArtifact(
MaxPotentialFID,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {MaxPotentialFIDComputed as MaxPotentialFID};

View File

@@ -0,0 +1,38 @@
export default Metric;
/**
* @fileOverview Encapsulates logic for choosing the correct metric computation method based on the
* specified throttling settings, supporting simulated and observed metric types.
*
* To implement a fully supported metric:
* - Override the computeObservedMetric method with the observed-mode implementation.
* - Override the computeSimulatedMetric method with the simulated-mode implementation (which
* may call another computed artifact with the name LanternMyMetricName).
*/
declare class Metric {
/**
* Narrows the metric computation data to the input so child metric requests can be cached.
*
* @param {LH.Artifacts.MetricComputationData} data
* @return {LH.Artifacts.MetricComputationDataInput}
*/
static getMetricComputationInput(data: LH.Artifacts.MetricComputationData): LH.Artifacts.MetricComputationDataInput;
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.MetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric | LH.Artifacts.Metric>;
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.MetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.Metric>;
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric | LH.Artifacts.Metric>;
}
//# sourceMappingURL=metric.d.ts.map

100
node_modules/lighthouse/core/computed/metrics/metric.js generated vendored Normal file
View File

@@ -0,0 +1,100 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {TraceProcessor} from '../../lib/tracehouse/trace-processor.js';
import {ProcessedTrace} from '../processed-trace.js';
import {ProcessedNavigation} from '../processed-navigation.js';
import {NetworkRecords} from '../network-records.js';
/**
* @fileOverview Encapsulates logic for choosing the correct metric computation method based on the
* specified throttling settings, supporting simulated and observed metric types.
*
* To implement a fully supported metric:
* - Override the computeObservedMetric method with the observed-mode implementation.
* - Override the computeSimulatedMetric method with the simulated-mode implementation (which
* may call another computed artifact with the name LanternMyMetricName).
*/
class Metric {
constructor() {}
/**
* Narrows the metric computation data to the input so child metric requests can be cached.
*
* @param {LH.Artifacts.MetricComputationData} data
* @return {LH.Artifacts.MetricComputationDataInput}
*/
static getMetricComputationInput(data) {
return {
trace: data.trace,
devtoolsLog: data.devtoolsLog,
gatherContext: data.gatherContext,
settings: data.settings,
URL: data.URL,
};
}
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static computeSimulatedMetric(data, context) { // eslint-disable-line no-unused-vars
throw new Error('Unimplemented');
}
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data, context) { // eslint-disable-line no-unused-vars
throw new Error('Unimplemented');
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static async compute_(data, context) {
// TODO: remove this fallback when lighthouse-pub-ads plugin can update.
const gatherContext = data.gatherContext || {gatherMode: 'navigation'};
const {trace, devtoolsLog, settings} = data;
if (!trace || !devtoolsLog || !settings) {
throw new Error('Did not provide necessary metric computation data');
}
const processedTrace = await ProcessedTrace.request(trace, context);
const processedNavigation = gatherContext.gatherMode === 'timespan' ?
undefined : await ProcessedNavigation.request(trace, context);
const augmentedData = Object.assign({
networkRecords: await NetworkRecords.request(devtoolsLog, context),
gatherContext,
processedTrace,
processedNavigation,
}, data);
TraceProcessor.assertHasToplevelEvents(augmentedData.processedTrace.mainThreadEvents);
switch (settings.throttlingMethod) {
case 'simulate':
if (gatherContext.gatherMode !== 'navigation') {
throw new Error(`${gatherContext.gatherMode} does not support throttlingMethod simulate`);
}
return this.computeSimulatedMetric(augmentedData, context);
case 'provided':
case 'devtools':
return this.computeObservedMetric(augmentedData, context);
default:
throw new TypeError(`Unrecognized throttling method: ${settings.throttlingMethod}`);
}
}
}
export default Metric;

View File

@@ -0,0 +1,16 @@
export class NavigationMetric extends Metric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric | LH.Artifacts.Metric>;
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.Metric>;
}
import Metric from './metric.js';
//# sourceMappingURL=navigation-metric.d.ts.map

View File

@@ -0,0 +1,46 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
/**
* @fileOverview Enforces that a metric can only be computed on navigations.
*/
import Metric from './metric.js';
class NavigationMetric extends Metric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static computeSimulatedMetric(data, context) { // eslint-disable-line no-unused-vars
throw new Error('Unimplemented');
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeObservedMetric(data, context) { // eslint-disable-line no-unused-vars
throw new Error('Unimplemented');
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
*/
static async compute_(data, context) {
if (data.gatherContext.gatherMode !== 'navigation') {
throw new Error(`${this.name} can only be computed on navigations`);
}
return super.compute_(data, context);
}
}
export {NavigationMetric};

View File

@@ -0,0 +1,87 @@
export { ResponsivenessComputed as Responsiveness };
export type ResponsivenessEvent = LH.Trace.CompleteEvent & {
name: 'Responsiveness.Renderer.UserInteraction';
args: {
frame: string;
data: {
interactionType: 'drag' | 'keyboard' | 'tapOrClick';
maxDuration: number;
};
};
};
export type EventTimingType = 'keydown' | 'keypress' | 'keyup' | 'mousedown' | 'mouseup' | 'pointerdown' | 'pointerup' | 'click';
export type EventTimingData = {
frame: string;
/**
* The time of user interaction (in ms from navStart).
*/
timeStamp: number;
/**
* The start of interaction handling (in ms from navStart).
*/
processingStart: number;
/**
* The end of interaction handling (in ms from navStart).
*/
processingEnd: number;
/**
* The time from user interaction to browser paint (in ms).
*/
duration: number;
type: EventTimingType;
nodeId: number;
interactionId: number;
};
export type EventTimingEvent = LH.Trace.AsyncEvent & {
name: 'EventTiming';
args: {
data: EventTimingData;
};
};
/**
* A fallback EventTiming placeholder, used if updated EventTiming events are not available.
* TODO: Remove once 103.0.5052.0 is sufficiently released.
*/
export type FallbackTimingEvent = {
name: 'FallbackTiming';
duration: number;
};
declare const ResponsivenessComputed: typeof Responsiveness & {
request: (dependencies: {
trace: LH.Trace;
settings: LH.Audit.Context['settings'];
}, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<EventTimingEvent | FallbackTimingEvent | null>;
};
declare class Responsiveness {
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {ResponsivenessEvent|null}
*/
static getHighPercentileResponsiveness(processedTrace: LH.Artifacts.ProcessedTrace): ResponsivenessEvent | null;
/**
* Finds the interaction event that was probably the responsivenessEvent.maxDuration
* source.
* Note that (presumably due to rounding to ms), the interaction duration may not
* be the same value as `maxDuration`, just the closest value. Function will throw
* if the closest match is off by more than 4ms.
* TODO: this doesn't try to match inputs to interactions and break ties if more than
* one interaction had this duration by returning the first found.
* @param {ResponsivenessEvent} responsivenessEvent
* @param {LH.Trace} trace
* @return {EventTimingEvent|FallbackTimingEvent}
*/
static findInteractionEvent(responsivenessEvent: ResponsivenessEvent, { traceEvents }: LH.Trace): EventTimingEvent | FallbackTimingEvent;
/**
* @param {{trace: LH.Trace, settings: LH.Audit.Context['settings']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<EventTimingEvent|FallbackTimingEvent|null>}
*/
static compute_(data: {
trace: LH.Trace;
settings: LH.Audit.Context['settings'];
}, context: LH.Artifacts.ComputedContext): Promise<EventTimingEvent | FallbackTimingEvent | null>;
}
import { ProcessedTrace } from '../processed-trace.js';
//# sourceMappingURL=responsiveness.d.ts.map

View File

@@ -0,0 +1,160 @@
/**
* @license Copyright 2022 The Lighthouse Authors. 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.
*/
/**
* @fileoverview Returns a high-percentle (usually 98th) measure of how long it
* takes the page to visibly respond to user input (or null, if there was no
* user input in the provided trace).
*/
/** @typedef {LH.Trace.CompleteEvent & {name: 'Responsiveness.Renderer.UserInteraction', args: {frame: string, data: {interactionType: 'drag'|'keyboard'|'tapOrClick', maxDuration: number}}}} ResponsivenessEvent */
/** @typedef {'keydown'|'keypress'|'keyup'|'mousedown'|'mouseup'|'pointerdown'|'pointerup'|'click'} EventTimingType */
/**
* @typedef EventTimingData
* @property {string} frame
* @property {number} timeStamp The time of user interaction (in ms from navStart).
* @property {number} processingStart The start of interaction handling (in ms from navStart).
* @property {number} processingEnd The end of interaction handling (in ms from navStart).
* @property {number} duration The time from user interaction to browser paint (in ms).
* @property {EventTimingType} type
* @property {number} nodeId
* @property {number} interactionId
*/
/** @typedef {LH.Trace.AsyncEvent & {name: 'EventTiming', args: {data: EventTimingData}}} EventTimingEvent */
/**
* A fallback EventTiming placeholder, used if updated EventTiming events are not available.
* TODO: Remove once 103.0.5052.0 is sufficiently released.
* @typedef {{name: 'FallbackTiming', duration: number}} FallbackTimingEvent
*/
import {ProcessedTrace} from '../processed-trace.js';
import {makeComputedArtifact} from '../computed-artifact.js';
const KEYBOARD_EVENTS = new Set(['keydown', 'keypress', 'keyup']);
const CLICK_TAP_DRAG_EVENTS = new Set([
'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']);
/** A map of Responsiveness `interactionType` to matching EventTiming `type`s. */
const interactionTypeToType = {
keyboard: KEYBOARD_EVENTS,
tapOrClick: CLICK_TAP_DRAG_EVENTS,
drag: CLICK_TAP_DRAG_EVENTS,
};
class Responsiveness {
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {ResponsivenessEvent|null}
*/
static getHighPercentileResponsiveness(processedTrace) {
const responsivenessEvents = processedTrace.frameTreeEvents
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=146-150;drc=a1a2302f30b0a58f7669a41c80acdf1fa11958dd
.filter(/** @return {e is ResponsivenessEvent} */ e => {
return e.name === 'Responsiveness.Renderer.UserInteraction';
}).sort((a, b) => b.args.data.maxDuration - a.args.data.maxDuration);
// If there were no interactions with the page, the metric is N/A.
if (responsivenessEvents.length === 0) {
return null;
}
// INP is the "nearest-rank"/inverted_cdf 98th percentile, except Chrome only
// keeps the 10 worst events around, so it can never be more than the 10th from
// last array element. To keep things simpler, sort desc and pick from front.
// See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
const index = Math.min(9, Math.floor(responsivenessEvents.length / 50));
return responsivenessEvents[index];
}
/**
* Finds the interaction event that was probably the responsivenessEvent.maxDuration
* source.
* Note that (presumably due to rounding to ms), the interaction duration may not
* be the same value as `maxDuration`, just the closest value. Function will throw
* if the closest match is off by more than 4ms.
* TODO: this doesn't try to match inputs to interactions and break ties if more than
* one interaction had this duration by returning the first found.
* @param {ResponsivenessEvent} responsivenessEvent
* @param {LH.Trace} trace
* @return {EventTimingEvent|FallbackTimingEvent}
*/
static findInteractionEvent(responsivenessEvent, {traceEvents}) {
const candidates = traceEvents.filter(/** @return {evt is EventTimingEvent} */ evt => {
// Examine only beginning/instant EventTiming events.
return evt.name === 'EventTiming' && evt.ph !== 'e';
});
// If trace is from < m103, the timestamps cannot be trusted, so we craft a fallback
// <m103 traces (bad) had a args.frame
// m103+ traces (good) have a args.data.frame (https://crrev.com/c/3632661)
// TODO(compat): remove FallbackTiming handling when we don't care about <m103
if (candidates.length && candidates.every(candidate => !candidate.args.data?.frame)) {
return {
name: 'FallbackTiming',
duration: responsivenessEvent.args.data.maxDuration,
};
}
const {maxDuration, interactionType} = responsivenessEvent.args.data;
let bestMatchEvent;
let minDurationDiff = Number.POSITIVE_INFINITY;
for (const candidate of candidates) {
// Must be from same frame.
if (candidate.args.data.frame !== responsivenessEvent.args.frame) continue;
// TODO(bckenny): must be in same navigation as well.
const {type, duration} = candidate.args.data;
// Discard if type is incompatible with responsiveness interactionType.
const matchingTypes = interactionTypeToType[interactionType];
if (!matchingTypes) {
throw new Error(`unexpected responsiveness interactionType '${interactionType}'`);
}
if (!matchingTypes.has(type)) continue;
const durationDiff = Math.abs(duration - maxDuration);
if (durationDiff < minDurationDiff) {
bestMatchEvent = candidate;
minDurationDiff = durationDiff;
}
}
if (!bestMatchEvent) {
throw new Error(`no interaction event found for responsiveness type '${interactionType}'`);
}
// TODO: seems to regularly happen up to 3ms and as high as 4. Allow for up to 5ms to be sure.
if (minDurationDiff > 5) {
throw new Error(`no interaction event found within 5ms of responsiveness maxDuration (max: ${maxDuration}, closest ${bestMatchEvent.args.data.duration})`); // eslint-disable-line max-len
}
return bestMatchEvent;
}
/**
* @param {{trace: LH.Trace, settings: LH.Audit.Context['settings']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<EventTimingEvent|FallbackTimingEvent|null>}
*/
static async compute_(data, context) {
const {settings, trace} = data;
if (settings.throttlingMethod === 'simulate') {
throw new Error('Responsiveness currently unsupported by simulated throttling');
}
const processedTrace = await ProcessedTrace.request(trace, context);
const responsivenessEvent = Responsiveness.getHighPercentileResponsiveness(processedTrace);
if (!responsivenessEvent) return null;
const interactionEvent = Responsiveness.findInteractionEvent(responsivenessEvent, trace);
return JSON.parse(JSON.stringify(interactionEvent));
}
}
const ResponsivenessComputed = makeComputedArtifact(Responsiveness, [
'trace',
'settings',
]);
export {ResponsivenessComputed as Responsiveness};

View File

@@ -0,0 +1,16 @@
export { SpeedIndexComputed as SpeedIndex };
declare const SpeedIndexComputed: typeof SpeedIndex & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class SpeedIndex extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=speed-index.d.ts.map

View File

@@ -0,0 +1,40 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {LanternSpeedIndex} from './lantern-speed-index.js';
import {Speedline} from '../speedline.js';
class SpeedIndex extends NavigationMetric {
/**
* @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 LanternSpeedIndex.request(metricData, context);
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data, context) {
const speedline = await Speedline.request(data.trace, context);
const timing = Math.round(speedline.speedIndex);
const timestamp = (timing + speedline.beginning) * 1000;
return Promise.resolve({timing, timestamp});
}
}
const SpeedIndexComputed = makeComputedArtifact(
SpeedIndex,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {SpeedIndexComputed as SpeedIndex};

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
export const BLOCKING_TIME_THRESHOLD: 50;
/**
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {number}
*/
export function calculateSumOfBlockingTime(topLevelEvents: Array<{
start: number;
end: number;
duration: number;
}>, startTimeMs: number, endTimeMs: number): number;
/**
* For TBT, We only want to consider tasks that fall in our time range
* - FCP and TTI for navigation mode
* - Trace start and trace end for timespan mode
*
* FCP is picked as `startTimeMs` because there is little risk of user input happening
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
*
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
*
* @param {{start: number, end: number, duration: number}} event
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
* @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
* @return {number}
*/
export function calculateTbtImpactForEvent(event: {
start: number;
end: number;
duration: number;
}, startTimeMs: number, endTimeMs: number, topLevelEvent?: {
start: number;
end: number;
duration: number;
} | undefined): number;
//# sourceMappingURL=tbt-utils.d.ts.map

View File

@@ -0,0 +1,76 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
const BLOCKING_TIME_THRESHOLD = 50;
/**
* For TBT, We only want to consider tasks that fall in our time range
* - FCP and TTI for navigation mode
* - Trace start and trace end for timespan mode
*
* FCP is picked as `startTimeMs` because there is little risk of user input happening
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
*
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
*
* @param {{start: number, end: number, duration: number}} event
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
* @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
* @return {number}
*/
function calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent) {
let threshold = BLOCKING_TIME_THRESHOLD;
// If a task is not top level, it doesn't make sense to subtract the entire 50ms
// blocking threshold from the event.
//
// e.g. A 80ms top level task with two 40ms children should attribute some blocking
// time to the 40ms tasks even though they do not meet the 50ms threshold.
//
// The solution is to scale the threshold for child events to be considered blocking.
if (topLevelEvent) threshold *= (event.duration / topLevelEvent.duration);
if (event.duration < threshold) return 0;
if (event.end < startTimeMs) return 0;
if (event.start > endTimeMs) return 0;
// Perform the clipping and then calculate Blocking Region. So if we have a 150ms task
// [0, 150] and `startTimeMs` is at 50ms, we first clip the task to [50, 150], and then
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before
// the start time are unimportant, so we care whether the main thread is busy more than
// 50ms at a time only after the start time.
const clippedStart = Math.max(event.start, startTimeMs);
const clippedEnd = Math.min(event.end, endTimeMs);
const clippedDuration = clippedEnd - clippedStart;
if (clippedDuration < threshold) return 0;
return clippedDuration - threshold;
}
/**
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {number}
*/
function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
if (endTimeMs <= startTimeMs) return 0;
let sumBlockingTime = 0;
for (const event of topLevelEvents) {
sumBlockingTime += calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
}
return sumBlockingTime;
}
export {
BLOCKING_TIME_THRESHOLD,
calculateSumOfBlockingTime,
calculateTbtImpactForEvent,
};

View File

@@ -0,0 +1,16 @@
export { TimeToFirstByteComputed as TimeToFirstByte };
declare const TimeToFirstByteComputed: typeof TimeToFirstByte & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
declare class TimeToFirstByte extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.NavigationMetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.Metric>;
}
import { NavigationMetric } from './navigation-metric.js';
//# sourceMappingURL=time-to-first-byte.d.ts.map

View File

@@ -0,0 +1,63 @@
/**
* @license Copyright 2023 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {MainResource} from '../main-resource.js';
import {NetworkAnalysis} from '../network-analysis.js';
class TimeToFirstByte extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeSimulatedMetric(data, context) {
const mainResource = await MainResource.request(data, context);
const networkAnalysis = await NetworkAnalysis.request(data.devtoolsLog, context);
const observedTTFB = (await this.computeObservedMetric(data, context)).timing;
const observedResponseTime =
networkAnalysis.serverResponseTimeByOrigin.get(mainResource.parsedURL.securityOrigin);
if (observedResponseTime === undefined) throw new Error('No response time for origin');
// Estimate when the connection is not warm.
// TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
let roundTrips = 2;
if (!mainResource.protocol.startsWith('h3')) roundTrips += 1; // TCP
if (mainResource.parsedURL.scheme === 'https') roundTrips += 1;
const estimatedTTFB = data.settings.throttling.rttMs * roundTrips + observedResponseTime;
const timing = Math.max(observedTTFB, estimatedTTFB);
return {timing};
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data, context) {
const mainResource = await MainResource.request(data, context);
if (!mainResource.timing) {
throw new Error('missing timing for main resource');
}
const {processedNavigation} = data;
const timeOriginTs = processedNavigation.timestamps.timeOrigin;
const timestampMs =
mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
const timestamp = timestampMs * 1000;
const timing = (timestamp - timeOriginTs) / 1000;
return {timing, timestamp};
}
}
const TimeToFirstByteComputed = makeComputedArtifact(
TimeToFirstByte,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {TimeToFirstByteComputed as TimeToFirstByte};

View File

@@ -0,0 +1,46 @@
export { TimingSummaryComputed as TimingSummary };
declare const TimingSummaryComputed: typeof TimingSummary & {
request: (dependencies: {
trace: LH.Trace;
devtoolsLog: import("../../index.js").DevtoolsLog;
gatherContext: LH.Artifacts['GatherContext'];
settings: LH.Util.ImmutableObject<LH.Config.Settings>;
URL: LH.Artifacts['URL'];
}, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<{
metrics: LH.Artifacts.TimingSummary;
debugInfo: Record<string, boolean>;
}>;
};
declare class TimingSummary {
/**
* @param {LH.Trace} trace
* @param {LH.DevtoolsLog} devtoolsLog
* @param {LH.Artifacts['GatherContext']} gatherContext
* @param {LH.Util.ImmutableObject<LH.Config.Settings>} settings
* @param {LH.Artifacts['URL']} URL
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
*/
static summarize(trace: LH.Trace, devtoolsLog: import("../../index.js").DevtoolsLog, gatherContext: LH.Artifacts['GatherContext'], settings: LH.Util.ImmutableObject<LH.Config.Settings>, URL: LH.Artifacts['URL'], context: LH.Artifacts.ComputedContext): Promise<{
metrics: LH.Artifacts.TimingSummary;
debugInfo: Record<string, boolean>;
}>;
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, gatherContext: LH.Artifacts['GatherContext']; settings: LH.Util.ImmutableObject<LH.Config.Settings>, URL: LH.Artifacts['URL']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
*/
static compute_(data: {
trace: LH.Trace;
devtoolsLog: import("../../index.js").DevtoolsLog;
gatherContext: LH.Artifacts['GatherContext'];
settings: LH.Util.ImmutableObject<LH.Config.Settings>;
URL: LH.Artifacts['URL'];
}, context: LH.Artifacts.ComputedContext): Promise<{
metrics: LH.Artifacts.TimingSummary;
debugInfo: Record<string, boolean>;
}>;
}
//# sourceMappingURL=timing-summary.d.ts.map

View File

@@ -0,0 +1,164 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {ProcessedTrace} from '../processed-trace.js';
import {ProcessedNavigation} from '../processed-navigation.js';
import {Speedline} from '../speedline.js';
import {FirstContentfulPaint} from './first-contentful-paint.js';
import {FirstContentfulPaintAllFrames} from './first-contentful-paint-all-frames.js';
import {FirstMeaningfulPaint} from './first-meaningful-paint.js';
import {LargestContentfulPaint} from './largest-contentful-paint.js';
import {LargestContentfulPaintAllFrames} from './largest-contentful-paint-all-frames.js';
import {Interactive} from './interactive.js';
import {CumulativeLayoutShift} from './cumulative-layout-shift.js';
import {SpeedIndex} from './speed-index.js';
import {MaxPotentialFID} from './max-potential-fid.js';
import {TotalBlockingTime} from './total-blocking-time.js';
import {makeComputedArtifact} from '../computed-artifact.js';
import {TimeToFirstByte} from './time-to-first-byte.js';
import {LCPBreakdown} from './lcp-breakdown.js';
class TimingSummary {
/**
* @param {LH.Trace} trace
* @param {LH.DevtoolsLog} devtoolsLog
* @param {LH.Artifacts['GatherContext']} gatherContext
* @param {LH.Util.ImmutableObject<LH.Config.Settings>} settings
* @param {LH.Artifacts['URL']} URL
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
*/
static async summarize(trace, devtoolsLog, gatherContext, settings, URL, context) {
const metricComputationData = {trace, devtoolsLog, gatherContext, settings, URL};
/**
* @template TArtifacts
* @template TReturn
* @param {{request: (artifact: TArtifacts, context: LH.Artifacts.ComputedContext) => Promise<TReturn>}} Artifact
* @param {TArtifacts} artifact
* @return {Promise<TReturn|undefined>}
*/
const requestOrUndefined = (Artifact, artifact) => {
return Artifact.request(artifact, context).catch(_ => undefined);
};
/* eslint-disable max-len */
const processedTrace = await ProcessedTrace.request(trace, context);
const processedNavigation = await requestOrUndefined(ProcessedNavigation, trace);
const speedline = await Speedline.request(trace, context);
const firstContentfulPaint = await requestOrUndefined(FirstContentfulPaint, metricComputationData);
const firstContentfulPaintAllFrames = await requestOrUndefined(FirstContentfulPaintAllFrames, metricComputationData);
const firstMeaningfulPaint = await requestOrUndefined(FirstMeaningfulPaint, metricComputationData);
const largestContentfulPaint = await requestOrUndefined(LargestContentfulPaint, metricComputationData);
const largestContentfulPaintAllFrames = await requestOrUndefined(LargestContentfulPaintAllFrames, metricComputationData);
const interactive = await requestOrUndefined(Interactive, metricComputationData);
const cumulativeLayoutShiftValues = await requestOrUndefined(CumulativeLayoutShift, trace);
const maxPotentialFID = await requestOrUndefined(MaxPotentialFID, metricComputationData);
const speedIndex = await requestOrUndefined(SpeedIndex, metricComputationData);
const totalBlockingTime = await requestOrUndefined(TotalBlockingTime, metricComputationData);
const lcpBreakdown = await requestOrUndefined(LCPBreakdown, metricComputationData);
const ttfb = await requestOrUndefined(TimeToFirstByte, metricComputationData);
const {
cumulativeLayoutShift,
cumulativeLayoutShiftMainFrame,
} = cumulativeLayoutShiftValues || {};
/** @type {LH.Artifacts.TimingSummary} */
const metrics = {
// Include the simulated/observed performance metrics
firstContentfulPaint: firstContentfulPaint?.timing,
firstContentfulPaintTs: firstContentfulPaint?.timestamp,
firstContentfulPaintAllFrames: firstContentfulPaintAllFrames?.timing,
firstContentfulPaintAllFramesTs: firstContentfulPaintAllFrames?.timestamp,
firstMeaningfulPaint: firstMeaningfulPaint?.timing,
firstMeaningfulPaintTs: firstMeaningfulPaint?.timestamp,
largestContentfulPaint: largestContentfulPaint?.timing,
largestContentfulPaintTs: largestContentfulPaint?.timestamp,
largestContentfulPaintAllFrames: largestContentfulPaintAllFrames?.timing,
largestContentfulPaintAllFramesTs: largestContentfulPaintAllFrames?.timestamp,
interactive: interactive?.timing,
interactiveTs: interactive?.timestamp,
speedIndex: speedIndex?.timing,
speedIndexTs: speedIndex?.timestamp,
totalBlockingTime: totalBlockingTime?.timing,
maxPotentialFID: maxPotentialFID?.timing,
cumulativeLayoutShift,
cumulativeLayoutShiftMainFrame,
lcpLoadStart: lcpBreakdown?.loadStart,
lcpLoadEnd: lcpBreakdown?.loadEnd,
timeToFirstByte: ttfb?.timing,
timeToFirstByteTs: ttfb?.timestamp,
// Include all timestamps of interest from the processed trace
observedTimeOrigin: processedTrace.timings.timeOrigin,
observedTimeOriginTs: processedTrace.timestamps.timeOrigin,
// For now, navigationStart is always timeOrigin.
observedNavigationStart: processedNavigation?.timings.timeOrigin,
observedNavigationStartTs: processedNavigation?.timestamps.timeOrigin,
observedFirstPaint: processedNavigation?.timings.firstPaint,
observedFirstPaintTs: processedNavigation?.timestamps.firstPaint,
observedFirstContentfulPaint: processedNavigation?.timings.firstContentfulPaint,
observedFirstContentfulPaintTs: processedNavigation?.timestamps.firstContentfulPaint,
observedFirstContentfulPaintAllFrames: processedNavigation?.timings.firstContentfulPaintAllFrames,
observedFirstContentfulPaintAllFramesTs: processedNavigation?.timestamps.firstContentfulPaintAllFrames,
observedFirstMeaningfulPaint: processedNavigation?.timings.firstMeaningfulPaint,
observedFirstMeaningfulPaintTs: processedNavigation?.timestamps.firstMeaningfulPaint,
observedLargestContentfulPaint: processedNavigation?.timings.largestContentfulPaint,
observedLargestContentfulPaintTs: processedNavigation?.timestamps.largestContentfulPaint,
observedLargestContentfulPaintAllFrames: processedNavigation?.timings.largestContentfulPaintAllFrames,
observedLargestContentfulPaintAllFramesTs: processedNavigation?.timestamps.largestContentfulPaintAllFrames,
observedTraceEnd: processedTrace.timings.traceEnd,
observedTraceEndTs: processedTrace.timestamps.traceEnd,
observedLoad: processedNavigation?.timings.load,
observedLoadTs: processedNavigation?.timestamps.load,
observedDomContentLoaded: processedNavigation?.timings.domContentLoaded,
observedDomContentLoadedTs: processedNavigation?.timestamps.domContentLoaded,
observedCumulativeLayoutShift: cumulativeLayoutShift,
observedCumulativeLayoutShiftMainFrame: cumulativeLayoutShiftMainFrame,
// Include some visual metrics from speedline
observedFirstVisualChange: speedline.first,
observedFirstVisualChangeTs: (speedline.first + speedline.beginning) * 1000,
observedLastVisualChange: speedline.complete,
observedLastVisualChangeTs: (speedline.complete + speedline.beginning) * 1000,
observedSpeedIndex: speedline.speedIndex,
observedSpeedIndexTs: (speedline.speedIndex + speedline.beginning) * 1000,
};
/* eslint-enable max-len */
/** @type {Record<string,boolean>} */
const debugInfo = {
lcpInvalidated: !!processedNavigation?.lcpInvalidated,
};
return {metrics, debugInfo};
}
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, gatherContext: LH.Artifacts['GatherContext']; settings: LH.Util.ImmutableObject<LH.Config.Settings>, URL: LH.Artifacts['URL']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
*/
static async compute_(data, context) {
return TimingSummary.summarize(
data.trace,
data.devtoolsLog,
data.gatherContext,
data.settings,
data.URL,
context
);
}
}
const TimingSummaryComputed = makeComputedArtifact(
TimingSummary,
['devtoolsLog', 'gatherContext', 'settings', 'trace', 'URL']
);
export {TimingSummaryComputed as TimingSummary};

View File

@@ -0,0 +1,29 @@
export { TotalBlockingTimeComputed as TotalBlockingTime };
declare const TotalBlockingTimeComputed: typeof TotalBlockingTime & {
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../index.js").Artifacts.Metric | import("../../index.js").Artifacts.LanternMetric>;
};
/**
* @fileoverview This audit determines Total Blocking Time.
* We define Blocking Time as any time interval in the loading timeline where task length exceeds
* 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
* Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
* Interactive Time (TTI).
*
* This is a new metric designed to accompany Time to Interactive. TTI is strict and does not
* reflect incremental improvements to the site performance unless the improvement concerns the last
* long task. Total Blocking Time on the other hand is designed to be much more responsive
* to smaller improvements to main thread responsiveness.
*/
declare class TotalBlockingTime extends ComputedMetric {
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data: LH.Artifacts.MetricComputationData, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
}
import ComputedMetric from './metric.js';
//# sourceMappingURL=total-blocking-time.d.ts.map

View File

@@ -0,0 +1,74 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from '../computed-artifact.js';
import ComputedMetric from './metric.js';
import {TraceProcessor} from '../../lib/tracehouse/trace-processor.js';
import {LanternTotalBlockingTime} from './lantern-total-blocking-time.js';
import {Interactive} from './interactive.js';
import {calculateSumOfBlockingTime} from './tbt-utils.js';
/**
* @fileoverview This audit determines Total Blocking Time.
* We define Blocking Time as any time interval in the loading timeline where task length exceeds
* 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
* Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
* Interactive Time (TTI).
*
* This is a new metric designed to accompany Time to Interactive. TTI is strict and does not
* reflect incremental improvements to the site performance unless the improvement concerns the last
* long task. Total Blocking Time on the other hand is designed to be much more responsive
* to smaller improvements to main thread responsiveness.
*/
class TotalBlockingTime extends ComputedMetric {
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data, context) {
const metricData = ComputedMetric.getMetricComputationInput(data);
return LanternTotalBlockingTime.request(metricData, context);
}
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data, context) {
const events = TraceProcessor.getMainThreadTopLevelEvents(data.processedTrace);
if (data.processedNavigation) {
const {firstContentfulPaint} = data.processedNavigation.timings;
const metricData = ComputedMetric.getMetricComputationInput(data);
const interactiveTimeMs = (await Interactive.request(metricData, context)).timing;
return {
timing: calculateSumOfBlockingTime(
events,
firstContentfulPaint,
interactiveTimeMs
),
};
} else {
return {
timing: calculateSumOfBlockingTime(
events,
0,
data.processedTrace.timings.traceEnd
),
};
}
}
}
const TotalBlockingTimeComputed = makeComputedArtifact(
TotalBlockingTime,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL']
);
export {TotalBlockingTimeComputed as TotalBlockingTime};

View File

@@ -0,0 +1,37 @@
export { ModuleDuplicationComputed as ModuleDuplication };
declare const ModuleDuplicationComputed: typeof ModuleDuplication & {
request: (dependencies: Pick<import("../index.js").Artifacts, "Scripts" | "SourceMaps">, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<Map<string, {
scriptId: string;
scriptUrl: string;
resourceSize: number;
}[]>>;
};
declare class ModuleDuplication {
/**
* @param {string} source
*/
static normalizeSource(source: string): string;
/**
* @param {string} source
*/
static _shouldIgnoreSource(source: string): boolean;
/**
* @param {Map<string, Array<{scriptId: string, resourceSize: number}>>} moduleNameToSourceData
*/
static _normalizeAggregatedData(moduleNameToSourceData: Map<string, Array<{
scriptId: string;
resourceSize: number;
}>>): void;
/**
* @param {Pick<LH.Artifacts, 'Scripts'|'SourceMaps'>} artifacts
* @param {LH.Artifacts.ComputedContext} context
*/
static compute_(artifacts: Pick<LH.Artifacts, 'Scripts' | 'SourceMaps'>, context: LH.Artifacts.ComputedContext): Promise<Map<string, {
scriptId: string;
scriptUrl: string;
resourceSize: number;
}[]>>;
}
//# sourceMappingURL=module-duplication.d.ts.map

View File

@@ -0,0 +1,138 @@
/**
* @license Copyright 2020 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {JSBundles} from './js-bundles.js';
const RELATIVE_SIZE_THRESHOLD = 0.1;
const ABSOLUTE_SIZE_THRESHOLD_BYTES = 1024 * 0.5;
class ModuleDuplication {
/**
* @param {string} source
*/
static normalizeSource(source) {
// Trim trailing question mark - b/c webpack.
source = source.replace(/\?$/, '');
// Normalize paths for dependencies by only keeping everything after the last `node_modules`.
const lastNodeModulesIndex = source.lastIndexOf('node_modules');
if (lastNodeModulesIndex !== -1) {
source = source.substring(lastNodeModulesIndex);
}
return source;
}
/**
* @param {string} source
*/
static _shouldIgnoreSource(source) {
// Ignore bundle overhead.
if (source.includes('webpack/bootstrap')) return true;
if (source.includes('(webpack)/buildin')) return true;
// Ignore webpack module shims, i.e. aliases of the form `module.exports = window.jQuery`
if (source.includes('external ')) return true;
return false;
}
/**
* @param {Map<string, Array<{scriptId: string, resourceSize: number}>>} moduleNameToSourceData
*/
static _normalizeAggregatedData(moduleNameToSourceData) {
for (const [key, originalSourceData] of moduleNameToSourceData.entries()) {
let sourceData = originalSourceData;
// Sort by resource size.
sourceData.sort((a, b) => b.resourceSize - a.resourceSize);
// Remove modules smaller than a % size of largest.
if (sourceData.length > 1) {
const largestResourceSize = sourceData[0].resourceSize;
sourceData = sourceData.filter(data => {
const percentSize = data.resourceSize / largestResourceSize;
return percentSize >= RELATIVE_SIZE_THRESHOLD;
});
}
// Remove modules smaller than an absolute theshold.
sourceData = sourceData.filter(data => data.resourceSize >= ABSOLUTE_SIZE_THRESHOLD_BYTES);
// Delete source datas with only one value (no duplicates).
if (sourceData.length > 1) {
moduleNameToSourceData.set(key, sourceData);
} else {
moduleNameToSourceData.delete(key);
}
}
}
/**
* @param {Pick<LH.Artifacts, 'Scripts'|'SourceMaps'>} artifacts
* @param {LH.Artifacts.ComputedContext} context
*/
static async compute_(artifacts, context) {
const bundles = await JSBundles.request(artifacts, context);
/**
* @typedef SourceData
* @property {string} source
* @property {number} resourceSize
*/
/** @type {Map<LH.Artifacts.RawSourceMap, SourceData[]>} */
const sourceDatasMap = new Map();
// Determine size of each `sources` entry.
for (const {rawMap, sizes} of bundles) {
if ('errorMessage' in sizes) continue;
/** @type {SourceData[]} */
const sourceDataArray = [];
sourceDatasMap.set(rawMap, sourceDataArray);
for (let i = 0; i < rawMap.sources.length; i++) {
if (this._shouldIgnoreSource(rawMap.sources[i])) continue;
const sourceKey = (rawMap.sourceRoot || '') + rawMap.sources[i];
const sourceSize = sizes.files[sourceKey];
sourceDataArray.push({
source: ModuleDuplication.normalizeSource(rawMap.sources[i]),
resourceSize: sourceSize,
});
}
}
/** @type {Map<string, Array<{scriptId: string, scriptUrl: string, resourceSize: number}>>} */
const moduleNameToSourceData = new Map();
for (const {rawMap, script} of bundles) {
const sourceDataArray = sourceDatasMap.get(rawMap);
if (!sourceDataArray) continue;
for (const sourceData of sourceDataArray) {
let data = moduleNameToSourceData.get(sourceData.source);
if (!data) {
data = [];
moduleNameToSourceData.set(sourceData.source, data);
}
data.push({
scriptId: script.scriptId,
scriptUrl: script.url,
resourceSize: sourceData.resourceSize,
});
}
}
this._normalizeAggregatedData(moduleNameToSourceData);
return moduleNameToSourceData;
}
}
const ModuleDuplicationComputed =
makeComputedArtifact(ModuleDuplication, ['Scripts', 'SourceMaps']);
export {ModuleDuplicationComputed as ModuleDuplication};

View File

@@ -0,0 +1,20 @@
export { NetworkAnalysisComputed as NetworkAnalysis };
declare const NetworkAnalysisComputed: typeof NetworkAnalysis & {
request: (dependencies: import("../index.js").DevtoolsLog, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.NetworkAnalysis>;
};
declare class NetworkAnalysis {
/**
* @param {Array<LH.Artifacts.NetworkRequest>} records
* @return {LH.Util.StrictOmit<LH.Artifacts.NetworkAnalysis, 'throughput'>}
*/
static computeRTTAndServerResponseTime(records: Array<LH.Artifacts.NetworkRequest>): LH.Util.StrictOmit<LH.Artifacts.NetworkAnalysis, 'throughput'>;
/**
* @param {LH.DevtoolsLog} devtoolsLog
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkAnalysis>}
*/
static compute_(devtoolsLog: import("../index.js").DevtoolsLog, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkAnalysis>;
}
//# sourceMappingURL=network-analysis.d.ts.map

View File

@@ -0,0 +1,64 @@
/**
* @license Copyright 2017 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
import {NetworkRecords} from './network-records.js';
class NetworkAnalysis {
/**
* @param {Array<LH.Artifacts.NetworkRequest>} records
* @return {LH.Util.StrictOmit<LH.Artifacts.NetworkAnalysis, 'throughput'>}
*/
static computeRTTAndServerResponseTime(records) {
// First pass compute the estimated observed RTT to each origin's servers.
/** @type {Map<string, number>} */
const rttByOrigin = new Map();
for (const [origin, summary] of NetworkAnalyzer.estimateRTTByOrigin(records).entries()) {
rttByOrigin.set(origin, summary.min);
}
// We'll use the minimum RTT as the assumed connection latency since we care about how much addt'l
// latency each origin introduces as Lantern will be simulating with its own connection latency.
const minimumRtt = Math.min(...Array.from(rttByOrigin.values()));
// We'll use the observed RTT information to help estimate the server response time
const responseTimeSummaries = NetworkAnalyzer.estimateServerResponseTimeByOrigin(records, {
rttByOrigin,
});
/** @type {Map<string, number>} */
const additionalRttByOrigin = new Map();
/** @type {Map<string, number>} */
const serverResponseTimeByOrigin = new Map();
for (const [origin, summary] of responseTimeSummaries.entries()) {
// Not all origins have usable timing data, we'll default to using no additional latency.
const rttForOrigin = rttByOrigin.get(origin) || minimumRtt;
additionalRttByOrigin.set(origin, rttForOrigin - minimumRtt);
serverResponseTimeByOrigin.set(origin, summary.median);
}
return {
rtt: minimumRtt,
additionalRttByOrigin,
serverResponseTimeByOrigin,
};
}
/**
* @param {LH.DevtoolsLog} devtoolsLog
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkAnalysis>}
*/
static async compute_(devtoolsLog, context) {
const records = await NetworkRecords.request(devtoolsLog, context);
const throughput = NetworkAnalyzer.estimateThroughput(records);
const rttAndServerResponseTime = NetworkAnalysis.computeRTTAndServerResponseTime(records);
return {throughput, ...rttAndServerResponseTime};
}
}
const NetworkAnalysisComputed = makeComputedArtifact(NetworkAnalysis, null);
export {NetworkAnalysisComputed as NetworkAnalysis};

View File

@@ -0,0 +1,15 @@
export { NetworkRecordsComputed as NetworkRecords };
declare const NetworkRecordsComputed: typeof NetworkRecords & {
request: (dependencies: LH.DevtoolsLog, context: LH.Util.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../lib/network-request.js").NetworkRequest[]>;
};
declare class NetworkRecords {
/**
* @param {LH.DevtoolsLog} devtoolsLog
* @return {Promise<Array<LH.Artifacts.NetworkRequest>>} networkRecords
*/
static compute_(devtoolsLog: LH.DevtoolsLog): Promise<Array<LH.Artifacts.NetworkRequest>>;
}
import * as LH from '../../types/lh.js';
//# sourceMappingURL=network-records.d.ts.map

View File

@@ -0,0 +1,22 @@
/**
* @license Copyright 2017 The Lighthouse Authors. 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.
*/
import * as LH from '../../types/lh.js';
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecorder} from '../lib/network-recorder.js';
class NetworkRecords {
/**
* @param {LH.DevtoolsLog} devtoolsLog
* @return {Promise<Array<LH.Artifacts.NetworkRequest>>} networkRecords
*/
static async compute_(devtoolsLog) {
return NetworkRecorder.recordsFromLogs(devtoolsLog);
}
}
const NetworkRecordsComputed = makeComputedArtifact(NetworkRecords, null);
export {NetworkRecordsComputed as NetworkRecords};

View File

@@ -0,0 +1,79 @@
export { PageDependencyGraphComputed as PageDependencyGraph };
export type Node = import('../lib/dependency-graph/base-node.js').Node;
export type URLArtifact = Omit<LH.Artifacts['URL'], 'finalDisplayedUrl'>;
export type NetworkNodeOutput = {
nodes: Array<NetworkNode>;
idToNodeMap: Map<string, NetworkNode>;
urlToNodeMap: Map<string, Array<NetworkNode>>;
frameIdToNodeMap: Map<string, NetworkNode | null>;
};
declare const PageDependencyGraphComputed: typeof PageDependencyGraph & {
request: (dependencies: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
URL: LH.Artifacts['URL'];
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../lib/dependency-graph/base-node.js").Node>;
};
import { NetworkNode } from '../lib/dependency-graph/network-node.js';
declare class PageDependencyGraph {
/**
* @param {LH.Artifacts.NetworkRequest} record
* @return {Array<string>}
*/
static getNetworkInitiators(record: LH.Artifacts.NetworkRequest): Array<string>;
/**
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @return {NetworkNodeOutput}
*/
static getNetworkNodeOutput(networkRecords: Array<LH.Artifacts.NetworkRequest>): NetworkNodeOutput;
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {Array<CPUNode>}
*/
static getCPUNodes({ mainThreadEvents }: LH.Artifacts.ProcessedTrace): Array<CPUNode>;
/**
* @param {NetworkNode} rootNode
* @param {NetworkNodeOutput} networkNodeOutput
*/
static linkNetworkNodes(rootNode: NetworkNode, networkNodeOutput: NetworkNodeOutput): void;
/**
* @param {Node} rootNode
* @param {NetworkNodeOutput} networkNodeOutput
* @param {Array<CPUNode>} cpuNodes
*/
static linkCPUNodes(rootNode: Node, networkNodeOutput: NetworkNodeOutput, cpuNodes: Array<CPUNode>): void;
/**
* Removes the given node from the graph, but retains all paths between its dependencies and
* dependents.
* @param {Node} node
*/
static _pruneNode(node: Node): void;
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @param {URLArtifact} URL
* @return {Node}
*/
static createGraph(processedTrace: LH.Artifacts.ProcessedTrace, networkRecords: Array<LH.Artifacts.NetworkRequest>, URL: URLArtifact): Node;
/**
*
* @param {Node} rootNode
*/
static printGraph(rootNode: Node, widthInCharacters?: number): void;
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, URL: LH.Artifacts['URL']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Node>}
*/
static compute_(data: {
trace: LH.Trace;
devtoolsLog: import("../index.js").DevtoolsLog;
URL: LH.Artifacts['URL'];
}, context: LH.Artifacts.ComputedContext): Promise<Node>;
}
import { NetworkRequest } from '../lib/network-request.js';
import { ProcessedTrace } from './processed-trace.js';
import { CPUNode } from '../lib/dependency-graph/cpu-node.js';
//# sourceMappingURL=page-dependency-graph.d.ts.map

View File

@@ -0,0 +1,485 @@
/**
* @license Copyright 2017 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkNode} from '../lib/dependency-graph/network-node.js';
import {CPUNode} from '../lib/dependency-graph/cpu-node.js';
import {TraceProcessor} from '../lib/tracehouse/trace-processor.js';
import {NetworkRequest} from '../lib/network-request.js';
import {ProcessedTrace} from './processed-trace.js';
import {NetworkRecords} from './network-records.js';
import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
import {DocumentUrls} from './document-urls.js';
/** @typedef {import('../lib/dependency-graph/base-node.js').Node} Node */
/** @typedef {Omit<LH.Artifacts['URL'], 'finalDisplayedUrl'>} URLArtifact */
/**
* @typedef {Object} NetworkNodeOutput
* @property {Array<NetworkNode>} nodes
* @property {Map<string, NetworkNode>} idToNodeMap
* @property {Map<string, Array<NetworkNode>>} urlToNodeMap
* @property {Map<string, NetworkNode|null>} frameIdToNodeMap
*/
// Shorter tasks have negligible impact on simulation results.
const SIGNIFICANT_DUR_THRESHOLD_MS = 10;
// TODO: video files tend to be enormous and throw off all graph traversals, move this ignore
// into estimation logic when we use the dependency graph for other purposes.
const IGNORED_MIME_TYPES_REGEX = /^video/;
class PageDependencyGraph {
/**
* @param {LH.Artifacts.NetworkRequest} record
* @return {Array<string>}
*/
static getNetworkInitiators(record) {
if (!record.initiator) return [];
if (record.initiator.url) return [record.initiator.url];
if (record.initiator.type === 'script') {
// Script initiators have the stack of callFrames from all functions that led to this request.
// If async stacks are enabled, then the stack will also have the parent functions that asynchronously
// led to this request chained in the `parent` property.
/** @type {Set<string>} */
const scriptURLs = new Set();
let stack = record.initiator.stack;
while (stack) {
const callFrames = stack.callFrames || [];
for (const frame of callFrames) {
if (frame.url) scriptURLs.add(frame.url);
}
stack = stack.parent;
}
return Array.from(scriptURLs);
}
return [];
}
/**
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @return {NetworkNodeOutput}
*/
static getNetworkNodeOutput(networkRecords) {
/** @type {Array<NetworkNode>} */
const nodes = [];
/** @type {Map<string, NetworkNode>} */
const idToNodeMap = new Map();
/** @type {Map<string, Array<NetworkNode>>} */
const urlToNodeMap = new Map();
/** @type {Map<string, NetworkNode|null>} */
const frameIdToNodeMap = new Map();
networkRecords.forEach(record => {
if (IGNORED_MIME_TYPES_REGEX.test(record.mimeType)) return;
// Network record requestIds can be duplicated for an unknown reason
// Suffix all subsequent records with `:duplicate` until it's unique
// NOTE: This should never happen with modern NetworkRequest library, but old fixtures
// might still have this issue.
while (idToNodeMap.has(record.requestId)) {
record.requestId += ':duplicate';
}
const node = new NetworkNode(record);
nodes.push(node);
const urlList = urlToNodeMap.get(record.url) || [];
urlList.push(node);
idToNodeMap.set(record.requestId, node);
urlToNodeMap.set(record.url, urlList);
// If the request was for the root document of an iframe, save an entry in our
// map so we can link up the task `args.data.frame` dependencies later in graph creation.
if (record.frameId &&
record.resourceType === NetworkRequest.TYPES.Document &&
record.documentURL === record.url) {
// If there's ever any ambiguity, permanently set the value to `false` to avoid loops in the graph.
const value = frameIdToNodeMap.has(record.frameId) ? null : node;
frameIdToNodeMap.set(record.frameId, value);
}
});
return {nodes, idToNodeMap, urlToNodeMap, frameIdToNodeMap};
}
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {Array<CPUNode>}
*/
static getCPUNodes({mainThreadEvents}) {
/** @type {Array<CPUNode>} */
const nodes = [];
let i = 0;
TraceProcessor.assertHasToplevelEvents(mainThreadEvents);
while (i < mainThreadEvents.length) {
const evt = mainThreadEvents[i];
i++;
// Skip all trace events that aren't schedulable tasks with sizable duration
if (!TraceProcessor.isScheduleableTask(evt) || !evt.dur) {
continue;
}
// Capture all events that occurred within the task
/** @type {Array<LH.TraceEvent>} */
const children = [];
for (
const endTime = evt.ts + evt.dur;
i < mainThreadEvents.length && mainThreadEvents[i].ts < endTime;
i++
) {
children.push(mainThreadEvents[i]);
}
nodes.push(new CPUNode(evt, children));
}
return nodes;
}
/**
* @param {NetworkNode} rootNode
* @param {NetworkNodeOutput} networkNodeOutput
*/
static linkNetworkNodes(rootNode, networkNodeOutput) {
networkNodeOutput.nodes.forEach(node => {
const directInitiatorRequest = node.record.initiatorRequest || rootNode.record;
const directInitiatorNode =
networkNodeOutput.idToNodeMap.get(directInitiatorRequest.requestId) || rootNode;
const canDependOnInitiator =
!directInitiatorNode.isDependentOn(node) &&
node.canDependOn(directInitiatorNode);
const initiators = PageDependencyGraph.getNetworkInitiators(node.record);
if (initiators.length) {
initiators.forEach(initiator => {
const parentCandidates = networkNodeOutput.urlToNodeMap.get(initiator) || [];
// Only add the edge if the parent is unambiguous with valid timing and isn't circular.
if (parentCandidates.length === 1 &&
parentCandidates[0].startTime <= node.startTime &&
!parentCandidates[0].isDependentOn(node)) {
node.addDependency(parentCandidates[0]);
} else if (canDependOnInitiator) {
directInitiatorNode.addDependent(node);
}
});
} else if (canDependOnInitiator) {
directInitiatorNode.addDependent(node);
}
// Make sure the nodes are attached to the graph if the initiator information was invalid.
if (node !== rootNode && node.getDependencies().length === 0 && node.canDependOn(rootNode)) {
node.addDependency(rootNode);
}
if (!node.record.redirects) return;
const redirects = [...node.record.redirects, node.record];
for (let i = 1; i < redirects.length; i++) {
const redirectNode = networkNodeOutput.idToNodeMap.get(redirects[i - 1].requestId);
const actualNode = networkNodeOutput.idToNodeMap.get(redirects[i].requestId);
if (actualNode && redirectNode) {
actualNode.addDependency(redirectNode);
}
}
});
}
/**
* @param {Node} rootNode
* @param {NetworkNodeOutput} networkNodeOutput
* @param {Array<CPUNode>} cpuNodes
*/
static linkCPUNodes(rootNode, networkNodeOutput, cpuNodes) {
/** @type {Set<LH.Crdp.Network.ResourceType|undefined>} */
const linkableResourceTypes = new Set([
NetworkRequest.TYPES.XHR, NetworkRequest.TYPES.Fetch, NetworkRequest.TYPES.Script,
]);
/** @param {CPUNode} cpuNode @param {string} reqId */
function addDependentNetworkRequest(cpuNode, reqId) {
const networkNode = networkNodeOutput.idToNodeMap.get(reqId);
if (!networkNode ||
// Ignore all network nodes that started before this CPU task started
// A network request that started earlier could not possibly have been started by this task
networkNode.startTime <= cpuNode.startTime) return;
const {record} = networkNode;
const resourceType = record.resourceType ||
record.redirectDestination?.resourceType;
if (!linkableResourceTypes.has(resourceType)) {
// We only link some resources to CPU nodes because we observe LCP simulation
// regressions when including images, etc.
return;
}
cpuNode.addDependent(networkNode);
}
/**
* If the node has an associated frameId, then create a dependency on the root document request
* for the frame. The task obviously couldn't have started before the frame was even downloaded.
*
* @param {CPUNode} cpuNode
* @param {string|undefined} frameId
*/
function addDependencyOnFrame(cpuNode, frameId) {
if (!frameId) return;
const networkNode = networkNodeOutput.frameIdToNodeMap.get(frameId);
if (!networkNode) return;
// Ignore all network nodes that started after this CPU task started
// A network request that started after could not possibly be required this task
if (networkNode.startTime >= cpuNode.startTime) return;
cpuNode.addDependency(networkNode);
}
/** @param {CPUNode} cpuNode @param {string} url */
function addDependencyOnUrl(cpuNode, url) {
if (!url) return;
// Allow network requests that end up to 100ms before the task started
// Some script evaluations can start before the script finishes downloading
const minimumAllowableTimeSinceNetworkNodeEnd = -100 * 1000;
const candidates = networkNodeOutput.urlToNodeMap.get(url) || [];
let minCandidate = null;
let minDistance = Infinity;
// Find the closest request that finished before this CPU task started
for (const candidate of candidates) {
// Explicitly ignore all requests that started after this CPU node
// A network request that started after this task started cannot possibly be a dependency
if (cpuNode.startTime <= candidate.startTime) return;
const distance = cpuNode.startTime - candidate.endTime;
if (distance >= minimumAllowableTimeSinceNetworkNodeEnd && distance < minDistance) {
minCandidate = candidate;
minDistance = distance;
}
}
if (!minCandidate) return;
cpuNode.addDependency(minCandidate);
}
/** @type {Map<string, CPUNode>} */
const timers = new Map();
for (const node of cpuNodes) {
for (const evt of node.childEvents) {
if (!evt.args.data) continue;
const argsUrl = evt.args.data.url;
const stackTraceUrls = (evt.args.data.stackTrace || []).map(l => l.url).filter(Boolean);
switch (evt.name) {
case 'TimerInstall':
// @ts-expect-error - 'TimerInstall' event means timerId exists.
timers.set(evt.args.data.timerId, node);
stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
break;
case 'TimerFire': {
// @ts-expect-error - 'TimerFire' event means timerId exists.
const installer = timers.get(evt.args.data.timerId);
if (!installer || installer.endTime > node.startTime) break;
installer.addDependent(node);
break;
}
case 'InvalidateLayout':
case 'ScheduleStyleRecalculation':
addDependencyOnFrame(node, evt.args.data.frame);
stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
break;
case 'EvaluateScript':
addDependencyOnFrame(node, evt.args.data.frame);
// @ts-expect-error - 'EvaluateScript' event means argsUrl is defined.
addDependencyOnUrl(node, argsUrl);
stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
break;
case 'XHRReadyStateChange':
// Only create the dependency if the request was completed
// 'XHRReadyStateChange' event means readyState is defined.
if (evt.args.data.readyState !== 4) break;
// @ts-expect-error - 'XHRReadyStateChange' event means argsUrl is defined.
addDependencyOnUrl(node, argsUrl);
stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
break;
case 'FunctionCall':
case 'v8.compile':
addDependencyOnFrame(node, evt.args.data.frame);
// @ts-expect-error - events mean argsUrl is defined.
addDependencyOnUrl(node, argsUrl);
break;
case 'ParseAuthorStyleSheet':
addDependencyOnFrame(node, evt.args.data.frame);
// @ts-expect-error - 'ParseAuthorStyleSheet' event means styleSheetUrl is defined.
addDependencyOnUrl(node, evt.args.data.styleSheetUrl);
break;
case 'ResourceSendRequest':
addDependencyOnFrame(node, evt.args.data.frame);
// @ts-expect-error - 'ResourceSendRequest' event means requestId is defined.
addDependentNetworkRequest(node, evt.args.data.requestId);
stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
break;
}
}
// Nodes starting before the root node cannot depend on it.
if (node.getNumberOfDependencies() === 0 && node.canDependOn(rootNode)) {
node.addDependency(rootNode);
}
}
// Second pass to prune the graph of short tasks.
const minimumEvtDur = SIGNIFICANT_DUR_THRESHOLD_MS * 1000;
let foundFirstLayout = false;
let foundFirstPaint = false;
let foundFirstParse = false;
for (const node of cpuNodes) {
// Don't prune if event is the first ParseHTML/Layout/Paint.
// See https://github.com/GoogleChrome/lighthouse/issues/9627#issuecomment-526699524 for more.
let isFirst = false;
if (!foundFirstLayout && node.childEvents.some(evt => evt.name === 'Layout')) {
isFirst = foundFirstLayout = true;
}
if (!foundFirstPaint && node.childEvents.some(evt => evt.name === 'Paint')) {
isFirst = foundFirstPaint = true;
}
if (!foundFirstParse && node.childEvents.some(evt => evt.name === 'ParseHTML')) {
isFirst = foundFirstParse = true;
}
if (isFirst || node.event.dur >= minimumEvtDur) {
// Don't prune this node. The task is long / important so it will impact simulation.
continue;
}
// Prune the node if it isn't highly connected to minimize graph size. Rewiring the graph
// here replaces O(M + N) edges with (M * N) edges, which is fine if either M or N is at
// most 1.
if (node.getNumberOfDependencies() === 1 || node.getNumberOfDependents() <= 1) {
PageDependencyGraph._pruneNode(node);
}
}
}
/**
* Removes the given node from the graph, but retains all paths between its dependencies and
* dependents.
* @param {Node} node
*/
static _pruneNode(node) {
const dependencies = node.getDependencies();
const dependents = node.getDependents();
for (const dependency of dependencies) {
node.removeDependency(dependency);
for (const dependent of dependents) {
dependency.addDependent(dependent);
}
}
for (const dependent of dependents) {
node.removeDependent(dependent);
}
}
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @param {URLArtifact} URL
* @return {Node}
*/
static createGraph(processedTrace, networkRecords, URL) {
const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
const {requestedUrl, mainDocumentUrl} = URL;
if (!requestedUrl) throw new Error('requestedUrl is required to get the root request');
if (!mainDocumentUrl) throw new Error('mainDocumentUrl is required to get the main resource');
const rootRequest = NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
if (!rootRequest) throw new Error('rootRequest not found');
const rootNode = networkNodeOutput.idToNodeMap.get(rootRequest.requestId);
if (!rootNode) throw new Error('rootNode not found');
const mainDocumentRequest = NetworkAnalyzer.findResourceForUrl(networkRecords, mainDocumentUrl);
if (!mainDocumentRequest) throw new Error('mainDocumentRequest not found');
const mainDocumentNode = networkNodeOutput.idToNodeMap.get(mainDocumentRequest.requestId);
if (!mainDocumentNode) throw new Error('mainDocumentNode not found');
PageDependencyGraph.linkNetworkNodes(rootNode, networkNodeOutput);
PageDependencyGraph.linkCPUNodes(rootNode, networkNodeOutput, cpuNodes);
mainDocumentNode.setIsMainDocument(true);
if (NetworkNode.hasCycle(rootNode)) {
throw new Error('Invalid dependency graph created, cycle detected');
}
return rootNode;
}
/**
*
* @param {Node} rootNode
*/
static printGraph(rootNode, widthInCharacters = 100) {
/** @param {string} str @param {number} target */
function padRight(str, target, padChar = ' ') {
return str + padChar.repeat(Math.max(target - str.length, 0));
}
/** @type {Array<Node>} */
const nodes = [];
rootNode.traverse(node => nodes.push(node));
nodes.sort((a, b) => a.startTime - b.startTime);
const min = nodes[0].startTime;
const max = nodes.reduce((max, node) => Math.max(max, node.endTime), 0);
const totalTime = max - min;
const timePerCharacter = totalTime / widthInCharacters;
nodes.forEach(node => {
const offset = Math.round((node.startTime - min) / timePerCharacter);
const length = Math.ceil((node.endTime - node.startTime) / timePerCharacter);
const bar = padRight('', offset) + padRight('', length, '=');
// @ts-expect-error -- disambiguate displayName from across possible Node types.
const displayName = node.record ? node.record.url : node.type;
// eslint-disable-next-line
console.log(padRight(bar, widthInCharacters), `| ${displayName.slice(0, 30)}`);
});
}
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, URL: LH.Artifacts['URL']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Node>}
*/
static async compute_(data, context) {
const {trace, devtoolsLog} = data;
const [processedTrace, networkRecords] = await Promise.all([
ProcessedTrace.request(trace, context),
NetworkRecords.request(devtoolsLog, context),
]);
// COMPAT: Backport for pre-10.0 clients that don't pass the URL artifact here (e.g. pubads).
// Calculates the URL artifact from the processed trace and DT log.
const URL = data.URL || await DocumentUrls.request(data, context);
return PageDependencyGraph.createGraph(processedTrace, networkRecords, URL);
}
}
const PageDependencyGraphComputed =
makeComputedArtifact(PageDependencyGraph, ['devtoolsLog', 'trace', 'URL']);
export {PageDependencyGraphComputed as PageDependencyGraph};

View File

@@ -0,0 +1,21 @@
export { ProcessedNavigationComputed as ProcessedNavigation };
declare const ProcessedNavigationComputed: typeof ProcessedNavigation & {
request: (dependencies: import("../index.js").Trace | import("../index.js").Artifacts.ProcessedTrace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.ProcessedNavigation>;
};
declare class ProcessedNavigation {
/**
* @param {LH.Trace | LH.Artifacts.ProcessedTrace} traceOrProcessedTrace
* @return {traceOrProcessedTrace is LH.Artifacts.ProcessedTrace}
*/
static isProcessedTrace(traceOrProcessedTrace: LH.Trace | LH.Artifacts.ProcessedTrace): traceOrProcessedTrace is import("../index.js").Artifacts.ProcessedTrace;
/**
* @param {LH.Trace | LH.Artifacts.ProcessedTrace} traceOrProcessedTrace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.ProcessedNavigation>}
*/
static compute_(traceOrProcessedTrace: LH.Trace | LH.Artifacts.ProcessedTrace, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.ProcessedNavigation>;
}
import { ProcessedTrace } from './processed-trace.js';
//# sourceMappingURL=processed-navigation.d.ts.map

View File

@@ -0,0 +1,37 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {ProcessedTrace} from './processed-trace.js';
import LHTraceProcessor from '../lib/lh-trace-processor.js';
class ProcessedNavigation {
/**
* @param {LH.Trace | LH.Artifacts.ProcessedTrace} traceOrProcessedTrace
* @return {traceOrProcessedTrace is LH.Artifacts.ProcessedTrace}
*/
static isProcessedTrace(traceOrProcessedTrace) {
return 'timeOriginEvt' in traceOrProcessedTrace;
}
/**
* @param {LH.Trace | LH.Artifacts.ProcessedTrace} traceOrProcessedTrace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.ProcessedNavigation>}
*/
static async compute_(traceOrProcessedTrace, context) {
// TODO: Remove this backport once pubads passes in a raw trace.
if (this.isProcessedTrace(traceOrProcessedTrace)) {
return LHTraceProcessor.processNavigation(traceOrProcessedTrace);
}
const processedTrace = await ProcessedTrace.request(traceOrProcessedTrace, context);
return LHTraceProcessor.processNavigation(processedTrace);
}
}
const ProcessedNavigationComputed = makeComputedArtifact(ProcessedNavigation, null);
export {ProcessedNavigationComputed as ProcessedNavigation};

View File

@@ -0,0 +1,14 @@
export { ProcessedTraceComputed as ProcessedTrace };
declare const ProcessedTraceComputed: typeof ProcessedTrace & {
request: (dependencies: import("../index.js").Trace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.ProcessedTrace>;
};
declare class ProcessedTrace {
/**
* @param {LH.Trace} trace
* @return {Promise<LH.Artifacts.ProcessedTrace>}
*/
static compute_(trace: LH.Trace): Promise<LH.Artifacts.ProcessedTrace>;
}
//# sourceMappingURL=processed-trace.d.ts.map

View File

@@ -0,0 +1,21 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import LHTraceProcessor from '../lib/lh-trace-processor.js';
class ProcessedTrace {
/**
* @param {LH.Trace} trace
* @return {Promise<LH.Artifacts.ProcessedTrace>}
*/
static async compute_(trace) {
return LHTraceProcessor.processTrace(trace);
}
}
const ProcessedTraceComputed = makeComputedArtifact(ProcessedTrace, null);
export {ProcessedTraceComputed as ProcessedTrace};

View File

@@ -0,0 +1,46 @@
export { ResourceSummaryComputed as ResourceSummary };
export type ResourceEntry = {
count: number;
resourceSize: number;
transferSize: number;
};
declare const ResourceSummaryComputed: typeof ResourceSummary & {
request: (dependencies: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
budgets: LH.Util.ImmutableObject<LH.Budget[] | null>;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<Record<import("../../types/lhr/budget.js").default.ResourceType, ResourceEntry>>;
};
/** @typedef {{count: number, resourceSize: number, transferSize: number}} ResourceEntry */
declare class ResourceSummary {
/**
* @param {LH.Artifacts.NetworkRequest} record
* @return {LH.Budget.ResourceType}
*/
static determineResourceType(record: LH.Artifacts.NetworkRequest): LH.Budget.ResourceType;
/**
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @param {LH.Artifacts.URL} URLArtifact
* @param {LH.Util.ImmutableObject<LH.Budget[]|null>} budgets
* @param {LH.Artifacts.EntityClassification} classifiedEntities
* @return {Record<LH.Budget.ResourceType, ResourceEntry>}
*/
static summarize(networkRecords: Array<LH.Artifacts.NetworkRequest>, URLArtifact: LH.Artifacts.URL, budgets: LH.Util.ImmutableObject<LH.Budget[] | null>, classifiedEntities: LH.Artifacts.EntityClassification): Record<LH.Budget.ResourceType, ResourceEntry>;
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog, budgets: LH.Util.ImmutableObject<LH.Budget[]|null>}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Record<LH.Budget.ResourceType,ResourceEntry>>}
*/
static compute_(data: {
URL: LH.Artifacts['URL'];
devtoolsLog: import("../index.js").DevtoolsLog;
budgets: LH.Util.ImmutableObject<LH.Budget[] | null>;
}, context: LH.Artifacts.ComputedContext): Promise<Record<LH.Budget.ResourceType, ResourceEntry>>;
}
import { Util } from '../../shared/util.js';
import { Budget } from '../config/budget.js';
import { NetworkRequest } from '../lib/network-request.js';
import { EntityClassification } from './entity-classification.js';
//# sourceMappingURL=resource-summary.d.ts.map

View File

@@ -0,0 +1,118 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {EntityClassification} from './entity-classification.js';
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecords} from './network-records.js';
import {NetworkRequest} from '../lib/network-request.js';
import {Budget} from '../config/budget.js';
import {Util} from '../../shared/util.js';
/** @typedef {{count: number, resourceSize: number, transferSize: number}} ResourceEntry */
class ResourceSummary {
/**
* @param {LH.Artifacts.NetworkRequest} record
* @return {LH.Budget.ResourceType}
*/
static determineResourceType(record) {
if (!record.resourceType) return 'other';
/** @type {Partial<Record<LH.Crdp.Network.ResourceType, LH.Budget.ResourceType>>} */
const requestToResourceType = {
'Stylesheet': 'stylesheet',
'Image': 'image',
'Media': 'media',
'Font': 'font',
'Script': 'script',
'Document': 'document',
};
return requestToResourceType[record.resourceType] || 'other';
}
/**
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @param {LH.Artifacts.URL} URLArtifact
* @param {LH.Util.ImmutableObject<LH.Budget[]|null>} budgets
* @param {LH.Artifacts.EntityClassification} classifiedEntities
* @return {Record<LH.Budget.ResourceType, ResourceEntry>}
*/
static summarize(networkRecords, URLArtifact, budgets, classifiedEntities) {
/** @type {Record<LH.Budget.ResourceType, ResourceEntry>} */
const resourceSummary = {
'stylesheet': {count: 0, resourceSize: 0, transferSize: 0},
'image': {count: 0, resourceSize: 0, transferSize: 0},
'media': {count: 0, resourceSize: 0, transferSize: 0},
'font': {count: 0, resourceSize: 0, transferSize: 0},
'script': {count: 0, resourceSize: 0, transferSize: 0},
'document': {count: 0, resourceSize: 0, transferSize: 0},
'other': {count: 0, resourceSize: 0, transferSize: 0},
'total': {count: 0, resourceSize: 0, transferSize: 0},
'third-party': {count: 0, resourceSize: 0, transferSize: 0},
};
const budget = Budget.getMatchingBudget(budgets, URLArtifact.mainDocumentUrl);
/** @type {ReadonlyArray<string>} */
let firstPartyHosts = [];
if (budget?.options?.firstPartyHostnames) {
firstPartyHosts = budget.options.firstPartyHostnames;
} else {
firstPartyHosts = classifiedEntities.firstParty?.domains.map(domain => `*.${domain}`) ||
[`*.${Util.getRootDomain(URLArtifact.finalDisplayedUrl)}`];
}
networkRecords.filter(record => {
// Ignore favicon.co
// Headless Chrome does not request /favicon.ico, so don't consider this request.
// Makes resource summary consistent across LR / other channels.
const type = this.determineResourceType(record);
if (type === 'other' && record.url.endsWith('/favicon.ico')) {
return false;
}
// Ignore non-network protocols
if (NetworkRequest.isNonNetworkRequest(record)) return false;
return true;
}).forEach((record) => {
const type = this.determineResourceType(record);
resourceSummary[type].count++;
resourceSummary[type].resourceSize += record.resourceSize;
resourceSummary[type].transferSize += record.transferSize;
resourceSummary.total.count++;
resourceSummary.total.resourceSize += record.resourceSize;
resourceSummary.total.transferSize += record.transferSize;
const isFirstParty = firstPartyHosts.some((hostExp) => {
const url = new URL(record.url);
if (hostExp.startsWith('*.')) {
return url.hostname.endsWith(hostExp.slice(2));
}
return url.hostname === hostExp;
});
if (!isFirstParty) {
resourceSummary['third-party'].count++;
resourceSummary['third-party'].resourceSize += record.resourceSize;
resourceSummary['third-party'].transferSize += record.transferSize;
}
});
return resourceSummary;
}
/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog, budgets: LH.Util.ImmutableObject<LH.Budget[]|null>}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Record<LH.Budget.ResourceType,ResourceEntry>>}
*/
static async compute_(data, context) {
const networkRecords = await NetworkRecords.request(data.devtoolsLog, context);
const classifiedEntities = await EntityClassification.request(
{URL: data.URL, devtoolsLog: data.devtoolsLog}, context);
return ResourceSummary.summarize(networkRecords, data.URL, data.budgets, classifiedEntities);
}
}
const ResourceSummaryComputed =
makeComputedArtifact(ResourceSummary, ['URL', 'devtoolsLog', 'budgets']);
export {ResourceSummaryComputed as ResourceSummary};

20
node_modules/lighthouse/core/computed/screenshots.d.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
export { ScreenshotsComputed as Screenshots };
declare const ScreenshotsComputed: typeof Screenshots & {
request: (dependencies: import("../index.js").Trace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<{
timestamp: number;
datauri: string;
}[]>;
};
declare class Screenshots {
/**
* @param {LH.Trace} trace
* @return {Promise<Array<{timestamp: number, datauri: string}>>}
*/
static compute_(trace: LH.Trace): Promise<Array<{
timestamp: number;
datauri: string;
}>>;
}
//# sourceMappingURL=screenshots.d.ts.map

29
node_modules/lighthouse/core/computed/screenshots.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
/**
* @license Copyright 2016 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
const SCREENSHOT_TRACE_NAME = 'Screenshot';
class Screenshots {
/**
* @param {LH.Trace} trace
* @return {Promise<Array<{timestamp: number, datauri: string}>>}
*/
static async compute_(trace) {
return trace.traceEvents
.filter(evt => evt.name === SCREENSHOT_TRACE_NAME)
.map(evt => {
return {
timestamp: evt.ts,
datauri: `data:image/jpeg;base64,${evt.args.snapshot}`,
};
});
}
}
const ScreenshotsComputed = makeComputedArtifact(Screenshots, null);
export {ScreenshotsComputed as Screenshots};

15
node_modules/lighthouse/core/computed/speedline.d.ts generated vendored Normal file
View File

@@ -0,0 +1,15 @@
export { SpeedlineComputed as Speedline };
declare const SpeedlineComputed: typeof Speedline & {
request: (dependencies: import("../index.js").Trace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../index.js").Artifacts.Speedline>;
};
declare class Speedline {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Speedline>}
*/
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.Speedline>;
}
//# sourceMappingURL=speedline.d.ts.map

55
node_modules/lighthouse/core/computed/speedline.js generated vendored Normal file
View File

@@ -0,0 +1,55 @@
/**
* @license Copyright 2016 The Lighthouse Authors. 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.
*/
import speedline from 'speedline-core';
import {makeComputedArtifact} from './computed-artifact.js';
import {LighthouseError} from '../lib/lh-error.js';
import {ProcessedTrace} from './processed-trace.js';
class Speedline {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Speedline>}
*/
static async compute_(trace, context) {
// speedline() may throw without a promise, so we resolve immediately
// to get in a promise chain.
return ProcessedTrace.request(trace, context).then(processedTrace => {
// Use a shallow copy of traceEvents so speedline can sort as it pleases.
// See https://github.com/GoogleChrome/lighthouse/issues/2333
const traceEvents = trace.traceEvents.slice();
// Force use of timeOrigin as reference point for speedline
// See https://github.com/GoogleChrome/lighthouse/issues/2095
const timeOrigin = processedTrace.timestamps.timeOrigin;
return speedline(traceEvents, {
timeOrigin,
fastMode: true,
include: 'speedIndex',
});
}).catch(err => {
if (/No screenshots found in trace/.test(err.message)) {
throw new LighthouseError(LighthouseError.errors.NO_SCREENSHOTS);
}
throw err;
}).then(speedline => {
if (speedline.frames.length === 0) {
throw new LighthouseError(LighthouseError.errors.NO_SPEEDLINE_FRAMES);
}
if (speedline.speedIndex === 0) {
throw new LighthouseError(LighthouseError.errors.SPEEDINDEX_OF_ZERO);
}
return speedline;
});
}
}
const SpeedlineComputed = makeComputedArtifact(Speedline, null);
export {SpeedlineComputed as Speedline};

View File

@@ -0,0 +1,54 @@
export { TBTImpactTasksComputed as TBTImpactTasks };
export type TBTImpactTask = LH.Artifacts.TaskNode & {
tbtImpact: number;
selfTbtImpact: number;
};
declare const TBTImpactTasksComputed: typeof TBTImpactTasks & {
request: (dependencies: import("../index.js").Artifacts.MetricComputationDataInput, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<TBTImpactTask[]>;
};
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
declare class TBTImpactTasks {
/**
* @param {LH.Artifacts.TaskNode} task
* @return {LH.Artifacts.TaskNode}
*/
static getTopLevelTask(task: LH.Artifacts.TaskNode): LH.Artifacts.TaskNode;
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
*/
static getTbtBounds(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
startTimeMs: number;
endTimeMs: number;
}>;
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
*/
static createImpactTasks(tasks: LH.Artifacts.TaskNode[], taskToImpact: Map<LH.Artifacts.TaskNode, number>): TBTImpactTask[];
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {TBTImpactTask[]}
*/
static computeImpactsFromObservedTasks(tasks: LH.Artifacts.TaskNode[], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {TBTImpactTask[]}
*/
static computeImpactsFromLantern(tasks: LH.Artifacts.TaskNode[], tbtNodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<TBTImpactTask[]>}
*/
static compute_(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<TBTImpactTask[]>;
}
//# sourceMappingURL=tbt-impact-tasks.d.ts.map

View File

@@ -0,0 +1,221 @@
/**
* @license Copyright 2023 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {MainThreadTasks} from './main-thread-tasks.js';
import {FirstContentfulPaint} from './metrics/first-contentful-paint.js';
import {Interactive} from './metrics/interactive.js';
import {TotalBlockingTime} from './metrics/total-blocking-time.js';
import {ProcessedTrace} from './processed-trace.js';
import {calculateTbtImpactForEvent} from './metrics/tbt-utils.js';
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
class TBTImpactTasks {
/**
* @param {LH.Artifacts.TaskNode} task
* @return {LH.Artifacts.TaskNode}
*/
static getTopLevelTask(task) {
let topLevelTask = task;
while (topLevelTask.parent) {
topLevelTask = topLevelTask.parent;
}
return topLevelTask;
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
*/
static async getTbtBounds(metricComputationData, context) {
const processedTrace = await ProcessedTrace.request(metricComputationData.trace, context);
if (metricComputationData.gatherContext.gatherMode !== 'navigation') {
return {
startTimeMs: 0,
endTimeMs: processedTrace.timings.traceEnd,
};
}
const fcpResult = await FirstContentfulPaint.request(metricComputationData, context);
const ttiResult = await Interactive.request(metricComputationData, context);
let startTimeMs = fcpResult.timing;
let endTimeMs = ttiResult.timing;
// When using lantern, we want to get a pessimistic view of the long tasks.
// This means we assume the earliest possible start time and latest possible end time.
if ('optimisticEstimate' in fcpResult) {
startTimeMs = fcpResult.optimisticEstimate.timeInMs;
}
if ('pessimisticEstimate' in ttiResult) {
endTimeMs = ttiResult.pessimisticEstimate.timeInMs;
}
return {startTimeMs, endTimeMs};
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
*/
static createImpactTasks(tasks, taskToImpact) {
/** @type {TBTImpactTask[]} */
const tbtImpactTasks = [];
for (const task of tasks) {
const tbtImpact = taskToImpact.get(task) || 0;
let selfTbtImpact = tbtImpact;
for (const child of task.children) {
const childTbtImpact = taskToImpact.get(child) || 0;
selfTbtImpact -= childTbtImpact;
}
tbtImpactTasks.push({
...task,
tbtImpact,
selfTbtImpact,
});
}
return tbtImpactTasks;
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {TBTImpactTask[]}
*/
static computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs) {
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToImpact = new Map();
for (const task of tasks) {
const event = {
start: task.startTime,
end: task.endTime,
duration: task.duration,
};
const topLevelTask = this.getTopLevelTask(task);
const topLevelEvent = {
start: topLevelTask.startTime,
end: topLevelTask.endTime,
duration: topLevelTask.duration,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
taskToImpact.set(task, tbtImpact);
}
return this.createImpactTasks(tasks, taskToImpact);
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {TBTImpactTask[]}
*/
static computeImpactsFromLantern(tasks, tbtNodeTimings, startTimeMs, endTimeMs) {
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToImpact = new Map();
/** @type {Map<LH.Artifacts.TaskNode, {start: number, end: number, duration: number}>} */
const topLevelTaskToEvent = new Map();
/** @type {Map<LH.TraceEvent, LH.Artifacts.TaskNode>} */
const traceEventToTask = new Map();
for (const task of tasks) {
traceEventToTask.set(task.event, task);
}
// Use lantern TBT timings to calculate the TBT impact of top level tasks.
for (const [node, timing] of tbtNodeTimings) {
if (node.type !== 'cpu') continue;
const event = {
start: timing.startTime,
end: timing.endTime,
duration: timing.duration,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
const task = traceEventToTask.get(node.event);
if (!task) continue;
topLevelTaskToEvent.set(task, event);
taskToImpact.set(task, tbtImpact);
}
// Interpolate the TBT impact of remaining tasks using the top level ancestor tasks.
// We don't have any lantern estimates for tasks that are not top level, so we need to estimate
// the lantern timing based on the task's observed timing relative to it's top level task's observed timing.
for (const task of tasks) {
if (taskToImpact.has(task)) continue;
const topLevelTask = this.getTopLevelTask(task);
const topLevelEvent = topLevelTaskToEvent.get(topLevelTask);
if (!topLevelEvent) continue;
const startRatio = (task.startTime - topLevelTask.startTime) / topLevelTask.duration;
const start = startRatio * topLevelEvent.duration + topLevelEvent.start;
const endRatio = (topLevelTask.endTime - task.endTime) / topLevelTask.duration;
const end = topLevelEvent.end - endRatio * topLevelEvent.duration;
const event = {
start,
end,
duration: end - start,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
taskToImpact.set(task, tbtImpact);
}
return this.createImpactTasks(tasks, taskToImpact);
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<TBTImpactTask[]>}
*/
static async compute_(metricComputationData, context) {
const tbtResult = await TotalBlockingTime.request(metricComputationData, context);
const tasks = await MainThreadTasks.request(metricComputationData.trace, context);
const {startTimeMs, endTimeMs} = await this.getTbtBounds(metricComputationData, context);
if ('pessimisticEstimate' in tbtResult) {
return this.computeImpactsFromLantern(
tasks,
tbtResult.pessimisticEstimate.nodeTimings,
startTimeMs,
endTimeMs
);
}
return this.computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs);
}
}
const TBTImpactTasksComputed = makeComputedArtifact(
TBTImpactTasks,
['trace', 'devtoolsLog', 'URL', 'gatherContext', 'settings', 'simulator']
);
export {TBTImpactTasksComputed as TBTImpactTasks};

62
node_modules/lighthouse/core/computed/unused-css.d.ts generated vendored Normal file
View File

@@ -0,0 +1,62 @@
export { UnusedCSSComputed as UnusedCSS };
export type StyleSheetInfo = LH.Artifacts.CSSStyleSheetInfo & {
networkRecord: LH.Artifacts.NetworkRequest;
usedRules: Array<LH.Crdp.CSS.RuleUsage>;
};
declare const UnusedCSSComputed: typeof UnusedCSS & {
request: (dependencies: {
CSSUsage: LH.Artifacts['CSSUsage'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<import("../../types/audit.js").default.ByteEfficiencyItem[]>;
};
/** @typedef {LH.Artifacts.CSSStyleSheetInfo & {networkRecord: LH.Artifacts.NetworkRequest, usedRules: Array<LH.Crdp.CSS.RuleUsage>}} StyleSheetInfo */
declare class UnusedCSS {
/**
* @param {Array<LH.Artifacts.CSSStyleSheetInfo>} styles The output of the Styles gatherer.
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @return {Object<string, StyleSheetInfo>} A map of styleSheetId to stylesheet information.
*/
static indexStylesheetsById(styles: Array<LH.Artifacts.CSSStyleSheetInfo>, networkRecords: Array<LH.Artifacts.NetworkRequest>): {
[x: string]: StyleSheetInfo;
};
/**
* Adds used rules to their corresponding stylesheet.
* @param {Array<LH.Crdp.CSS.RuleUsage>} rules The output of the CSSUsage gatherer.
* @param {Object<string, StyleSheetInfo>} indexedStylesheets Stylesheet information indexed by id.
*/
static indexUsedRules(rules: Array<LH.Crdp.CSS.RuleUsage>, indexedStylesheets: {
[x: string]: StyleSheetInfo;
}): void;
/**
* @param {StyleSheetInfo} stylesheetInfo
* @return {{wastedBytes: number, totalBytes: number, wastedPercent: number}}
*/
static computeUsage(stylesheetInfo: StyleSheetInfo): {
wastedBytes: number;
totalBytes: number;
wastedPercent: number;
};
/**
* Trims stylesheet content down to the first rule-set definition.
* @param {string=} content
* @return {string}
*/
static determineContentPreview(content?: string | undefined): string;
/**
* @param {StyleSheetInfo} stylesheetInfo The stylesheetInfo object.
* @return {LH.Audit.ByteEfficiencyItem}
*/
static mapSheetToResult(stylesheetInfo: StyleSheetInfo): LH.Audit.ByteEfficiencyItem;
/**
* @param {{CSSUsage: LH.Artifacts['CSSUsage'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Audit.ByteEfficiencyItem[]>}
*/
static compute_(data: {
CSSUsage: LH.Artifacts['CSSUsage'];
devtoolsLog: import("../index.js").DevtoolsLog;
}, context: LH.Artifacts.ComputedContext): Promise<LH.Audit.ByteEfficiencyItem[]>;
}
//# sourceMappingURL=unused-css.d.ts.map

154
node_modules/lighthouse/core/computed/unused-css.js generated vendored Normal file
View File

@@ -0,0 +1,154 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {ByteEfficiencyAudit} from '../audits/byte-efficiency/byte-efficiency-audit.js';
import {NetworkRecords} from './network-records.js';
import {Util} from '../../shared/util.js';
const PREVIEW_LENGTH = 100;
/** @typedef {LH.Artifacts.CSSStyleSheetInfo & {networkRecord: LH.Artifacts.NetworkRequest, usedRules: Array<LH.Crdp.CSS.RuleUsage>}} StyleSheetInfo */
class UnusedCSS {
/**
* @param {Array<LH.Artifacts.CSSStyleSheetInfo>} styles The output of the Styles gatherer.
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @return {Object<string, StyleSheetInfo>} A map of styleSheetId to stylesheet information.
*/
static indexStylesheetsById(styles, networkRecords) {
const indexedNetworkRecords = networkRecords
// Some phantom network records appear with a 0 resourceSize that aren't real.
// A network record that has no size data is just as good as no network record at all for our
// purposes, so we'll just filter them out. https://github.com/GoogleChrome/lighthouse/issues/9684#issuecomment-532381611
.filter(record => record.resourceSize > 0)
.reduce((indexed, record) => {
indexed[record.url] = record;
return indexed;
}, /** @type {Object<string, LH.Artifacts.NetworkRequest>} */ ({}));
return styles.reduce((indexed, stylesheet) => {
indexed[stylesheet.header.styleSheetId] = Object.assign({
usedRules: [],
networkRecord: indexedNetworkRecords[stylesheet.header.sourceURL],
}, stylesheet);
return indexed;
}, /** @type {Object<string, StyleSheetInfo>} */ ({}));
}
/**
* Adds used rules to their corresponding stylesheet.
* @param {Array<LH.Crdp.CSS.RuleUsage>} rules The output of the CSSUsage gatherer.
* @param {Object<string, StyleSheetInfo>} indexedStylesheets Stylesheet information indexed by id.
*/
static indexUsedRules(rules, indexedStylesheets) {
rules.forEach(rule => {
const stylesheetInfo = indexedStylesheets[rule.styleSheetId];
if (!stylesheetInfo) {
return;
}
if (rule.used) {
stylesheetInfo.usedRules.push(rule);
}
});
}
/**
* @param {StyleSheetInfo} stylesheetInfo
* @return {{wastedBytes: number, totalBytes: number, wastedPercent: number}}
*/
static computeUsage(stylesheetInfo) {
let usedUncompressedBytes = 0;
const totalUncompressedBytes = stylesheetInfo.content.length;
for (const usedRule of stylesheetInfo.usedRules) {
usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
}
const totalTransferredBytes = ByteEfficiencyAudit.estimateTransferSize(
stylesheetInfo.networkRecord, totalUncompressedBytes, 'Stylesheet');
const percentUnused = (totalUncompressedBytes - usedUncompressedBytes) / totalUncompressedBytes;
const wastedBytes = Math.round(percentUnused * totalTransferredBytes);
return {
wastedBytes,
wastedPercent: percentUnused * 100,
totalBytes: totalTransferredBytes,
};
}
/**
* Trims stylesheet content down to the first rule-set definition.
* @param {string=} content
* @return {string}
*/
static determineContentPreview(content) {
let preview = Util.truncate(content || '', PREVIEW_LENGTH * 5, '')
.replace(/( {2,}|\t)+/g, ' ') // remove leading indentation if present
.replace(/\n\s+}/g, '\n}') // completely remove indentation of closing braces
.trim(); // trim the leading whitespace
if (preview.length > PREVIEW_LENGTH) {
const firstRuleStart = preview.indexOf('{');
const firstRuleEnd = preview.indexOf('}');
if (firstRuleStart === -1 || firstRuleEnd === -1 ||
firstRuleStart > firstRuleEnd ||
firstRuleStart > PREVIEW_LENGTH) {
// We couldn't determine the first rule-set or it's not within the preview
preview = Util.truncate(preview, PREVIEW_LENGTH);
} else if (firstRuleEnd < PREVIEW_LENGTH) {
// The entire first rule-set fits within the preview
preview = preview.slice(0, firstRuleEnd + 1) + ' …';
} else {
// The first rule-set doesn't fit within the preview, just show as many as we can
const truncated = Util.truncate(preview, PREVIEW_LENGTH, '');
const lastSemicolonIndex = truncated.lastIndexOf(';');
preview = lastSemicolonIndex < firstRuleStart ?
truncated + '… } …' :
preview.slice(0, lastSemicolonIndex + 1) + ' … } …';
}
}
return preview;
}
/**
* @param {StyleSheetInfo} stylesheetInfo The stylesheetInfo object.
* @return {LH.Audit.ByteEfficiencyItem}
*/
static mapSheetToResult(stylesheetInfo) {
let url = stylesheetInfo.header.sourceURL;
if (!url || stylesheetInfo.header.isInline) {
const contentPreview = UnusedCSS.determineContentPreview(stylesheetInfo.content);
url = contentPreview;
}
const usage = UnusedCSS.computeUsage(stylesheetInfo);
return {url, ...usage};
}
/**
* @param {{CSSUsage: LH.Artifacts['CSSUsage'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Audit.ByteEfficiencyItem[]>}
*/
static async compute_(data, context) {
const {CSSUsage, devtoolsLog} = data;
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const indexedSheets = UnusedCSS.indexStylesheetsById(CSSUsage.stylesheets, networkRecords);
UnusedCSS.indexUsedRules(CSSUsage.rules, indexedSheets);
const items = Object.keys(indexedSheets)
.map(sheetId => UnusedCSS.mapSheetToResult(indexedSheets[sheetId]));
return items;
}
}
const UnusedCSSComputed = makeComputedArtifact(UnusedCSS, ['CSSUsage', 'devtoolsLog']);
export {UnusedCSSComputed as UnusedCSS};

View File

@@ -0,0 +1,71 @@
export { UnusedJavascriptSummaryComputed as UnusedJavascriptSummary };
export type WasteData = {
unusedByIndex: Uint8Array;
unusedLength: number;
contentLength: number;
};
export type ComputeInput = {
scriptId: string;
scriptCoverage: Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>;
bundle?: LH.Artifacts.Bundle | undefined;
};
export type Summary = {
scriptId: string;
wastedBytes: number;
totalBytes: number;
wastedPercent?: number | undefined;
/**
* Keyed by file name. Includes (unmapped) key too.
*/
sourcesWastedBytes?: Record<string, number> | undefined;
};
declare const UnusedJavascriptSummaryComputed: typeof UnusedJavascriptSummary & {
request: (dependencies: ComputeInput, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<Summary>;
};
/**
* @typedef WasteData
* @property {Uint8Array} unusedByIndex
* @property {number} unusedLength
* @property {number} contentLength
*/
/**
* @typedef ComputeInput
* @property {string} scriptId
* @property {Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>} scriptCoverage
* @property {LH.Artifacts.Bundle=} bundle
*/
/**
* @typedef Summary
* @property {string} scriptId
* @property {number} wastedBytes
* @property {number} totalBytes
* @property {number} wastedBytes
* @property {number=} wastedPercent
* @property {Record<string, number>=} sourcesWastedBytes Keyed by file name. Includes (unmapped) key too.
*/
declare class UnusedJavascriptSummary {
/**
* @param {Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>} scriptCoverage
* @return {WasteData}
*/
static computeWaste(scriptCoverage: Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>): WasteData;
/**
* @param {string} scriptId
* @param {WasteData} wasteData
* @return {Summary}
*/
static createItem(scriptId: string, wasteData: WasteData): Summary;
/**
* @param {WasteData} wasteData
* @param {LH.Artifacts.Bundle} bundle
*/
static createSourceWastedBytes(wasteData: WasteData, bundle: LH.Artifacts.Bundle): Record<string, number> | undefined;
/**
* @param {ComputeInput} data
* @return {Promise<Summary>}
*/
static compute_(data: ComputeInput): Promise<Summary>;
}
//# sourceMappingURL=unused-javascript-summary.d.ts.map

View File

@@ -0,0 +1,155 @@
/**
* @license Copyright 2020 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
/**
* @typedef WasteData
* @property {Uint8Array} unusedByIndex
* @property {number} unusedLength
* @property {number} contentLength
*/
/**
* @typedef ComputeInput
* @property {string} scriptId
* @property {Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>} scriptCoverage
* @property {LH.Artifacts.Bundle=} bundle
*/
/**
* @typedef Summary
* @property {string} scriptId
* @property {number} wastedBytes
* @property {number} totalBytes
* @property {number} wastedBytes
* @property {number=} wastedPercent
* @property {Record<string, number>=} sourcesWastedBytes Keyed by file name. Includes (unmapped) key too.
*/
class UnusedJavascriptSummary {
/**
* @param {Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>} scriptCoverage
* @return {WasteData}
*/
static computeWaste(scriptCoverage) {
let maximumEndOffset = 0;
for (const func of scriptCoverage.functions) {
maximumEndOffset = Math.max(maximumEndOffset, ...func.ranges.map(r => r.endOffset));
}
// We only care about unused ranges of the script, so we can ignore all the nesting and safely
// assume that if a range is unexecuted, all nested ranges within it will also be unexecuted.
const unusedByIndex = new Uint8Array(maximumEndOffset);
for (const func of scriptCoverage.functions) {
for (const range of func.ranges) {
if (range.count === 0) {
for (let i = range.startOffset; i < range.endOffset; i++) {
unusedByIndex[i] = 1;
}
}
}
}
let unused = 0;
for (const x of unusedByIndex) {
unused += x;
}
return {
unusedByIndex,
unusedLength: unused,
contentLength: maximumEndOffset,
};
}
/**
* @param {string} scriptId
* @param {WasteData} wasteData
* @return {Summary}
*/
static createItem(scriptId, wasteData) {
const wastedRatio = (wasteData.unusedLength / wasteData.contentLength) || 0;
const wastedBytes = Math.round(wasteData.contentLength * wastedRatio);
return {
scriptId,
totalBytes: wasteData.contentLength,
wastedBytes,
wastedPercent: 100 * wastedRatio,
};
}
/**
* @param {WasteData} wasteData
* @param {LH.Artifacts.Bundle} bundle
*/
static createSourceWastedBytes(wasteData, bundle) {
if (!bundle.script.content) return;
/** @type {Record<string, number>} */
const files = {};
const lineLengths = bundle.script.content.split('\n').map(l => l.length);
let totalSoFar = 0;
const lineOffsets = lineLengths.map(len => {
const retVal = totalSoFar;
totalSoFar += len + 1;
return retVal;
});
// @ts-expect-error: We will upstream computeLastGeneratedColumns to CDT eventually.
bundle.map.computeLastGeneratedColumns();
for (const mapping of bundle.map.mappings()) {
let offset = lineOffsets[mapping.lineNumber];
offset += mapping.columnNumber;
const lastColumnOfMapping = mapping.lastColumnNumber !== undefined ?
mapping.lastColumnNumber - 1 :
lineLengths[mapping.lineNumber];
for (let i = mapping.columnNumber; i <= lastColumnOfMapping; i++) {
if (wasteData.unusedByIndex[offset] === 1) {
const key = mapping.sourceURL || '(unmapped)';
files[key] = (files[key] || 0) + 1;
}
offset += 1;
}
}
const dataSorted = Object.entries(files)
.sort(([_, unusedBytes1], [__, unusedBytes2]) => unusedBytes2 - unusedBytes1);
/** @type {Record<string, number>} */
const bundleData = {};
for (const [key, unusedBytes] of dataSorted) {
bundleData[key] = unusedBytes;
}
return bundleData;
}
/**
* @param {ComputeInput} data
* @return {Promise<Summary>}
*/
static async compute_(data) {
const {scriptId, scriptCoverage, bundle} = data;
const wasteData = UnusedJavascriptSummary.computeWaste(scriptCoverage);
const item = UnusedJavascriptSummary.createItem(scriptId, wasteData);
if (!bundle) return item;
return {
...item,
sourcesWastedBytes: UnusedJavascriptSummary.createSourceWastedBytes(wasteData, bundle),
};
}
}
const UnusedJavascriptSummaryComputed = makeComputedArtifact(
UnusedJavascriptSummary,
['bundle', 'scriptCoverage', 'scriptId']
);
export {UnusedJavascriptSummaryComputed as UnusedJavascriptSummary};

View File

@@ -0,0 +1,31 @@
export { UserTimingsComputed as UserTimings };
export type MarkEvent = {
name: string;
isMark: true;
args: LH.TraceEvent['args'];
startTime: number;
};
export type MeasureEvent = {
name: string;
isMark: false;
args: LH.TraceEvent['args'];
startTime: number;
endTime: number;
duration: number;
};
declare const UserTimingsComputed: typeof UserTimings & {
request: (dependencies: import("../index.js").Trace, context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<(MarkEvent | MeasureEvent)[]>;
};
/** @typedef {{name: string, isMark: true, args: LH.TraceEvent['args'], startTime: number}} MarkEvent */
/** @typedef {{name: string, isMark: false, args: LH.TraceEvent['args'], startTime: number, endTime: number, duration: number}} MeasureEvent */
declare class UserTimings {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Array<MarkEvent|MeasureEvent>>}
*/
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<Array<MarkEvent | MeasureEvent>>;
}
//# sourceMappingURL=user-timings.d.ts.map

83
node_modules/lighthouse/core/computed/user-timings.js generated vendored Normal file
View File

@@ -0,0 +1,83 @@
/**
* @license Copyright 2018 The Lighthouse Authors. 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.
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {ProcessedTrace} from './processed-trace.js';
/** @typedef {{name: string, isMark: true, args: LH.TraceEvent['args'], startTime: number}} MarkEvent */
/** @typedef {{name: string, isMark: false, args: LH.TraceEvent['args'], startTime: number, endTime: number, duration: number}} MeasureEvent */
class UserTimings {
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Array<MarkEvent|MeasureEvent>>}
*/
static async compute_(trace, context) {
const processedTrace = await ProcessedTrace.request(trace, context);
/** @type {Array<MarkEvent|MeasureEvent>} */
const userTimings = [];
/** @type {Record<string, number>} */
const measuresStartTimes = {};
// Get all blink.user_timing events
// The event phases we are interested in are mark and instant events (R, i, I)
// and duration events which correspond to measures (B, b, E, e).
// @see https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#
processedTrace.processEvents.filter(evt => {
if (!evt.cat.includes('blink.user_timing')) {
return false;
}
// reject these "userTiming" events that aren't really UserTiming, by nuking ones with frame data (or requestStart)
// https://cs.chromium.org/search/?q=trace_event.*?user_timing&sq=package:chromium&type=cs
return evt.name !== 'requestStart' &&
evt.name !== 'navigationStart' &&
evt.name !== 'paintNonDefaultBackgroundColor' &&
evt.args.frame === undefined;
})
.forEach(ut => {
// Mark events fall under phases R and I (or i)
if (ut.ph === 'R' || ut.ph.toUpperCase() === 'I') {
userTimings.push({
name: ut.name,
isMark: true,
args: ut.args,
startTime: ut.ts,
});
// Beginning of measure event, keep track of this events start time
} else if (ut.ph.toLowerCase() === 'b') {
measuresStartTimes[ut.name] = ut.ts;
// End of measure event
} else if (ut.ph.toLowerCase() === 'e') {
userTimings.push({
name: ut.name,
isMark: false,
args: ut.args,
startTime: measuresStartTimes[ut.name],
endTime: ut.ts,
duration: ut.ts - measuresStartTimes[ut.name],
});
}
});
// baseline the timestamps against the timeOrigin, and translate to milliseconds
userTimings.forEach(ut => {
ut.startTime = (ut.startTime - processedTrace.timeOriginEvt.ts) / 1000;
if (!ut.isMark) {
ut.endTime = (ut.endTime - processedTrace.timeOriginEvt.ts) / 1000;
ut.duration = ut.duration / 1000;
}
});
return userTimings;
}
}
const UserTimingsComputed = makeComputedArtifact(UserTimings, null);
export {UserTimingsComputed as UserTimings};

View File

@@ -0,0 +1,35 @@
export { ViewportMetaComputed as ViewportMeta };
export type ViewportMetaResult = {
/**
* Whether the page has any viewport tag.
*/
hasViewportTag: boolean;
/**
* Whether the viewport tag is optimized for mobile screens.
*/
isMobileOptimized: boolean;
/**
* Warnings if the parser encountered invalid content in the viewport tag.
*/
parserWarnings: Array<string>;
};
declare const ViewportMetaComputed: typeof ViewportMeta & {
request: (dependencies: {
name?: string | undefined;
content?: string | undefined;
property?: string | undefined;
httpEquiv?: string | undefined;
charset?: string | undefined;
node: import("../index.js").Artifacts.NodeDetails;
}[], context: import("../../types/utility-types.js").default.ImmutableObject<{
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
}>) => Promise<ViewportMetaResult>;
};
declare class ViewportMeta {
/**
* @param {LH.GathererArtifacts['MetaElements']} MetaElements
* @return {Promise<ViewportMetaResult>}
*/
static compute_(MetaElements: LH.GathererArtifacts['MetaElements']): Promise<ViewportMetaResult>;
}
//# sourceMappingURL=viewport-meta.d.ts.map

56
node_modules/lighthouse/core/computed/viewport-meta.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
/**
* @license Copyright 2019 The Lighthouse Authors. 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.
*/
import Parser from 'metaviewport-parser';
import {makeComputedArtifact} from './computed-artifact.js';
class ViewportMeta {
/**
* @param {LH.GathererArtifacts['MetaElements']} MetaElements
* @return {Promise<ViewportMetaResult>}
*/
static async compute_(MetaElements) {
const viewportMeta = MetaElements.find(meta => meta.name === 'viewport');
if (!viewportMeta) {
return {
hasViewportTag: false,
isMobileOptimized: false,
parserWarnings: [],
};
}
const warnings = [];
const parsedProps = Parser.parseMetaViewPortContent(viewportMeta.content || '');
if (Object.keys(parsedProps.unknownProperties).length) {
warnings.push(`Invalid properties found: ${JSON.stringify(parsedProps.unknownProperties)}`);
}
if (Object.keys(parsedProps.invalidValues).length) {
warnings.push(`Invalid values found: ${JSON.stringify(parsedProps.invalidValues)}`);
}
const viewportProps = parsedProps.validProperties;
const isMobileOptimized = Boolean(viewportProps.width || viewportProps['initial-scale']);
return {
hasViewportTag: true,
isMobileOptimized,
parserWarnings: warnings,
};
}
}
const ViewportMetaComputed = makeComputedArtifact(ViewportMeta, null);
export {ViewportMetaComputed as ViewportMeta};
/**
* @typedef {object} ViewportMetaResult
* @property {boolean} hasViewportTag Whether the page has any viewport tag.
* @property {boolean} isMobileOptimized Whether the viewport tag is optimized for mobile screens.
* @property {Array<string>} parserWarnings Warnings if the parser encountered invalid content in the viewport tag.
*/