lighthouse 13.5.0-dev.20261021 → 13.5.0-dev.20261023
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/cli-flags.js +4 -3
- package/cli/run.js +1 -1
- package/cli/test/smokehouse/smokehouse-test.d.ts +2 -0
- package/cli/test/smokehouse/smokehouse-test.js +120 -0
- package/cli/test/smokehouse/smokehouse.js +11 -3
- package/core/audits/script-treemap-data.js +18 -8
- package/core/computed/metrics/lantern-metric.js +1 -1
- package/core/computed/page-dependency-graph.js +55 -1
- package/core/computed/unused-css.js +15 -2
- package/core/config/lr-desktop-config.js +13 -0
- package/core/config/lr-mobile-config.js +13 -0
- package/core/gather/gatherers/trace.d.ts +6 -0
- package/core/gather/gatherers/trace.js +56 -16
- package/core/lib/arbitrary-equality-map.d.ts +2 -2
- package/core/lib/arbitrary-equality-map.js +46 -6
- package/core/lib/legacy-javascript/legacy-javascript.js +2 -2
- package/core/lib/network-request.js +3 -0
- package/dist/report/bundle.esm.js +1 -1
- package/dist/report/flow.js +1 -1
- package/dist/report/standalone.js +1 -1
- package/package.json +1 -1
- package/readme.md +2 -1
- package/report/generator/file-namer.js +1 -1
package/cli/cli-flags.js
CHANGED
|
@@ -232,7 +232,8 @@ function getYargsParser(manualArgv) {
|
|
|
232
232
|
'output-path': {
|
|
233
233
|
type: 'string',
|
|
234
234
|
coerce: coerceOutputPath,
|
|
235
|
-
describe: `The file path to output the results. Use 'stdout' to write to stdout.
|
|
235
|
+
describe: `The file or directory path to output the results. Use 'stdout' to write to stdout.
|
|
236
|
+
Directories in the path must already exist. If a directory is specified, the file name will be based on the test URL and date.
|
|
236
237
|
If using JSON output, default is stdout.
|
|
237
238
|
If using HTML or CSV output, default is a file in the working directory with a name based on the test URL and date.
|
|
238
239
|
If using multiple outputs, --output-path is appended with the standard extension for each output type. "reports/my-run" -> "reports/my-run.report.html", "reports/my-run.report.json", etc.
|
|
@@ -327,7 +328,7 @@ Example: --output-path=./lighthouse-results.html`,
|
|
|
327
328
|
if (
|
|
328
329
|
typeof argv.outputPath === 'string' &&
|
|
329
330
|
argv.outputPath &&
|
|
330
|
-
(!fs.existsSync(argv.outputPath) || !fs.
|
|
331
|
+
(!fs.existsSync(argv.outputPath) || !fs.statSync(argv.outputPath).isDirectory())
|
|
331
332
|
) {
|
|
332
333
|
throw new Error('--output-path must be an existing directory when auditing multiple URLs');
|
|
333
334
|
}
|
|
@@ -458,7 +459,7 @@ function coerceUrlFile(value) {
|
|
|
458
459
|
typeof value !== 'string' ||
|
|
459
460
|
!value ||
|
|
460
461
|
!fs.existsSync(value) ||
|
|
461
|
-
fs.
|
|
462
|
+
fs.statSync(value).isDirectory()
|
|
462
463
|
) {
|
|
463
464
|
throw new Error(`--url-file (${value}) cannot be read`);
|
|
464
465
|
}
|
package/cli/run.js
CHANGED
|
@@ -149,7 +149,7 @@ async function saveResults(runnerResult, flags) {
|
|
|
149
149
|
const isDirectoryOutput = Boolean(
|
|
150
150
|
flags.outputPath &&
|
|
151
151
|
fs.existsSync(flags.outputPath) &&
|
|
152
|
-
fs.
|
|
152
|
+
fs.statSync(flags.outputPath).isDirectory()
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
// Use the output path as the prefix for all generated files.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as td from 'testdouble';
|
|
8
|
+
|
|
9
|
+
import {runSmokehouse} from './smokehouse.js';
|
|
10
|
+
import {ChildProcessError} from './lib/child-process-error.js';
|
|
11
|
+
|
|
12
|
+
/** @type {Smokehouse.TestDfn} */
|
|
13
|
+
const smokeTestDefn = {
|
|
14
|
+
id: 'test-smoke',
|
|
15
|
+
expectations: {
|
|
16
|
+
lhr: {
|
|
17
|
+
requestedUrl: 'http://localhost:10200/simple-page.html',
|
|
18
|
+
finalDisplayedUrl: 'http://localhost:10200/simple-page.html',
|
|
19
|
+
audits: {},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('runSmokehouse error logging', () => {
|
|
25
|
+
/** @type {string[]} */
|
|
26
|
+
let loggedMessages;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
loggedMessages = [];
|
|
30
|
+
td.replace(console, 'log', (/** @type {unknown[]} */ ...args) => {
|
|
31
|
+
loggedMessages.push(args.map(String).join(' '));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
td.reset();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('logs runner logger output when lighthouseRunner returns a malformed result', async () => {
|
|
40
|
+
/** @type {Smokehouse.LighthouseRunner} */
|
|
41
|
+
// @ts-expect-error - testing malformed runner result
|
|
42
|
+
const lighthouseRunner = async (_url, _config, logger) => {
|
|
43
|
+
logger?.log('runner progress before malformed result');
|
|
44
|
+
return {lhr: {}, artifacts: undefined};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const {success, testResults} = await runSmokehouse([smokeTestDefn], {
|
|
48
|
+
lighthouseRunner,
|
|
49
|
+
retries: 0,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(success).toBe(false);
|
|
53
|
+
expect(testResults).toHaveLength(1);
|
|
54
|
+
expect(testResults[0].failed).toBe(1);
|
|
55
|
+
|
|
56
|
+
const combinedOutput = loggedMessages.join('\n');
|
|
57
|
+
expect(combinedOutput).toContain('lighthouse runner returned a bad result');
|
|
58
|
+
expect(combinedOutput).toContain('runner progress before malformed result');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('logs runner logger output when lighthouseRunner throws a regular Error', async () => {
|
|
62
|
+
/** @type {Smokehouse.LighthouseRunner} */
|
|
63
|
+
const lighthouseRunner = async (_url, _config, logger) => {
|
|
64
|
+
logger?.log('runner progress before throwing');
|
|
65
|
+
throw new Error('unexpected runner failure');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const {success, testResults} = await runSmokehouse([smokeTestDefn], {
|
|
69
|
+
lighthouseRunner,
|
|
70
|
+
retries: 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(success).toBe(false);
|
|
74
|
+
expect(testResults).toHaveLength(1);
|
|
75
|
+
expect(testResults[0].failed).toBe(1);
|
|
76
|
+
|
|
77
|
+
const combinedOutput = loggedMessages.join('\n');
|
|
78
|
+
expect(combinedOutput).toContain('unexpected runner failure');
|
|
79
|
+
expect(combinedOutput).toContain('runner progress before throwing');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not duplicate runner logger output for ChildProcessError', async () => {
|
|
83
|
+
/** @type {Smokehouse.LighthouseRunner} */
|
|
84
|
+
const lighthouseRunner = async (_url, _config, logger) => {
|
|
85
|
+
logger?.log('unique-cli-runner-log-line');
|
|
86
|
+
throw new ChildProcessError('Lighthouse run failed', logger?.getLog());
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const {success} = await runSmokehouse([smokeTestDefn], {
|
|
90
|
+
lighthouseRunner,
|
|
91
|
+
retries: 0,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(success).toBe(false);
|
|
95
|
+
|
|
96
|
+
const combinedOutput = loggedMessages.join('\n');
|
|
97
|
+
const occurrences = combinedOutput.split('unique-cli-runner-log-line').length - 1;
|
|
98
|
+
expect(occurrences).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not duplicate runner logger output when Error message already includes it', async () => {
|
|
102
|
+
/** @type {Smokehouse.LighthouseRunner} */
|
|
103
|
+
const lighthouseRunner = async (_url, _config, logger) => {
|
|
104
|
+
logger?.log('unique-worker-runner-log-line');
|
|
105
|
+
const log = logger?.getLog() || '';
|
|
106
|
+
throw new Error(`Worker returned an error: boom\nLog:\n${log}\n`);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const {success} = await runSmokehouse([smokeTestDefn], {
|
|
110
|
+
lighthouseRunner,
|
|
111
|
+
retries: 0,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(success).toBe(false);
|
|
115
|
+
|
|
116
|
+
const combinedOutput = loggedMessages.join('\n');
|
|
117
|
+
const occurrences = combinedOutput.split('unique-worker-runner-log-line').length - 1;
|
|
118
|
+
expect(occurrences).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -190,10 +190,10 @@ async function runSmokeTest(smokeTestDefn, testOptions) {
|
|
|
190
190
|
clearTimeout(timeoutId);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
if (!timedResult
|
|
193
|
+
if (!timedResult?.lhr?.audits || !timedResult.artifacts) {
|
|
194
194
|
// Something went really wrong and the runner didn't catch it.
|
|
195
195
|
throw new Error('lighthouse runner returned a bad result. got lhr:\n' +
|
|
196
|
-
JSON.stringify(timedResult
|
|
196
|
+
JSON.stringify(timedResult?.lhr, null, 2));
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
result = {
|
|
@@ -206,9 +206,17 @@ async function runSmokeTest(smokeTestDefn, testOptions) {
|
|
|
206
206
|
if (takeNetworkRequestUrls) takeNetworkRequestUrls();
|
|
207
207
|
|
|
208
208
|
logChildProcessError(bufferedConsole, e);
|
|
209
|
+
const runnerLog = logger.getLog();
|
|
209
210
|
if (isTimeout) {
|
|
210
211
|
bufferedConsole.log('Timed out. log from lighthouseRunner:');
|
|
211
|
-
bufferedConsole.log(
|
|
212
|
+
bufferedConsole.log(runnerLog);
|
|
213
|
+
} else if (
|
|
214
|
+
!(e instanceof ChildProcessError) &&
|
|
215
|
+
runnerLog &&
|
|
216
|
+
!e.message?.includes(runnerLog)
|
|
217
|
+
) {
|
|
218
|
+
bufferedConsole.log('log from lighthouseRunner:');
|
|
219
|
+
bufferedConsole.log(runnerLog);
|
|
212
220
|
}
|
|
213
221
|
continue; // Retry, if possible.
|
|
214
222
|
}
|
|
@@ -178,21 +178,31 @@ class ScriptTreemapDataAudit extends Audit {
|
|
|
178
178
|
const htmlNodesByFrameId = new Map();
|
|
179
179
|
/** @type {Set<LH.Artifacts.NetworkRequest>} */
|
|
180
180
|
const seenNetworkRecords = new Set();
|
|
181
|
-
const
|
|
182
|
-
|
|
181
|
+
const deduplicatedScripts = artifacts.Scripts.filter(script => {
|
|
182
|
+
if (script.scriptLanguage !== 'JavaScript') return false;
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
if (script.scriptLanguage !== 'JavaScript') continue;
|
|
186
|
-
|
|
187
|
-
const isInlineScript = isInline(script);
|
|
188
|
-
const networkRecord = isInlineScript ?
|
|
184
|
+
const networkRecord = isInline(script) ?
|
|
189
185
|
undefined :
|
|
190
186
|
getRequestForScript(networkRecords, script);
|
|
191
187
|
if (networkRecord) {
|
|
192
|
-
if (seenNetworkRecords.has(networkRecord))
|
|
188
|
+
if (seenNetworkRecords.has(networkRecord)) return false;
|
|
193
189
|
seenNetworkRecords.add(networkRecord);
|
|
194
190
|
}
|
|
195
191
|
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
const deduplicatedArtifacts = deduplicatedScripts.length === artifacts.Scripts.length ?
|
|
195
|
+
artifacts :
|
|
196
|
+
{...artifacts, Scripts: deduplicatedScripts};
|
|
197
|
+
const bundles = await JSBundles.request(deduplicatedArtifacts, context);
|
|
198
|
+
const duplicationByPath = await ModuleDuplication.request(deduplicatedArtifacts, context);
|
|
199
|
+
|
|
200
|
+
for (const script of deduplicatedScripts) {
|
|
201
|
+
const isInlineScript = isInline(script);
|
|
202
|
+
const networkRecord = isInlineScript ?
|
|
203
|
+
undefined :
|
|
204
|
+
getRequestForScript(networkRecords, script);
|
|
205
|
+
|
|
196
206
|
const name = script.url;
|
|
197
207
|
const bundle = bundles.find(bundle => script.scriptId === bundle.script.scriptId) ?? null;
|
|
198
208
|
const scriptCoverage = /** @type {LH.Artifacts['JsUsage'][string] | undefined} */
|
|
@@ -40,7 +40,7 @@ async function getComputationDataParamsFromTrace(data, context) {
|
|
|
40
40
|
const traceEngineResult = await TraceEngineResult.request(data, context);
|
|
41
41
|
const frameId = traceEngineResult.data.Meta.mainFrameId;
|
|
42
42
|
const navigation =
|
|
43
|
-
traceEngineResult.data.Meta.mainFrameNavigations
|
|
43
|
+
traceEngineResult.data.Meta.mainFrameNavigations.at(-1);
|
|
44
44
|
if (!navigation) {
|
|
45
45
|
throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
|
|
46
46
|
}
|
|
@@ -11,6 +11,26 @@ import {ProcessedTrace} from './processed-trace.js';
|
|
|
11
11
|
import {NetworkRecords} from './network-records.js';
|
|
12
12
|
import {TraceEngineResult} from './trace-engine-result.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* ResourceSendRequest URLs in the trace retain #hash fragments, whereas CDP
|
|
16
|
+
* Network.requestWillBeSent, Page.frameNavigated, initiator.url, documentURL,
|
|
17
|
+
* and V8 script events strip #hash fragments, and Lantern's NetworkAnalyzer
|
|
18
|
+
* requires request.url not to have a fragment.
|
|
19
|
+
* TODO: Remove once @paulirish/trace_engine includes the fix in LanternComputationData.
|
|
20
|
+
* @param {string} urlStr
|
|
21
|
+
* @return {string}
|
|
22
|
+
*/
|
|
23
|
+
function stripUrlHash(urlStr) {
|
|
24
|
+
if (!urlStr.includes('#')) return urlStr;
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(urlStr);
|
|
27
|
+
url.hash = '';
|
|
28
|
+
return url.href;
|
|
29
|
+
} catch {
|
|
30
|
+
return urlStr;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
class PageDependencyGraph {
|
|
15
35
|
/**
|
|
16
36
|
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings'], URL: LH.Artifacts['URL'], SourceMaps: LH.Artifacts['SourceMaps'], HostDPR: LH.Artifacts['HostDPR'], fromTrace: boolean}} data
|
|
@@ -27,7 +47,41 @@ class PageDependencyGraph {
|
|
|
27
47
|
if (data.fromTrace) {
|
|
28
48
|
const traceEngineResult =
|
|
29
49
|
await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
|
|
30
|
-
|
|
50
|
+
let parsedTrace = traceEngineResult.data;
|
|
51
|
+
if (parsedTrace.NetworkRequests.byTime.some(
|
|
52
|
+
r => r.args.data.url.includes('#') ||
|
|
53
|
+
r.args.data.redirects.some(red => red.url.includes('#'))
|
|
54
|
+
)) {
|
|
55
|
+
const byTime = parsedTrace.NetworkRequests.byTime.map(request => {
|
|
56
|
+
const url = stripUrlHash(request.args.data.url);
|
|
57
|
+
const redirects = request.args.data.redirects.map(redirect => ({
|
|
58
|
+
...redirect,
|
|
59
|
+
url: stripUrlHash(redirect.url),
|
|
60
|
+
}));
|
|
61
|
+
if (url === request.args.data.url &&
|
|
62
|
+
redirects.every((red, i) => red.url === request.args.data.redirects[i].url)) {
|
|
63
|
+
return request;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...request,
|
|
67
|
+
args: {
|
|
68
|
+
...request.args,
|
|
69
|
+
data: {
|
|
70
|
+
...request.args.data,
|
|
71
|
+
url,
|
|
72
|
+
redirects,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
parsedTrace = {
|
|
78
|
+
...parsedTrace,
|
|
79
|
+
NetworkRequests: {
|
|
80
|
+
...parsedTrace.NetworkRequests,
|
|
81
|
+
byTime,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
31
85
|
const requests =
|
|
32
86
|
Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
|
|
33
87
|
const graph =
|
|
@@ -66,8 +66,21 @@ class UnusedCSS {
|
|
|
66
66
|
let usedUncompressedBytes = 0;
|
|
67
67
|
const totalUncompressedBytes = stylesheetInfo.content.length;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// CSS rule usage tracking can return overlapping ranges (e.g. a nested rule inside its
|
|
70
|
+
// parent rule's range) or duplicate ranges (parent rules are emitted once per used child rule).
|
|
71
|
+
// Sort and merge overlapping ranges so used bytes are only counted once.
|
|
72
|
+
const usedRules = stylesheetInfo.usedRules
|
|
73
|
+
.slice()
|
|
74
|
+
.sort((a, b) => a.startOffset - b.startOffset);
|
|
75
|
+
|
|
76
|
+
let lastEndOffset = -1;
|
|
77
|
+
for (const usedRule of usedRules) {
|
|
78
|
+
if (usedRule.endOffset <= lastEndOffset) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const startOffset = Math.max(usedRule.startOffset, lastEndOffset);
|
|
82
|
+
usedUncompressedBytes += usedRule.endOffset - startOffset;
|
|
83
|
+
lastEndOffset = usedRule.endOffset;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
const compressedSize = estimateCompressedContentSize(
|
|
@@ -24,6 +24,19 @@ const config = {
|
|
|
24
24
|
'bf-cache',
|
|
25
25
|
],
|
|
26
26
|
},
|
|
27
|
+
audits: [
|
|
28
|
+
// Lightrider does not support WebSocket connections.
|
|
29
|
+
// See https://github.com/GoogleChrome/lighthouse/issues/12459
|
|
30
|
+
{
|
|
31
|
+
path: 'errors-in-console',
|
|
32
|
+
options: {
|
|
33
|
+
ignoredPatterns: [
|
|
34
|
+
'ERR_BLOCKED_BY_CLIENT.Inspector',
|
|
35
|
+
/^WebSocket connection to 'wss?:\/\/.*' failed:/,
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
27
40
|
};
|
|
28
41
|
|
|
29
42
|
export default config;
|
|
@@ -24,6 +24,19 @@ const config = {
|
|
|
24
24
|
'bf-cache',
|
|
25
25
|
],
|
|
26
26
|
},
|
|
27
|
+
audits: [
|
|
28
|
+
// Lightrider does not support WebSocket connections.
|
|
29
|
+
// See https://github.com/GoogleChrome/lighthouse/issues/12459
|
|
30
|
+
{
|
|
31
|
+
path: 'errors-in-console',
|
|
32
|
+
options: {
|
|
33
|
+
ignoredPatterns: [
|
|
34
|
+
'ERR_BLOCKED_BY_CLIENT.Inspector',
|
|
35
|
+
/^WebSocket connection to 'wss?:\/\/.*' failed:/,
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
27
40
|
};
|
|
28
41
|
|
|
29
42
|
export default config;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export default Trace;
|
|
2
2
|
declare class Trace extends BaseGatherer {
|
|
3
3
|
static getDefaultTraceCategories(): string[];
|
|
4
|
+
/**
|
|
5
|
+
* @param {LH.Gatherer.ProtocolSession} session
|
|
6
|
+
* @param {string} handle
|
|
7
|
+
* @return {Promise<LH.Trace>}
|
|
8
|
+
*/
|
|
9
|
+
static _readTraceFromStream(session: LH.Gatherer.ProtocolSession, handle: string): Promise<LH.Trace>;
|
|
4
10
|
/**
|
|
5
11
|
* @param {LH.Gatherer.ProtocolSession} session
|
|
6
12
|
* @return {Promise<LH.Trace>}
|
|
@@ -73,29 +73,68 @@ class Trace extends BaseGatherer {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* @param {LH.Gatherer.ProtocolSession} session
|
|
76
|
+
* @param {string} handle
|
|
76
77
|
* @return {Promise<LH.Trace>}
|
|
77
78
|
*/
|
|
78
|
-
static async
|
|
79
|
+
static async _readTraceFromStream(session, handle) {
|
|
79
80
|
/** @type {Array<LH.TraceEvent>} */
|
|
80
81
|
const traceEvents = [];
|
|
82
|
+
const decoder = new TextDecoder();
|
|
83
|
+
let isEof = false;
|
|
84
|
+
let remainder = '';
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
while (!isEof) {
|
|
88
|
+
const response = await session.sendCommand('IO.read', {handle});
|
|
89
|
+
isEof = Boolean(response.eof);
|
|
90
|
+
remainder += response.base64Encoded ?
|
|
91
|
+
decoder.decode(Buffer.from(response.data, 'base64'), {stream: !isEof}) :
|
|
92
|
+
response.data;
|
|
93
|
+
|
|
94
|
+
// Perfetto's `ExportJson` serializes each trace event and trailing `metadata`
|
|
95
|
+
// as compact single-line JSON (with string newlines escaped as `\\n`), emitting
|
|
96
|
+
// raw `,\n` only between top-level elements of `traceEvents`.
|
|
97
|
+
// Maintain the invariant that `remainder` always starts with `{"traceEvents":[`.
|
|
98
|
+
const lastEventSep = remainder.lastIndexOf('},\n');
|
|
99
|
+
if (lastEventSep !== -1) {
|
|
100
|
+
const {traceEvents: batch} = JSON.parse(`${remainder.slice(0, lastEventSep + 1)}]}`);
|
|
101
|
+
for (const event of batch) {
|
|
102
|
+
traceEvents.push(event);
|
|
103
|
+
}
|
|
104
|
+
remainder = `{"traceEvents":[${remainder.slice(lastEventSep + 3)}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
await session.sendCommand('IO.close', {handle});
|
|
109
|
+
}
|
|
81
110
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
session.on('Tracing.dataCollected', dataListener);
|
|
90
|
-
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
session.once('Tracing.tracingComplete', _ => {
|
|
93
|
-
session.off('Tracing.dataCollected', dataListener);
|
|
94
|
-
resolve({traceEvents});
|
|
95
|
-
});
|
|
111
|
+
const traceJson = JSON.parse(remainder);
|
|
112
|
+
for (const event of traceJson.traceEvents) {
|
|
113
|
+
traceEvents.push(event);
|
|
114
|
+
}
|
|
115
|
+
traceJson.traceEvents = traceEvents;
|
|
116
|
+
return traceJson;
|
|
117
|
+
}
|
|
96
118
|
|
|
97
|
-
|
|
119
|
+
/**
|
|
120
|
+
* @param {LH.Gatherer.ProtocolSession} session
|
|
121
|
+
* @return {Promise<LH.Trace>}
|
|
122
|
+
*/
|
|
123
|
+
static async endTraceAndCollectEvents(session) {
|
|
124
|
+
/** @type {LH.Crdp.Tracing.TracingCompleteEvent} */
|
|
125
|
+
const completeEvent = await new Promise((resolve, reject) => {
|
|
126
|
+
session.once('Tracing.tracingComplete', resolve);
|
|
127
|
+
session.sendCommand('Tracing.end').catch(err => {
|
|
128
|
+
session.off('Tracing.tracingComplete', resolve);
|
|
129
|
+
reject(err);
|
|
130
|
+
});
|
|
98
131
|
});
|
|
132
|
+
|
|
133
|
+
if (!completeEvent.stream) {
|
|
134
|
+
throw new Error('Tracing.tracingComplete did not return a stream handle');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Trace._readTraceFromStream(session, completeEvent.stream);
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
static symbol = Symbol('Trace');
|
|
@@ -115,6 +154,7 @@ class Trace extends BaseGatherer {
|
|
|
115
154
|
await driver.defaultSession.sendCommand('Page.enable');
|
|
116
155
|
await driver.defaultSession.sendCommand('Tracing.start', {
|
|
117
156
|
categories: traceCategories.join(','),
|
|
157
|
+
transferMode: 'ReturnAsStream',
|
|
118
158
|
options: 'sampling-frequency=10000', // 1000 is default and too slow.
|
|
119
159
|
});
|
|
120
160
|
|
|
@@ -50,9 +50,9 @@ export class ArbitraryEqualityMap {
|
|
|
50
50
|
/**
|
|
51
51
|
* @param {*} a
|
|
52
52
|
* @param {*} b
|
|
53
|
-
* @param {WeakMap<object, object
|
|
53
|
+
* @param {WeakMap<object, WeakSet<object>>=} seen
|
|
54
54
|
* @return {boolean}
|
|
55
55
|
*/
|
|
56
|
-
declare function deepEquals(a: any, b: any, seen?: WeakMap<object, object
|
|
56
|
+
declare function deepEquals(a: any, b: any, seen?: WeakMap<object, WeakSet<object>> | undefined): boolean;
|
|
57
57
|
export { deepEquals as isEqual };
|
|
58
58
|
//# sourceMappingURL=arbitrary-equality-map.d.ts.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {*} a
|
|
9
9
|
* @param {*} b
|
|
10
|
-
* @param {WeakMap<object, object
|
|
10
|
+
* @param {WeakMap<object, WeakSet<object>>=} seen
|
|
11
11
|
* @return {boolean}
|
|
12
12
|
*/
|
|
13
13
|
function deepEquals(a, b, seen) {
|
|
@@ -32,13 +32,27 @@ function deepEquals(a, b, seen) {
|
|
|
32
32
|
}
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
|
+
if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
|
|
36
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
37
|
+
const viewA = new Uint8Array(a);
|
|
38
|
+
const viewB = new Uint8Array(b);
|
|
39
|
+
for (let i = 0; i < viewA.length; i++) {
|
|
40
|
+
if (viewA[i] !== viewB[i]) return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
35
44
|
|
|
36
|
-
if (seen) {
|
|
37
|
-
if (seen.get(a) === b) return true;
|
|
38
|
-
} else {
|
|
45
|
+
if (!seen) {
|
|
39
46
|
seen = new WeakMap();
|
|
40
47
|
}
|
|
41
|
-
seen.
|
|
48
|
+
let seenB = seen.get(a);
|
|
49
|
+
if (seenB) {
|
|
50
|
+
if (seenB.has(b)) return true;
|
|
51
|
+
} else {
|
|
52
|
+
seenB = new WeakSet();
|
|
53
|
+
seen.set(a, seenB);
|
|
54
|
+
}
|
|
55
|
+
seenB.add(b);
|
|
42
56
|
|
|
43
57
|
if (Array.isArray(a)) {
|
|
44
58
|
const arrB = /** @type {unknown[]} */ (b);
|
|
@@ -59,12 +73,38 @@ function deepEquals(a, b, seen) {
|
|
|
59
73
|
|
|
60
74
|
if (a instanceof Set && b instanceof Set) {
|
|
61
75
|
if (a.size !== b.size) return false;
|
|
76
|
+
/** @type {unknown[]} */
|
|
77
|
+
const unmatchedA = [];
|
|
62
78
|
for (const v of a) {
|
|
63
|
-
if (!b.has(v))
|
|
79
|
+
if (!b.has(v)) {
|
|
80
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
81
|
+
unmatchedA.push(v);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (unmatchedA.length === 0) return true;
|
|
85
|
+
/** @type {unknown[]} */
|
|
86
|
+
const unmatchedB = [];
|
|
87
|
+
for (const v of b) {
|
|
88
|
+
if (!a.has(v)) {
|
|
89
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
90
|
+
unmatchedB.push(v);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (unmatchedA.length !== unmatchedB.length) return false;
|
|
94
|
+
for (const itemA of unmatchedA) {
|
|
95
|
+
const matchIndex = unmatchedB.findIndex(itemB => deepEquals(itemA, itemB, seen));
|
|
96
|
+
if (matchIndex === -1) return false;
|
|
97
|
+
unmatchedB.splice(matchIndex, 1);
|
|
64
98
|
}
|
|
65
99
|
return true;
|
|
66
100
|
}
|
|
67
101
|
|
|
102
|
+
if (a instanceof Error && b instanceof Error) {
|
|
103
|
+
if (a.name !== b.name || a.message !== b.message || !deepEquals(a.cause, b.cause, seen)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
68
108
|
const keysA = Object.keys(a);
|
|
69
109
|
const keysB = Object.keys(b);
|
|
70
110
|
if (keysA.length !== keysB.length) return false;
|
|
@@ -189,8 +189,8 @@ function getPolyfillPatterns() {
|
|
|
189
189
|
// 4. es-shims: `no(Object,{entries:r},{entries:function`
|
|
190
190
|
const esShimsBranches = [];
|
|
191
191
|
for (const [object, props] of byObject) {
|
|
192
|
-
const
|
|
193
|
-
esShimsBranches.push(`${escapeRegExp(object)},\\s*{
|
|
192
|
+
const propBranches = props.map(p => `${p}:[^};]*},\\s*{${p}\\b`).join('|');
|
|
193
|
+
esShimsBranches.push(`${escapeRegExp(object)},\\s*{(?:${propBranches})`);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// 5. core-js@3 minified pattern:
|
|
@@ -345,6 +345,9 @@ class NetworkRequest {
|
|
|
345
345
|
this.networkEndTime = data.timestamp * 1000;
|
|
346
346
|
|
|
347
347
|
this._updateResponseHeadersEndTimeIfNecessary();
|
|
348
|
+
this._updateProtocolForLightrider();
|
|
349
|
+
this._updateTransferSizeForLightrider();
|
|
350
|
+
this._updateTimingsForLightrider();
|
|
348
351
|
}
|
|
349
352
|
|
|
350
353
|
/**
|