browsertime 26.3.2 → 27.0.1
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/CHANGELOG.md +72 -0
- package/README.md +1 -1
- package/browserscripts/timings/softNavigations.js +94 -0
- package/lib/chrome/har.js +36 -5
- package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
- package/lib/chrome/webdriver/chromium.js +40 -1
- package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
- package/lib/core/engine/collector.js +340 -317
- package/lib/core/engine/command/addText.js +93 -12
- package/lib/core/engine/command/click.js +268 -139
- package/lib/core/engine/command/commandHelper.js +45 -0
- package/lib/core/engine/command/cookie.js +85 -0
- package/lib/core/engine/command/element.js +37 -2
- package/lib/core/engine/command/javaScript.js +14 -24
- package/lib/core/engine/command/measure/screenshots.js +114 -0
- package/lib/core/engine/command/measure/video.js +85 -0
- package/lib/core/engine/command/measure.js +104 -180
- package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
- package/lib/core/engine/command/mouse/contextClick.js +56 -4
- package/lib/core/engine/command/mouse/doubleClick.js +49 -1
- package/lib/core/engine/command/mouse/mouseMove.js +50 -1
- package/lib/core/engine/command/mouse/singleClick.js +165 -129
- package/lib/core/engine/command/navigation.js +6 -6
- package/lib/core/engine/command/select.js +95 -3
- package/lib/core/engine/command/selectorParser.js +58 -0
- package/lib/core/engine/command/set.js +55 -1
- package/lib/core/engine/command/wait.js +41 -2
- package/lib/core/engine/commands.js +550 -42
- package/lib/core/engine/index.js +206 -150
- package/lib/core/engine/iteration.js +277 -137
- package/lib/firefox/networkManager.js +22 -30
- package/lib/firefox/webdriver/builder.js +14 -0
- package/lib/safari/har.js +131 -0
- package/lib/safari/native/ios-capture +0 -0
- package/lib/safari/native/ios-capture.m +340 -0
- package/lib/safari/safariInspectorClient.js +322 -0
- package/lib/safari/webdriver/safari.js +303 -13
- package/lib/safari/webkitToCdpAdapter.js +241 -0
- package/lib/support/cli.js +13 -0
- package/lib/support/engineUtils.js +16 -5
- package/lib/support/getViewPort.js +5 -0
- package/lib/support/har/index.js +41 -3
- package/lib/support/usbPower.js +2 -3
- package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
- package/lib/video/postprocessing/finetune/getFont.js +3 -3
- package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
- package/lib/video/postprocessing/finetune/index.js +26 -9
- package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
- package/lib/video/screenRecording/recorder.js +2 -2
- package/lib/video/screenRecording/setOrangeBackground.js +3 -0
- package/package.json +11 -11
- package/types/core/engine/command/addText.d.ts +35 -6
- package/types/core/engine/command/addText.d.ts.map +1 -1
- package/types/core/engine/command/click.d.ts +87 -16
- package/types/core/engine/command/click.d.ts.map +1 -1
- package/types/core/engine/command/commandHelper.d.ts +16 -0
- package/types/core/engine/command/commandHelper.d.ts.map +1 -0
- package/types/core/engine/command/cookie.d.ts +65 -0
- package/types/core/engine/command/cookie.d.ts.map +1 -0
- package/types/core/engine/command/element.d.ts +19 -1
- package/types/core/engine/command/element.d.ts.map +1 -1
- package/types/core/engine/command/javaScript.d.ts +4 -0
- package/types/core/engine/command/javaScript.d.ts.map +1 -1
- package/types/core/engine/command/measure/screenshots.d.ts +15 -0
- package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
- package/types/core/engine/command/measure/video.d.ts +26 -0
- package/types/core/engine/command/measure/video.d.ts.map +1 -0
- package/types/core/engine/command/measure.d.ts +59 -33
- package/types/core/engine/command/measure.d.ts.map +1 -1
- package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
- package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
- package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
- package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
- package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
- package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
- package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
- package/types/core/engine/command/navigation.d.ts +3 -3
- package/types/core/engine/command/select.d.ts +45 -8
- package/types/core/engine/command/select.d.ts.map +1 -1
- package/types/core/engine/command/selectorParser.d.ts +20 -0
- package/types/core/engine/command/selectorParser.d.ts.map +1 -0
- package/types/core/engine/command/set.d.ts +34 -7
- package/types/core/engine/command/set.d.ts.map +1 -1
- package/types/core/engine/command/wait.d.ts +29 -6
- package/types/core/engine/command/wait.d.ts.map +1 -1
- package/types/core/engine/commands.d.ts +296 -28
- package/types/core/engine/commands.d.ts.map +1 -1
- package/types/support/engineUtils.d.ts.map +1 -1
- package/types/support/getViewPort.d.ts.map +1 -1
- package/types/support/usbPower.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
- package/types/video/screenRecording/recorder.d.ts +2 -2
- package/types/video/screenRecording/recorder.d.ts.map +1 -1
- package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
- package/types/video/video.d.ts +1 -1
- package/types/video/video.d.ts.map +1 -1
- package/visualmetrics/visualmetrics-portable.py +136 -136
- package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
- package/lib/video/screenRecording/ios/iosRecorder.js +0 -68
|
@@ -82,18 +82,10 @@ export class Iteration {
|
|
|
82
82
|
const batteryTemperature = {};
|
|
83
83
|
|
|
84
84
|
const engineDelegate = this.engineDelegate;
|
|
85
|
+
let afterBrowserStoppedRan = false;
|
|
85
86
|
|
|
86
87
|
try {
|
|
87
|
-
|
|
88
|
-
await flushDNS(options);
|
|
89
|
-
}
|
|
90
|
-
if (options.connectivity && options.connectivity.variance) {
|
|
91
|
-
await addConnectivity(options);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
await engineDelegate.beforeBrowserStart();
|
|
95
|
-
await browser.start();
|
|
96
|
-
await engineDelegate.afterBrowserStart(browser);
|
|
88
|
+
await this._startBrowser(browser, engineDelegate, options);
|
|
97
89
|
|
|
98
90
|
// The data we push to all selenium scripts
|
|
99
91
|
const context = new Context(
|
|
@@ -106,35 +98,23 @@ export class Iteration {
|
|
|
106
98
|
browser.getDriver()
|
|
107
99
|
);
|
|
108
100
|
|
|
109
|
-
const commands = new Commands(
|
|
101
|
+
const commands = new Commands({
|
|
110
102
|
browser,
|
|
111
103
|
engineDelegate,
|
|
112
104
|
index,
|
|
113
105
|
result,
|
|
114
|
-
this.storageManager,
|
|
115
|
-
this.pageCompleteCheck,
|
|
106
|
+
storageManager: this.storageManager,
|
|
107
|
+
pageCompleteCheck: this.pageCompleteCheck,
|
|
116
108
|
context,
|
|
117
109
|
videos,
|
|
118
110
|
screenshotManager,
|
|
119
|
-
this.scriptsByCategory,
|
|
120
|
-
this.asyncScriptsByCategory,
|
|
121
|
-
this.postURLScripts,
|
|
111
|
+
scriptsByCategory: this.scriptsByCategory,
|
|
112
|
+
asyncScriptsByCategory: this.asyncScriptsByCategory,
|
|
113
|
+
postURLScripts: this.postURLScripts,
|
|
122
114
|
options
|
|
123
|
-
);
|
|
115
|
+
});
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
// your own URL or use the blank one
|
|
127
|
-
const startURL =
|
|
128
|
-
options.safari && options.safari.startURL
|
|
129
|
-
? options.safari.startURL
|
|
130
|
-
: 'data:text/html;charset=utf-8,<html><body></body></html>';
|
|
131
|
-
if (options.spa) {
|
|
132
|
-
await setResourceTimingBufferSize(startURL, browser.getDriver(), 1000);
|
|
133
|
-
} else if (!options.processStartTime) {
|
|
134
|
-
// Before we needed Chrome to open once to get correct values for Chrome HAR
|
|
135
|
-
// but now Firefox first visual change is slower if we remove this ...
|
|
136
|
-
await browser.getDriver().get(startURL);
|
|
137
|
-
}
|
|
117
|
+
await this._loadStartURL(browser, options);
|
|
138
118
|
|
|
139
119
|
// On slowish Android phones it takes some time for the
|
|
140
120
|
// browser to get ready
|
|
@@ -150,32 +130,14 @@ export class Iteration {
|
|
|
150
130
|
await this.video.record(0, index);
|
|
151
131
|
}
|
|
152
132
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await navigationScript(context, commands, this.postURLScripts);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
commands.error(error.message);
|
|
165
|
-
commands.markAsFailure(error.message);
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
if (commands.measure.areWeMeasuring === true) {
|
|
169
|
-
// someone forgot to call stop();
|
|
170
|
-
log.info(
|
|
171
|
-
'It looks like you missed to call measure.stop() after calling measure.start(..) (without using a URL). Checkout https://www.sitespeed.io/documentation/sitespeed.io/scripting/#measurestartalias'
|
|
172
|
-
);
|
|
173
|
-
await commands.measure.stop();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
for (const postScript of this.postScripts) {
|
|
177
|
-
await postScript(context, commands);
|
|
178
|
-
}
|
|
133
|
+
await this._runScripts(
|
|
134
|
+
navigationScript,
|
|
135
|
+
context,
|
|
136
|
+
commands,
|
|
137
|
+
options,
|
|
138
|
+
browser,
|
|
139
|
+
engineDelegate
|
|
140
|
+
);
|
|
179
141
|
|
|
180
142
|
if (recordVideo && options.videoParams.debug) {
|
|
181
143
|
// Just use the first URL
|
|
@@ -184,95 +146,33 @@ export class Iteration {
|
|
|
184
146
|
await this.video.stop(url);
|
|
185
147
|
}
|
|
186
148
|
|
|
187
|
-
await
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
} else {
|
|
197
|
-
if (isAndroidConfigured(options) && options.ignoreShutdownFailures) {
|
|
198
|
-
try {
|
|
199
|
-
log.info('Ignoring shutdown failures on android...');
|
|
200
|
-
await browser.stop();
|
|
201
|
-
} catch {
|
|
202
|
-
// Ignore shutdown failures on android
|
|
203
|
-
log.info('Shutdown problem hit, ignoring...');
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
await browser.stop();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Give the browsers some time to stop
|
|
210
|
-
await delay(2000);
|
|
211
|
-
await engineDelegate.afterBrowserStopped();
|
|
149
|
+
await this._stopBrowser(
|
|
150
|
+
browser,
|
|
151
|
+
engineDelegate,
|
|
152
|
+
options,
|
|
153
|
+
result,
|
|
154
|
+
index,
|
|
155
|
+
commands
|
|
156
|
+
);
|
|
157
|
+
afterBrowserStoppedRan = true;
|
|
212
158
|
|
|
213
159
|
if (isAndroidConfigured(options)) {
|
|
214
160
|
batteryTemperature.stop = await this.android.getTemperature();
|
|
215
161
|
await this.android.removeDevtoolsFw();
|
|
216
162
|
}
|
|
217
163
|
|
|
218
|
-
|
|
219
|
-
let index_ = 0;
|
|
220
|
-
for (let myVideo of videos) {
|
|
221
|
-
try {
|
|
222
|
-
const videoMetrics = await myVideo.postProcessing(
|
|
223
|
-
result[index_].browserScripts.timings.pageTimings,
|
|
224
|
-
result[index_].browserScripts.pageinfo.visualElements
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
for (let progress of [
|
|
228
|
-
'VisualProgress',
|
|
229
|
-
'ContentfulSpeedIndexProgress',
|
|
230
|
-
'PerceptualSpeedIndexProgress'
|
|
231
|
-
]) {
|
|
232
|
-
if (videoMetrics.visualMetrics[progress]) {
|
|
233
|
-
videoMetrics.visualMetrics[progress] = jsonifyVisualProgress(
|
|
234
|
-
videoMetrics.visualMetrics[progress]
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (videoMetrics.visualMetrics['KeyColorFrames']) {
|
|
239
|
-
videoMetrics.visualMetrics['KeyColorFrames'] =
|
|
240
|
-
jsonifyKeyColorFrames(
|
|
241
|
-
videoMetrics.visualMetrics['KeyColorFrames']
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
result[index_].videoRecordingStart =
|
|
245
|
-
videoMetrics.videoRecordingStart;
|
|
246
|
-
result[index_].visualMetrics = videoMetrics.visualMetrics;
|
|
247
|
-
index_++;
|
|
248
|
-
} catch (error) {
|
|
249
|
-
// If one of the Visual Metrics runs fail, try the next one
|
|
250
|
-
log.error('Visual Metrics failed to analyse the video', error);
|
|
251
|
-
if (result[index_]) {
|
|
252
|
-
result[index_].error.push(error.message);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
result.screenshots = screenshotManager.getSaved();
|
|
164
|
+
await this._processVisualMetrics(videos, result, options);
|
|
259
165
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
result.description = commands.meta.description;
|
|
270
|
-
}
|
|
271
|
-
if (commands.meta.title) {
|
|
272
|
-
result.title = commands.meta.title;
|
|
273
|
-
}
|
|
166
|
+
this._collectResults(
|
|
167
|
+
result,
|
|
168
|
+
screenshotManager,
|
|
169
|
+
engineDelegate,
|
|
170
|
+
index,
|
|
171
|
+
options,
|
|
172
|
+
commands,
|
|
173
|
+
batteryTemperature
|
|
174
|
+
);
|
|
274
175
|
|
|
275
|
-
result.processesAtStart = this.processesAtStart;
|
|
276
176
|
return result;
|
|
277
177
|
} catch (error) {
|
|
278
178
|
log.error(error);
|
|
@@ -306,6 +206,246 @@ export class Iteration {
|
|
|
306
206
|
} catch {
|
|
307
207
|
// Most cases the browser been stopped already
|
|
308
208
|
}
|
|
209
|
+
// Ensure delegate cleanup runs even on failure paths so that
|
|
210
|
+
// long-lived helpers (e.g. ios-capture, safaridriver) are released.
|
|
211
|
+
// Skip when _stopBrowser already reached afterBrowserStopped on the
|
|
212
|
+
// happy path — running the cleanup twice surfaced as ENOENT noise on
|
|
213
|
+
// the Chrome user-data dir cleanup and could double-kill processes
|
|
214
|
+
// on Safari.
|
|
215
|
+
if (!afterBrowserStoppedRan) {
|
|
216
|
+
try {
|
|
217
|
+
await engineDelegate.afterBrowserStopped();
|
|
218
|
+
} catch {
|
|
219
|
+
// best-effort — helpers may already have been released
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async _startBrowser(browser, engineDelegate, options) {
|
|
226
|
+
if (options.flushDNS) {
|
|
227
|
+
await flushDNS(options);
|
|
228
|
+
}
|
|
229
|
+
if (options.connectivity && options.connectivity.variance) {
|
|
230
|
+
await addConnectivity(options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await engineDelegate.beforeBrowserStart();
|
|
234
|
+
await browser.start();
|
|
235
|
+
// The runner deep-merges options into a fresh object before passing them
|
|
236
|
+
// to the browser-specific builder, so anything the builder records there
|
|
237
|
+
// never reaches the engine. Copy it back to the engine's options object
|
|
238
|
+
// so result.info.browser can pick it up. Issue #1622.
|
|
239
|
+
if (browser.options && browser.options.recordedBrowserSettings) {
|
|
240
|
+
this.options.recordedBrowserSettings =
|
|
241
|
+
browser.options.recordedBrowserSettings;
|
|
242
|
+
}
|
|
243
|
+
await engineDelegate.afterBrowserStart(browser);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async _loadStartURL(browser, options) {
|
|
247
|
+
// Safari can't load data:text URLs at startup, so you can either choose
|
|
248
|
+
// your own URL or use the blank one
|
|
249
|
+
const startURL =
|
|
250
|
+
options.safari && options.safari.startURL
|
|
251
|
+
? options.safari.startURL
|
|
252
|
+
: 'data:text/html;charset=utf-8,<html><body></body></html>';
|
|
253
|
+
if (options.spa) {
|
|
254
|
+
await setResourceTimingBufferSize(startURL, browser.getDriver(), 1000);
|
|
255
|
+
} else if (!options.processStartTime) {
|
|
256
|
+
// Before we needed Chrome to open once to get correct values for Chrome HAR
|
|
257
|
+
// but now Firefox first visual change is slower if we remove this ...
|
|
258
|
+
await browser.getDriver().get(startURL);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async _runScripts(
|
|
263
|
+
navigationScript,
|
|
264
|
+
context,
|
|
265
|
+
commands,
|
|
266
|
+
options,
|
|
267
|
+
browser,
|
|
268
|
+
engineDelegate
|
|
269
|
+
) {
|
|
270
|
+
for (const preScript of this.preScripts) {
|
|
271
|
+
await preScript(context, commands);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (options.preURL) {
|
|
275
|
+
await preURL(browser, engineDelegate, this.pageCompleteCheck, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await navigationScript(context, commands, this.postURLScripts);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
commands.error(error.message);
|
|
282
|
+
commands.markAsFailure(error.message);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
if (commands.measure.areWeMeasuring === true) {
|
|
286
|
+
// someone forgot to call stop();
|
|
287
|
+
log.info(
|
|
288
|
+
'It looks like you missed to call measure.stop() after calling measure.start(..) (without using a URL). Checkout https://www.sitespeed.io/documentation/sitespeed.io/scripting/#measurestartalias'
|
|
289
|
+
);
|
|
290
|
+
await commands.measure.stop();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const postScript of this.postScripts) {
|
|
294
|
+
await postScript(context, commands);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async _stopBrowser(
|
|
299
|
+
browser,
|
|
300
|
+
engineDelegate,
|
|
301
|
+
options,
|
|
302
|
+
result,
|
|
303
|
+
index,
|
|
304
|
+
commands
|
|
305
|
+
) {
|
|
306
|
+
await engineDelegate.beforeBrowserStop(browser, index, result);
|
|
307
|
+
|
|
308
|
+
// if we are in debig mode we wait on a continue command
|
|
309
|
+
if (options.debug) {
|
|
310
|
+
await commands.debug.breakpoint('End-of-iteration');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options.browser === 'firefox' && options.debug) {
|
|
314
|
+
log.info('Firefox is kept open in debug mode');
|
|
315
|
+
} else {
|
|
316
|
+
if (isAndroidConfigured(options) && options.ignoreShutdownFailures) {
|
|
317
|
+
try {
|
|
318
|
+
log.info('Ignoring shutdown failures on android...');
|
|
319
|
+
await browser.stop();
|
|
320
|
+
} catch {
|
|
321
|
+
// Ignore shutdown failures on android
|
|
322
|
+
log.info('Shutdown problem hit, ignoring...');
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
await browser.stop();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Give the browsers some time to stop
|
|
329
|
+
await delay(2000);
|
|
330
|
+
await engineDelegate.afterBrowserStopped();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async _processVisualMetrics(videos, result, options) {
|
|
334
|
+
if (options.visualMetrics && !options.videoParams.debug) {
|
|
335
|
+
let index_ = 0;
|
|
336
|
+
for (let myVideo of videos) {
|
|
337
|
+
try {
|
|
338
|
+
const videoMetrics = await myVideo.postProcessing(
|
|
339
|
+
result[index_].browserScripts.timings.pageTimings,
|
|
340
|
+
result[index_].browserScripts.pageinfo.visualElements
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
for (let progress of [
|
|
344
|
+
'VisualProgress',
|
|
345
|
+
'ContentfulSpeedIndexProgress',
|
|
346
|
+
'PerceptualSpeedIndexProgress'
|
|
347
|
+
]) {
|
|
348
|
+
if (videoMetrics.visualMetrics[progress]) {
|
|
349
|
+
videoMetrics.visualMetrics[progress] = jsonifyVisualProgress(
|
|
350
|
+
videoMetrics.visualMetrics[progress]
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (videoMetrics.visualMetrics['KeyColorFrames']) {
|
|
355
|
+
videoMetrics.visualMetrics['KeyColorFrames'] =
|
|
356
|
+
jsonifyKeyColorFrames(
|
|
357
|
+
videoMetrics.visualMetrics['KeyColorFrames']
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
result[index_].videoRecordingStart = videoMetrics.videoRecordingStart;
|
|
361
|
+
result[index_].visualMetrics = videoMetrics.visualMetrics;
|
|
362
|
+
|
|
363
|
+
// iOS real device video has USB mirroring latency (~1s).
|
|
364
|
+
// Sync visual metrics to FCP so firstVisualChange matches
|
|
365
|
+
// the browser-reported first contentful paint.
|
|
366
|
+
if (options.safari?.ios) {
|
|
367
|
+
syncVisualMetricsToFCP(result[index_]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
index_++;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
// If one of the Visual Metrics runs fail, try the next one
|
|
373
|
+
log.error('Visual Metrics failed to analyse the video', error);
|
|
374
|
+
if (result[index_]) {
|
|
375
|
+
result[index_].error.push(error.message);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_collectResults(
|
|
383
|
+
result,
|
|
384
|
+
screenshotManager,
|
|
385
|
+
engineDelegate,
|
|
386
|
+
index,
|
|
387
|
+
options,
|
|
388
|
+
commands,
|
|
389
|
+
batteryTemperature
|
|
390
|
+
) {
|
|
391
|
+
result.screenshots = screenshotManager.getSaved();
|
|
392
|
+
|
|
393
|
+
if (engineDelegate.postWork) {
|
|
394
|
+
engineDelegate.postWork(index, result);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (isAndroidConfigured(options)) {
|
|
398
|
+
result.batteryTemperature = batteryTemperature;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (commands.meta.description) {
|
|
402
|
+
result.description = commands.meta.description;
|
|
403
|
+
}
|
|
404
|
+
if (commands.meta.title) {
|
|
405
|
+
result.title = commands.meta.title;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
result.processesAtStart = this.processesAtStart;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Compensate for USB screen mirroring latency on iOS real devices.
|
|
414
|
+
* The video stream is delayed ~1s compared to what happens on the device.
|
|
415
|
+
* We align visual metrics to the browser-reported FCP (First Contentful Paint)
|
|
416
|
+
* since both measure the same event — when content first appears on screen.
|
|
417
|
+
*/
|
|
418
|
+
function syncVisualMetricsToFCP(resultEntry) {
|
|
419
|
+
const vm = resultEntry.visualMetrics;
|
|
420
|
+
if (!vm) return;
|
|
421
|
+
|
|
422
|
+
const fcp =
|
|
423
|
+
resultEntry.browserScripts?.timings?.paintTiming?.[
|
|
424
|
+
'first-contentful-paint'
|
|
425
|
+
];
|
|
426
|
+
if (!fcp || fcp <= 0) return;
|
|
427
|
+
if (!vm.FirstVisualChange || vm.FirstVisualChange <= fcp) return;
|
|
428
|
+
|
|
429
|
+
const offset = vm.FirstVisualChange - fcp;
|
|
430
|
+
|
|
431
|
+
for (const key of [
|
|
432
|
+
'FirstVisualChange',
|
|
433
|
+
'LastVisualChange',
|
|
434
|
+
'SpeedIndex',
|
|
435
|
+
'VisualComplete85',
|
|
436
|
+
'VisualComplete95',
|
|
437
|
+
'VisualComplete99'
|
|
438
|
+
]) {
|
|
439
|
+
if (vm[key] > offset) {
|
|
440
|
+
vm[key] -= offset;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (vm.VisualProgress) {
|
|
445
|
+
for (const entry of vm.VisualProgress) {
|
|
446
|
+
if (entry.timestamp > offset) {
|
|
447
|
+
entry.timestamp -= offset;
|
|
448
|
+
}
|
|
309
449
|
}
|
|
310
450
|
}
|
|
311
451
|
}
|
|
@@ -14,6 +14,21 @@ export class NetworkManager {
|
|
|
14
14
|
this.idleTime = getProperty(options, 'timeouts.networkIdle', 5000);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
async _cleanup(messageHandler) {
|
|
18
|
+
if (messageHandler && this.ws) {
|
|
19
|
+
this.ws.removeListener('message', messageHandler);
|
|
20
|
+
}
|
|
21
|
+
await this.bidi.unsubscribe(
|
|
22
|
+
'network.beforeRequestSent',
|
|
23
|
+
this.browsingContextIds
|
|
24
|
+
);
|
|
25
|
+
await this.bidi.unsubscribe(
|
|
26
|
+
'network.responseCompleted',
|
|
27
|
+
this.browsingContextIds
|
|
28
|
+
);
|
|
29
|
+
await this.bidi.unsubscribe('network.fetchError', this.browsingContextIds);
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
async waitForNetworkIdle() {
|
|
18
33
|
await this.bidi.subscribe(
|
|
19
34
|
'network.beforeRequestSent',
|
|
@@ -26,10 +41,10 @@ export class NetworkManager {
|
|
|
26
41
|
await this.bidi.subscribe('network.fetchError', this.browsingContextIds);
|
|
27
42
|
|
|
28
43
|
let inflight = 0;
|
|
29
|
-
let lastRequestTimestamp;
|
|
30
|
-
let lastResponseTimestamp;
|
|
44
|
+
let lastRequestTimestamp = Date.now();
|
|
45
|
+
let lastResponseTimestamp = Date.now();
|
|
31
46
|
this.ws = await this.bidi.socket;
|
|
32
|
-
|
|
47
|
+
const messageHandler = function (event) {
|
|
33
48
|
const { method } = JSON.parse(Buffer.from(event.toString()));
|
|
34
49
|
if (method) {
|
|
35
50
|
switch (method) {
|
|
@@ -54,7 +69,8 @@ export class NetworkManager {
|
|
|
54
69
|
// No default
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
|
-
}
|
|
72
|
+
};
|
|
73
|
+
this.ws.on('message', messageHandler);
|
|
58
74
|
|
|
59
75
|
const startTime = Date.now();
|
|
60
76
|
|
|
@@ -71,20 +87,7 @@ export class NetworkManager {
|
|
|
71
87
|
inflight
|
|
72
88
|
);
|
|
73
89
|
}
|
|
74
|
-
await this.
|
|
75
|
-
'network.beforeRequestSent',
|
|
76
|
-
this.browsingContextIds
|
|
77
|
-
);
|
|
78
|
-
await this.bidi.unsubscribe(
|
|
79
|
-
'network.responseCompleted',
|
|
80
|
-
this.browsingContextIds
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
await this.bidi.unsubscribe(
|
|
84
|
-
'network.fetchError',
|
|
85
|
-
this.browsingContextIds
|
|
86
|
-
);
|
|
87
|
-
|
|
90
|
+
await this._cleanup(messageHandler);
|
|
88
91
|
break;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -92,18 +95,7 @@ export class NetworkManager {
|
|
|
92
95
|
log.info(
|
|
93
96
|
'Timeout waiting for network idle. Inflight requests:' + inflight
|
|
94
97
|
);
|
|
95
|
-
await this.
|
|
96
|
-
'network.beforeRequestSent',
|
|
97
|
-
this.browsingContextIds
|
|
98
|
-
);
|
|
99
|
-
await this.bidi.unsubscribe(
|
|
100
|
-
'network.responseCompleted',
|
|
101
|
-
this.browsingContextIds
|
|
102
|
-
);
|
|
103
|
-
await this.bidi.unsubscribe(
|
|
104
|
-
'network.fetchError',
|
|
105
|
-
this.browsingContextIds
|
|
106
|
-
);
|
|
98
|
+
await this._cleanup(messageHandler);
|
|
107
99
|
break;
|
|
108
100
|
}
|
|
109
101
|
|
|
@@ -266,4 +266,18 @@ export async function configureBuilder(builder, baseDir, options) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
builder.setFirefoxOptions(ffOptions);
|
|
269
|
+
|
|
270
|
+
// Snapshot every argument / preference / capability we just applied so it
|
|
271
|
+
// can be surfaced in the result JSON for debugging — issue #1622. The
|
|
272
|
+
// runner deep-merges options into a fresh container before this is called,
|
|
273
|
+
// so iteration.js threads it back to the engine after browser.start().
|
|
274
|
+
const ffInternals = ffOptions.firefoxOptions_();
|
|
275
|
+
options.recordedBrowserSettings = {
|
|
276
|
+
args: [...(ffInternals.args || [])],
|
|
277
|
+
preferences: { ...ffInternals.prefs },
|
|
278
|
+
binary: ffInternals.binary,
|
|
279
|
+
profile: ffInternals.profile,
|
|
280
|
+
androidPackage: ffInternals.androidPackage,
|
|
281
|
+
androidActivity: ffInternals.androidActivity
|
|
282
|
+
};
|
|
269
283
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { harFromMessages } from 'chrome-har';
|
|
2
|
+
import { getLogger } from '@sitespeed.io/log';
|
|
3
|
+
import { cleanSensitiveHeaders } from '../support/har/index.js';
|
|
4
|
+
|
|
5
|
+
const log = getLogger('browsertime.safari.har');
|
|
6
|
+
|
|
7
|
+
export async function getHar(
|
|
8
|
+
adapter,
|
|
9
|
+
result,
|
|
10
|
+
index,
|
|
11
|
+
inspectorClient,
|
|
12
|
+
includeResponseBodies,
|
|
13
|
+
aliasAndUrl,
|
|
14
|
+
cleanHeaders
|
|
15
|
+
) {
|
|
16
|
+
const messages = adapter.getMessages();
|
|
17
|
+
|
|
18
|
+
let har;
|
|
19
|
+
try {
|
|
20
|
+
har = harFromMessages(messages);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
log.error('Failed to generate HAR from messages: %s', error.message);
|
|
23
|
+
log.verbose(error);
|
|
24
|
+
return getEmptyHar();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fix timing values and remove cached entries without timing data
|
|
28
|
+
har.log.entries = har.log.entries.filter(entry => {
|
|
29
|
+
// Remove entries without _requestTime — these are cached resources
|
|
30
|
+
// that WebKit reported but have no useful timing data
|
|
31
|
+
const noTiming =
|
|
32
|
+
entry._requestTime === undefined || entry._requestTime === 0;
|
|
33
|
+
if (noTiming) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fix any remaining NaN/non-number timing values
|
|
38
|
+
if (entry.timings) {
|
|
39
|
+
for (const key of Object.keys(entry.timings)) {
|
|
40
|
+
if (key === 'comment') {
|
|
41
|
+
// comment must be a string per HAR spec
|
|
42
|
+
if (typeof entry.timings[key] !== 'string') {
|
|
43
|
+
delete entry.timings[key];
|
|
44
|
+
}
|
|
45
|
+
} else if (
|
|
46
|
+
typeof entry.timings[key] !== 'number' ||
|
|
47
|
+
!Number.isFinite(entry.timings[key])
|
|
48
|
+
) {
|
|
49
|
+
entry.timings[key] = 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (typeof entry.time !== 'number' || !Number.isFinite(entry.time)) {
|
|
54
|
+
entry.time = 0;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add browser info
|
|
60
|
+
har.log.creator = {
|
|
61
|
+
name: 'Browsertime',
|
|
62
|
+
version: '1.0'
|
|
63
|
+
};
|
|
64
|
+
har.log.browser = {
|
|
65
|
+
name: 'Safari',
|
|
66
|
+
version: ''
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Clean sensitive headers if requested
|
|
70
|
+
if (cleanHeaders === true) {
|
|
71
|
+
for (const entry of har.log.entries ?? []) {
|
|
72
|
+
for (const header of entry.request?.headers ?? []) {
|
|
73
|
+
header.value = cleanSensitiveHeaders(header.name, header.value);
|
|
74
|
+
}
|
|
75
|
+
for (const header of entry.response?.headers ?? []) {
|
|
76
|
+
header.value = cleanSensitiveHeaders(header.name, header.value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle response bodies
|
|
82
|
+
if (
|
|
83
|
+
inspectorClient &&
|
|
84
|
+
(includeResponseBodies === 'html' || includeResponseBodies === 'all')
|
|
85
|
+
) {
|
|
86
|
+
for (const entry of har.log.entries) {
|
|
87
|
+
if (
|
|
88
|
+
includeResponseBodies === 'html' &&
|
|
89
|
+
!entry.response.content.mimeType?.includes('text/html')
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const response = await inspectorClient.send('Network.getResponseBody', {
|
|
95
|
+
requestId: entry._requestId
|
|
96
|
+
});
|
|
97
|
+
if (response?.result?.body) {
|
|
98
|
+
entry.response.content.text = response.result.body;
|
|
99
|
+
if (response.result.base64Encoded) {
|
|
100
|
+
entry.response.content.encoding = 'base64';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
log.debug('Could not get response body for %s', entry.request.url);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Set page title and URL
|
|
110
|
+
if (har.log.pages.length > 0) {
|
|
111
|
+
const url = result.url || '';
|
|
112
|
+
har.log.pages[0].title = `${url} run ${index}`;
|
|
113
|
+
const alias = aliasAndUrl[url] || url;
|
|
114
|
+
har.log.pages[0]._url = url;
|
|
115
|
+
har.log.pages[0]._alias = alias;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return har;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getEmptyHar() {
|
|
122
|
+
return {
|
|
123
|
+
log: {
|
|
124
|
+
version: '1.2',
|
|
125
|
+
creator: { name: 'Browsertime', version: '1.0' },
|
|
126
|
+
browser: { name: 'Safari', version: '' },
|
|
127
|
+
pages: [],
|
|
128
|
+
entries: []
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
Binary file
|