browsertime 25.1.0 → 25.3.0

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 25.3.0 - 2025-10-17
4
+ ### Added
5
+ * Make it possible to strip cookie and auth headers in the HAR file for Firefox [#2329](https://github.com/sitespeedio/browsertime/pull/2329) and Chrome [#2330](https://github.com/sitespeedio/browsertime/pull/2330). Use `--cleanSensitiveHeaders` to remove a [couple of headers](https://github.com/sitespeedio/browsertime/blob/main/lib/support/har/index.js#L11-L24).
6
+ * Firefox 144 [#2331](https://github.com/sitespeedio/browsertime/pull/2331).
7
+
8
+ ### Fixed
9
+ * Updated developer dependencies [#2326](https://github.com/sitespeedio/browsertime/pull/2326) and [#2327](https://github.com/sitespeedio/browsertime/pull/2327).
10
+ * Updated log dependency [#2328](https://github.com/sitespeedio/b23272327rowsertime/pull/2328)
11
+
12
+ ## 25.2.0 - 2025-10-12
13
+ ### Added
14
+ * Updated to Chrome/Chromedriver/Edge/Edgedriver 141, Firefox 143 [#2325](https://github.com/sitespeedio/browsertime/pull/2325) and [#2323](https://github.com/sitespeedio/browsertime/pull/2323).
15
+ * Updated webdriver and bidi-har [#2322](https://github.com/sitespeedio/browsertime/pull/2322).
16
+ * Added simpleperf and perfetto support for Android, thank you [Abhishek Nimalan](https://github.com/animalan) for PR [#2315](https://github.com/sitespeedio/browsertime/pull/2315).
17
+
3
18
  ## 25.1.0 - 2025-09-05
4
19
  ### Added
5
20
  * Updated to Chrome/Chromedriver 140, Firefox 142 [#2318](https://github.com/sitespeedio/browsertime/pull/2318).
@@ -1,5 +1,5 @@
1
1
  import { promisify } from 'node:util';
2
- import { mkdir as _mkdir, createWriteStream } from 'node:fs';
2
+ import { mkdir as _mkdir, createWriteStream, createReadStream } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { EOL as endOfLine } from 'node:os';
5
5
  import { execa } from 'execa';
@@ -100,6 +100,11 @@ export class Android {
100
100
  });
101
101
  }
102
102
 
103
+ async _uploadFile(sourcePath, destinationPath) {
104
+ log.debug(`Pushing to ${destinationPath} from ${sourcePath}`);
105
+ return this.device.push(createReadStream(sourcePath), destinationPath);
106
+ }
107
+
103
108
  async _downloadDir(sourcePath, destinationPath) {
104
109
  const files = await this.device.readdir(sourcePath);
105
110
 
package/lib/chrome/har.js CHANGED
@@ -2,7 +2,7 @@ import { getLogger } from '@sitespeed.io/log';
2
2
  import { harFromMessages } from 'chrome-har';
3
3
  import { logging } from 'selenium-webdriver';
4
4
  const log = getLogger('browsertime.chrome');
5
- import { addBrowser } from '../support/har/index.js';
5
+ import { addBrowser, cleanSensitiveHeaders } from '../support/har/index.js';
6
6
  const { Type } = logging;
7
7
 
8
8
  export async function getHar(
@@ -15,7 +15,8 @@ export async function getHar(
15
15
  mobileEmulation,
16
16
  androidClient,
17
17
  chrome,
18
- aliasAndUrl
18
+ aliasAndUrl,
19
+ cleanHeaders
19
20
  ) {
20
21
  log.debug('Getting performance logs from Chrome');
21
22
 
@@ -34,6 +35,17 @@ export async function getHar(
34
35
 
35
36
  const har = harFromMessages(messages);
36
37
 
38
+ if (cleanHeaders === true) {
39
+ for (const entry of har.log.entries ?? []) {
40
+ for (const header of entry.request?.headers ?? []) {
41
+ header.value = cleanSensitiveHeaders(header.name, header.value);
42
+ }
43
+ for (const header of entry.response?.headers ?? []) {
44
+ header.value = cleanSensitiveHeaders(header.name, header.value);
45
+ }
46
+ }
47
+ }
48
+
37
49
  if (includeResponseBodies === 'html' || includeResponseBodies === 'all') {
38
50
  await cdpClient.setResponseBodies(har);
39
51
  }
@@ -227,7 +227,7 @@ export class Chromium {
227
227
  const minLength = ${this.options.minLongTaskLength || 50};
228
228
  const observer = new PerformanceObserver(list => {});
229
229
  observer.observe({type: 'longtask', buffered: true});
230
- const entries = observer.takeRecords();
230
+ const entries = observer.takeRecords();
231
231
  const cleaned = [];
232
232
  for (let event of entries) {
233
233
  if (event.duration >= minLength) {
@@ -319,7 +319,8 @@ export class Chromium {
319
319
  this.chrome.mobileEmulation,
320
320
  this.android,
321
321
  this.chrome,
322
- this.aliasAndUrl
322
+ this.aliasAndUrl,
323
+ this.options.cleanSensitiveHeaders
323
324
  )
324
325
  );
325
326
  }
@@ -0,0 +1,215 @@
1
+ import { getLogger } from '@sitespeed.io/log';
2
+ const log = getLogger('browsertime.command.perfetto');
3
+ import path from 'node:path';
4
+ const { join } = path;
5
+ import { writeFile } from 'node:fs/promises';
6
+ import { existsSync } from 'node:fs';
7
+ import { isAndroidConfigured, Android } from '../../../android/index.js';
8
+ const delay = ms => new Promise(res => setTimeout(res, ms));
9
+ /**
10
+ * Manages the collection of perfetto traces on Android.
11
+ *
12
+ * @class
13
+ * @hideconstructor
14
+ */
15
+
16
+ // Default config that works for both chrome and firefox. It contains
17
+ // basic system information, but no track events. Those must be specified
18
+ // manually through perfetto.start(config).
19
+ const defaultConfig = `write_into_file: true
20
+
21
+ buffers: {
22
+ size_kb: 522240
23
+ fill_policy: DISCARD
24
+ }
25
+ buffers: {
26
+ size_kb: 2048
27
+ fill_policy: DISCARD
28
+ }
29
+ data_sources: {
30
+ config {
31
+ name: "android.packages_list"
32
+ target_buffer: 1
33
+ }
34
+ }
35
+ data_sources: {
36
+ config {
37
+ name: "linux.process_stats"
38
+ target_buffer: 1
39
+ process_stats_config {
40
+ scan_all_processes_on_start: true
41
+ }
42
+ }
43
+ }
44
+ data_sources: {
45
+ config {
46
+ name: "linux.ftrace"
47
+ ftrace_config {
48
+ ftrace_events: "power/suspend_resume"
49
+ ftrace_events: "power/cpu_frequency"
50
+ ftrace_events: "power/cpu_idle"
51
+ ftrace_events: "regulator/regulator_set_voltage"
52
+ ftrace_events: "regulator/regulator_set_voltage_complete"
53
+ ftrace_events: "power/clock_enable"
54
+ ftrace_events: "power/clock_disable"
55
+ ftrace_events: "power/clock_set_rate"
56
+ }
57
+ }
58
+ }
59
+ duration_ms: 60000`;
60
+ export class PerfettoTrace {
61
+ constructor(browser, index, storageManager, options) {
62
+ /**
63
+ * @private
64
+ */
65
+ this.browser = browser;
66
+ /**
67
+ * @private
68
+ */
69
+ this.storageManager = storageManager;
70
+ /**
71
+ * @private
72
+ */
73
+ this.options = options;
74
+ /**
75
+ * @private
76
+ */
77
+ this.index = index;
78
+ }
79
+
80
+ async uploadConfigFile(android, config) {
81
+ let destinationFilename = join(this.dataDir, `perfetto-config.txt`);
82
+
83
+ try {
84
+ await writeFile(destinationFilename, config);
85
+ } catch (error) {
86
+ console.error(error);
87
+ }
88
+
89
+ return android._uploadFile(
90
+ destinationFilename,
91
+ '/sdcard/browsertime-perfetto-config.txt'
92
+ );
93
+ }
94
+
95
+ async downloadTrace() {
96
+ let destinationFilename = join(this.dataDir, `trace.perfetto`);
97
+ let deviceTraceFilename = '/data/misc/perfetto-traces/trace';
98
+ log.info(
99
+ `Downloading perfetto trace from ${deviceTraceFilename} to ${destinationFilename}`
100
+ );
101
+ return this.android._downloadFile(deviceTraceFilename, destinationFilename);
102
+ }
103
+
104
+ /**
105
+ * Begin Perfetto Trace Collection.
106
+ *
107
+ * @async
108
+ * @returns {Promise<void>} A promise that resolves when tracing is finished.
109
+ * @throws {Error} Throws an error if the configuration is not set for perfetto tracing.
110
+ */
111
+ async start(config = defaultConfig) {
112
+ if (!isAndroidConfigured(this.options)) {
113
+ throw new Error('Perfetto tracing is only available on Android.');
114
+ }
115
+
116
+ this.android = new Android(this.options);
117
+
118
+ // Create empty subdir for simpleperf data.
119
+ let dirname = `perfetto-${this.index}`;
120
+ let counter = 1;
121
+
122
+ while (true) {
123
+ log.info(`Checking if ${dirname} exists...`);
124
+ if (existsSync(join(this.storageManager.directory, dirname))) {
125
+ dirname = `perfetto-${this.index}.${counter}`;
126
+ counter++;
127
+ log.info(`Directory already exists.`);
128
+ } else {
129
+ this.dataDir = await this.storageManager.createSubDataDir(dirname);
130
+ log.info(`Creating subdir ${this.dataDir}.`);
131
+ break;
132
+ }
133
+ }
134
+ log.info('Finished creating directory.');
135
+
136
+ // Check if perfetto is enabled on this device.
137
+ const tracedEnabled = await this.android._runCommandAndGet(
138
+ `getprop persist.traced.enable`
139
+ );
140
+ if (tracedEnabled !== '1') {
141
+ log.info(`Disabling Perfetto: persist.traced.enable=${tracedEnabled}`);
142
+ return;
143
+ }
144
+
145
+ // Create the trace config on the device.
146
+ log.info('Creating perfetto config on device.');
147
+ await this.uploadConfigFile(this.android, config);
148
+ await delay(5000);
149
+
150
+ // Start perfetto trace and detach.
151
+ log.info('Starting perfetto tracing.');
152
+ const output = await this.android._runCommandAndGet(
153
+ 'cat /sdcard/browsertime-perfetto-config.txt | perfetto -c - --txt --detach=browsertime -o /data/misc/perfetto-traces/trace'
154
+ );
155
+ log.info(`Perfetto starting output: ${output}`);
156
+
157
+ // Remove the config file.
158
+ await this.android.removeFileOnSdCard('browsertime-perfetto-config.txt');
159
+
160
+ return new Promise((resolve, reject) => {
161
+ if (output.includes('Connected to the Perfetto traced service')) {
162
+ this.running = true;
163
+ return resolve();
164
+ } else {
165
+ return reject();
166
+ }
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Stop Perfetto Trace Collection.
172
+ *
173
+ * @async
174
+ * @returns {Promise<void>} A promise that resolves when tracing is finished and the perfetto
175
+ * trace has been collected.
176
+ * @throws {Error} Throws an error if the the perfetto session or trace was not found.
177
+ */
178
+ async stop() {
179
+ if (!isAndroidConfigured(this.options)) {
180
+ throw new Error('Perfetto tracing is only available on Android.');
181
+ }
182
+
183
+ if (!this.running) {
184
+ throw new Error('Perfetto tracing was not started.');
185
+ }
186
+
187
+ // Attach and stop perfetto trace.
188
+ log.info('Starting perfetto tracing.');
189
+ const output = await this.android._runCommandAndGet(
190
+ 'perfetto --attach=browsertime --stop'
191
+ );
192
+ log.info(`Perfetto ending output: ${output}`);
193
+
194
+ let stopSuccessful = false;
195
+
196
+ if (output.includes('Trace written into the output file')) {
197
+ stopSuccessful = true;
198
+ } else {
199
+ const logcat = await this.android._runCommandAndGet(`logcat -d`);
200
+ if (/Tracing session \d+ ended, total sessions:0/.test(logcat)) {
201
+ log.info('Warning: Perfetto session ended prematurely.');
202
+ stopSuccessful = true;
203
+ }
204
+ }
205
+ this.running = false;
206
+
207
+ if (!stopSuccessful) {
208
+ throw new Error('Perfetto process failed or ended suddenly.');
209
+ }
210
+
211
+ log.info('Perfetto tracing finished.');
212
+ // Download perfetto trace.
213
+ return this.downloadTrace();
214
+ }
215
+ }
@@ -0,0 +1,181 @@
1
+ import { getLogger } from '@sitespeed.io/log';
2
+ import path from 'node:path';
3
+ const { join } = path;
4
+ import { execa } from 'execa';
5
+ import { existsSync, renameSync } from 'node:fs';
6
+ import { isAndroidConfigured } from '../../../android/index.js';
7
+ // const delay = ms => new Promise(res => setTimeout(res, ms));
8
+ /**
9
+ * Manages the collection of perfetto traces on Android.
10
+ *
11
+ * @class
12
+ * @hideconstructor
13
+ */
14
+
15
+ const log = getLogger('browsertime.command.simpleperf');
16
+
17
+ const defaultOptions =
18
+ '--call-graph fp --duration 240 -f 1000 --trace-offcpu -e cpu-clock';
19
+
20
+ /**
21
+ * Timeout a promise after ms. Use promise.race to compete
22
+ * about the timeout and the promise.
23
+ * @param {promise} promise - the promise to wait for
24
+ * @param {int} ms - how long in ms to wait for the promise to fininsh
25
+ * @param {string} errorMessage - the error message in the Error if we timeouts
26
+ */
27
+
28
+ async function timeout(promise, ms, errorMessage) {
29
+ let timer;
30
+
31
+ return Promise.race([
32
+ new Promise((resolve, reject) => {
33
+ timer = setTimeout(reject, ms, new Error(errorMessage));
34
+ return timer;
35
+ }),
36
+ promise.then(value => {
37
+ clearTimeout(timer);
38
+ return value;
39
+ })
40
+ ]);
41
+ }
42
+
43
+ export class SimplePerfProfiler {
44
+ constructor(browser, index, storageManager, options) {
45
+ /**
46
+ * @private
47
+ */
48
+ this.browser = browser;
49
+ /**
50
+ * @private
51
+ */
52
+ this.storageManager = storageManager;
53
+ /**
54
+ * @private
55
+ */
56
+ this.options = options;
57
+ /**
58
+ * @private
59
+ */
60
+ this.index = index;
61
+ /**
62
+ * @private
63
+ */
64
+ this.running = false;
65
+ }
66
+
67
+ /**
68
+ * Start Simpleperf profiling.
69
+ *
70
+ * @async
71
+ * @returns {Promise<void>} A promise that resolves when simpleperf has started profiling.
72
+ * @throws {Error} Throws an error if app_profiler.py fails to execute.
73
+ */
74
+
75
+ async start(profilerOptions = defaultOptions, dirName = 'simpleperf') {
76
+ if (!isAndroidConfigured(this.options)) {
77
+ throw new Error('Simpleperf profiling is only available on Android.');
78
+ }
79
+
80
+ log.info('Starting simpleperf profiler.');
81
+
82
+ // Create empty subdirectory for simpleperf data.
83
+ let dirname = `${dirName}-${this.index}`;
84
+ let counter = 0;
85
+
86
+ while (true) {
87
+ log.info(`Checking if ${dirname} exists...`);
88
+
89
+ if (existsSync(join(this.storageManager.directory, dirname))) {
90
+ dirname = `${dirName}-${this.index}.${counter}`;
91
+ counter++;
92
+ log.info(`Directory already exists.`);
93
+ } else {
94
+ this.dataDir = await this.storageManager.createSubDataDir(dirname);
95
+ log.info(`Creating subdir ${this.dataDir}.`);
96
+ break;
97
+ }
98
+ }
99
+
100
+ // Execute simpleperf.
101
+ const packageName =
102
+ this.options.browser === 'firefox'
103
+ ? this.options.firefox?.android?.package
104
+ : this.options.chrome?.android?.package;
105
+ let ndkPath = this.options.androidNDK;
106
+ let cmd = `${ndkPath}/simpleperf/app_profiler.py`;
107
+ let args = [
108
+ '-p',
109
+ packageName,
110
+ '-r',
111
+ profilerOptions,
112
+ '--log',
113
+ 'debug',
114
+ '-o',
115
+ join(this.dataDir, 'perf.data')
116
+ ];
117
+ this.simpleperfProcess = execa(cmd, args);
118
+
119
+ // Waiting for simpleperf to start.
120
+ let simpleperfPromise = new Promise((resolve, reject) => {
121
+ let stderrStream = this.simpleperfProcess.stderr;
122
+ stderrStream.on('data', data => {
123
+ let dataStr = data.toString();
124
+ log.info(dataStr);
125
+ if (/command 'record' starts running/.test(dataStr)) {
126
+ this.running = true;
127
+ stderrStream.removeAllListeners('data');
128
+ return resolve();
129
+ }
130
+ if (/Failed to record profiling data./.test(dataStr)) {
131
+ this.running = false;
132
+ log.info(`Error starting simpleperf: ${dataStr}`);
133
+ throw new Error('Simpleperf failed to start.');
134
+ }
135
+ });
136
+ stderrStream.once('error', reject);
137
+ });
138
+
139
+ // Set a 30s timeout for starting simpleperf.
140
+ return timeout(simpleperfPromise, 30_000, 'Simpleperf timed out.');
141
+ }
142
+
143
+ /**
144
+ * Stop Simpleperf profiling.
145
+ *
146
+ * @async
147
+ * @returns {Promise<void>} A promise that resolves when simpleperf has stopped profiling
148
+ * and collected profile data.
149
+ * @throws {Error} Throws an error if app_profiler.py fails to execute.
150
+ */
151
+ async stop() {
152
+ if (!isAndroidConfigured(this.options)) {
153
+ throw new Error('Simpleperf profiling is only available on Android.');
154
+ }
155
+
156
+ if (!this.running) {
157
+ throw new Error('Simpleperf profiling was not started.');
158
+ }
159
+
160
+ log.info('Stop simpleperf profiler.');
161
+ this.simpleperfProcess.kill('SIGINT');
162
+
163
+ // Return when "profiling is finished." is found, or an error.
164
+ return new Promise((resolve, reject) => {
165
+ let stderrStream = this.simpleperfProcess.stderr;
166
+ log.info('Reading stderr.');
167
+ stderrStream.on('data', data => {
168
+ const dataStr = data.toString();
169
+ log.info(dataStr);
170
+ if (/profiling is finished./.test(dataStr)) {
171
+ stderrStream.removeAllListeners('data');
172
+ // There is no way to specify the output of binary_cache, so manually move
173
+ // it into the data directory.
174
+ renameSync('binary_cache', join(this.dataDir, 'binary_cache'));
175
+ return resolve();
176
+ }
177
+ });
178
+ stderrStream.once('error', reject);
179
+ });
180
+ }
181
+ }
@@ -29,6 +29,8 @@ import { Navigation } from './command/navigation.js';
29
29
  import { GeckoProfiler } from '../../firefox/geckoProfiler.js';
30
30
  import { GeckoProfiler as GeckoProfilerCommand } from './command/geckoProfiler.js';
31
31
  import { PerfStatsInterface } from './command/perfStats.js';
32
+ import { PerfettoTrace } from './command/perfetto.js';
33
+ import { SimplePerfProfiler } from './command/simpleperf.js';
32
34
  /**
33
35
  * Represents the set of commands available in a Browsertime script.
34
36
  * @hideconstructor
@@ -65,6 +67,23 @@ export class Commands {
65
67
  options
66
68
  );
67
69
 
70
+ /**
71
+ * Provides functionality to collect perfetto traces.
72
+ * @type {PerfettoTrace}
73
+ */
74
+ this.perfetto = new PerfettoTrace(browser, index, storageManager, options);
75
+
76
+ /**
77
+ * Provides functionality to collect simpleperf profiles.
78
+ * @type {SimplePerfProfiler}
79
+ */
80
+ this.simpleperf = new SimplePerfProfiler(
81
+ browser,
82
+ index,
83
+ storageManager,
84
+ options
85
+ );
86
+
68
87
  /**
69
88
  * Manages GeckoProfiler functionality to collect performance profiles.
70
89
  * @type {GeckoProfiler}
@@ -6,15 +6,51 @@ export class PerfStats {
6
6
  this.runner = runner;
7
7
  }
8
8
 
9
- async start(featureMask) {
9
+ async start(features) {
10
10
  const runner = this.runner;
11
- const script = `ChromeUtils.setPerfStatsCollectionMask(${featureMask});`;
11
+
12
+ // Parse features if it's a string
13
+ let featuresArray = features;
14
+ if (typeof features === 'string') {
15
+ featuresArray = features
16
+ .split(',')
17
+ .map(f => f.trim())
18
+ .filter(f => f.length > 0);
19
+ }
20
+
21
+ // eslint-disable-next-line prettier/prettier
22
+ const allFeaturesMask = 0xFF_FF_FF_FF;
23
+
24
+ const script = `
25
+ if (typeof ChromeUtils.setPerfStatsFeatures === 'function') {
26
+ if (!${JSON.stringify(featuresArray)} || ${JSON.stringify(featuresArray)}.length === 0) {
27
+ ChromeUtils.enableAllPerfStatsFeatures();
28
+ } else {
29
+ ChromeUtils.setPerfStatsFeatures(${JSON.stringify(featuresArray)});
30
+ }
31
+ } else if (typeof ChromeUtils.setPerfStatsCollectionMask === 'function') {
32
+ ChromeUtils.setPerfStatsCollectionMask(${features || allFeaturesMask});
33
+ } else {
34
+ throw new Error('PerfStats API not available in this Firefox version');
35
+ }
36
+ `;
37
+
12
38
  return runner.runPrivilegedScript(script, 'Start PerfStats Measurement');
13
39
  }
14
40
 
15
41
  async stop() {
16
42
  const runner = this.runner;
17
- const script = `ChromeUtils.setPerfStatsCollectionMask(0);`;
43
+
44
+ const script = `
45
+ if (typeof ChromeUtils.setPerfStatsFeatures === 'function') {
46
+ ChromeUtils.setPerfStatsFeatures([]);
47
+ } else if (typeof ChromeUtils.setPerfStatsCollectionMask === 'function') {
48
+ ChromeUtils.setPerfStatsCollectionMask(0);
49
+ } else {
50
+ throw new Error('PerfStats API not available in this Firefox version');
51
+ }
52
+ `;
53
+
18
54
  return runner.runPrivilegedScript(script, 'Stop PerfStats Measurement');
19
55
  }
20
56
 
@@ -3,7 +3,11 @@ import { promisify } from 'node:util';
3
3
  import path from 'node:path';
4
4
  import { getLogger } from '@sitespeed.io/log';
5
5
  import { adapters } from 'ff-test-bidi-har-export';
6
- import { getEmptyHAR, mergeHars } from '../../support/har/index.js';
6
+ import {
7
+ getEmptyHAR,
8
+ mergeHars,
9
+ cleanSensitiveHeaders
10
+ } from '../../support/har/index.js';
7
11
  import { loadUsbPowerProfiler } from '../../support/usbPower.js';
8
12
  import { pathToFolder } from '../../support/pathToFolder.js';
9
13
  import { isAndroidConfigured, Android } from '../../android/index.js';
@@ -63,7 +67,12 @@ export class Firefox {
63
67
  browsingContextIds: [this.windowId],
64
68
  debugLogs:
65
69
  this.options.verbose >= 2 || this.firefoxConfig.enableBidiHarLog,
66
- driver: runner.getDriver()
70
+ driver: runner.getDriver(),
71
+ headerValueFormatter: this.options.cleanSensitiveHeaders
72
+ ? cleanSensitiveHeaders
73
+ : function (name, value) {
74
+ return value;
75
+ }
67
76
  });
68
77
  }
69
78
 
@@ -177,7 +186,7 @@ export class Firefox {
177
186
 
178
187
  if (this.firefoxConfig.perfStats) {
179
188
  this.perfStats = new PerfStats(runner);
180
- return this.perfStats.start(this.firefoxConfig.perfStatsParams.mask);
189
+ return this.perfStats.start(this.firefoxConfig.perfStatsParams?.features);
181
190
  }
182
191
 
183
192
  this.testStartTime = Date.now();
@@ -549,11 +549,10 @@ export function parseCommandLine() {
549
549
  type: 'boolean',
550
550
  group: 'firefox'
551
551
  })
552
- .option('firefox.perfStatsParams.mask', {
553
- describe: 'Mask to decide which features to enable',
554
- // eslint-disable-next-line prettier/prettier
555
- default: 0xFF_FF_FF_FF,
556
- type: 'number',
552
+ .option('firefox.perfStatsParams.features', {
553
+ describe:
554
+ 'Comma-separated list of PerfStats features to enable. If not provided, all features will be enabled.',
555
+ type: 'string',
557
556
  group: 'firefox'
558
557
  })
559
558
  .option('firefox.collectMozLog', {
@@ -838,6 +837,18 @@ export function parseCommandLine() {
838
837
  'Before a test start, verify that the device has a Internet connection by pinging 8.8.8.8 (or a configurable domain with --androidPingAddress)',
839
838
  group: 'android'
840
839
  })
840
+ .option('android.ndk', {
841
+ alias: 'androidNDK',
842
+ type: 'string',
843
+ describe: 'Path to the Android NDK (required for simpleperf profiling).',
844
+ group: 'android'
845
+ })
846
+ .option('android.perfettoTrace', {
847
+ alias: 'androidPerfettoTrace',
848
+ describe: 'Collect a perfetto trace with the given configuration.',
849
+ default: false,
850
+ group: 'android'
851
+ })
841
852
  /** Process start time. Android-only for now. */
842
853
  .option('processStartTime', {
843
854
  type: 'boolean',
@@ -1206,6 +1217,11 @@ export function parseCommandLine() {
1206
1217
  type: 'boolean',
1207
1218
  describe: 'Pass --gzipHar to gzip the HAR file'
1208
1219
  })
1220
+ .option('cleanSensitiveHeaders', {
1221
+ type: 'boolean',
1222
+ describe:
1223
+ 'Pass --cleanSensitiveHeaders to remove sensitive headers in the HAR file'
1224
+ })
1209
1225
  .option('config', {
1210
1226
  describe:
1211
1227
  'Path to JSON config file. You can also use a .browsertime.json file that will automatically be found by Browsertime using find-up.',
@@ -8,6 +8,21 @@ const require = createRequire(import.meta.url);
8
8
  const version = require('../../../package.json').version;
9
9
  import { pick, isEmpty, getProperty } from '../../support/util.js';
10
10
 
11
+ const sensitiveHeaders = new Set([
12
+ 'authorization',
13
+ 'proxy-authorization',
14
+ 'cookie',
15
+ 'set-cookie',
16
+ 'x-api-key',
17
+ 'x-auth-token',
18
+ 'x-access-token',
19
+ 'x-client-secret',
20
+ 'x-csrf-token',
21
+ 'x-xsrf-token',
22
+ 'x-amz-security-token',
23
+ 'x-amz-signature'
24
+ ]);
25
+
11
26
  function generateUniquePageId(baseId, existingIdMap) {
12
27
  let newId = baseId;
13
28
  while (existingIdMap.has(newId)) {
@@ -385,3 +400,10 @@ export function addExtraFieldsToHar(totalResults, har, options) {
385
400
  }
386
401
  }
387
402
  }
403
+
404
+ export function cleanSensitiveHeaders(name, value) {
405
+ if (sensitiveHeaders.has(name.toLowerCase())) {
406
+ return '[REMOVED]';
407
+ }
408
+ return value;
409
+ }
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "browsertime",
3
3
  "description": "Get performance metrics from your web page using Browsertime.",
4
- "version": "25.1.0",
4
+ "version": "25.3.0",
5
5
  "bin": "./bin/browsertime.js",
6
6
  "type": "module",
7
7
  "types": "./types/scripting.d.ts",
8
8
  "dependencies": {
9
9
  "@devicefarmer/adbkit": "3.3.8",
10
- "@sitespeed.io/chromedriver": "140.0.7339-80",
11
- "@sitespeed.io/edgedriver": "138.0.3351-83",
10
+ "@sitespeed.io/chromedriver": "141.0.7390-76",
11
+ "@sitespeed.io/edgedriver": "141.0.3537-71",
12
12
  "@sitespeed.io/geckodriver": "0.36.0",
13
- "@sitespeed.io/log": "0.2.6",
13
+ "@sitespeed.io/log": "1.0.0",
14
14
  "@sitespeed.io/throttle": "5.0.1",
15
15
  "@sitespeed.io/tracium": "0.3.3",
16
- "chrome-har": "1.0.1",
16
+ "chrome-har": "1.1.1",
17
17
  "chrome-remote-interface": "0.33.3",
18
18
  "execa": "9.6.0",
19
19
  "fast-stats": "0.0.7",
20
- "ff-test-bidi-har-export": "0.0.17",
20
+ "ff-test-bidi-har-export": "0.0.18",
21
21
  "lodash.merge": "4.6.2",
22
- "selenium-webdriver": "4.35.0",
22
+ "selenium-webdriver": "4.36.0",
23
23
  "yargs": "18.0.0"
24
24
  },
25
25
  "optionalDependencies": {
@@ -31,18 +31,18 @@
31
31
  "usb-power-profiling": "1.6.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/selenium-webdriver": "4.1.25",
35
- "ava": "6.4.0",
34
+ "@types/selenium-webdriver": "4.35.1",
35
+ "ava": "6.4.1",
36
36
  "clean-jsdoc-theme": "4.3.0",
37
- "eslint": "9.30.1",
38
- "eslint-config-prettier": "10.1.5",
39
- "eslint-plugin-prettier": "5.5.1",
40
- "eslint-plugin-unicorn": "59.0.1",
41
- "jsdoc": "4.0.4",
37
+ "eslint": "9.37.0",
38
+ "eslint-config-prettier": "10.1.8",
39
+ "eslint-plugin-prettier": "5.5.4",
40
+ "eslint-plugin-unicorn": "61.0.2",
41
+ "jsdoc": "4.0.5",
42
42
  "prettier": "3.6.2",
43
- "serve": "14.2.4",
43
+ "serve": "14.2.5",
44
44
  "serve-handler": "6.1.6",
45
- "typescript": "5.7.2"
45
+ "typescript": "5.9.3"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=20.0.0"
@@ -15,6 +15,7 @@ export class Android {
15
15
  _runAsRootAndGet(command: any): Promise<any>;
16
16
  _runAsRoot(command: any): Promise<boolean>;
17
17
  _downloadFile(sourcePath: any, destinationPath: any): Promise<any>;
18
+ _uploadFile(sourcePath: any, destinationPath: any): Promise<any>;
18
19
  _downloadDir(sourcePath: any, destinationPath: any): Promise<void>;
19
20
  getFullPathOnSdCard(path: any): string;
20
21
  mkDirOnSdCard(dirName: any): Promise<any>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/android/index.js"],"names":[],"mappings":"AAglBA,2DAWC;AA5kBD;IACE,0BAwBC;IAfC,YAAgC;IAEhC,QAIC;IACD,UAAgC;IAGhC,6BAA6B;IAC7B,yBAA2B;IAC3B,eAAgC;IAKlC,uBAgBC;IALC,YAA4C;IAG1C,YAAoE;IAIxE,wCAEC;IAED,8CAIC;IAED,6CAIC;IAED,2CAUC;IAED,mEAYC;IAED,mEAcC;IAED,uCAEC;IAED,0CAGC;IAED,4CAMC;IAED,4CAMC;IAED,uBAGC;IAED,kCAOC;IAED;;;;;;;OAuBC;IAED,2CAKC;IAED,8BAKC;IAED,oKAuBC;IAED,2BAIC;IAED,qCAGC;IAED,iCAGC;IAED,wBASC;IAED,2CAOC;IAED,gCAEC;IAED,0BAIC;IAED,iCAGC;IAED,8CAIC;IAED,4BAEC;IAED,sCAYC;IAED,mDAsBC;IAED,gDA+CC;IAED;;;;;;;OAqDC;IAED,4DAQC;IAED,mCAcC;IAED,kCAQC;IAED,iCAIC;IAED;;;;OAGC;IAED;;;OAEC;IAED,6GAaC;CACF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/android/index.js"],"names":[],"mappings":"AAqlBA,2DAWC;AAjlBD;IACE,0BAwBC;IAfC,YAAgC;IAEhC,QAIC;IACD,UAAgC;IAGhC,6BAA6B;IAC7B,yBAA2B;IAC3B,eAAgC;IAKlC,uBAgBC;IALC,YAA4C;IAG1C,YAAoE;IAIxE,wCAEC;IAED,8CAIC;IAED,6CAIC;IAED,2CAUC;IAED,mEAYC;IAED,iEAGC;IAED,mEAcC;IAED,uCAEC;IAED,0CAGC;IAED,4CAMC;IAED,4CAMC;IAED,uBAGC;IAED,kCAOC;IAED;;;;;;;OAuBC;IAED,2CAKC;IAED,8BAKC;IAED,oKAuBC;IAED,2BAIC;IAED,qCAGC;IAED,iCAGC;IAED,wBASC;IAED,2CAOC;IAED,gCAEC;IAED,0BAIC;IAED,iCAGC;IAED,8CAIC;IAED,4BAEC;IAED,sCAYC;IAED,mDAsBC;IAED,gDA+CC;IAED;;;;;;;OAqDC;IAED,4DAQC;IAED,mCAcC;IAED,kCAQC;IAED,iCAIC;IAED;;;;OAGC;IAED;;;OAEC;IAED,6GAaC;CACF"}
@@ -0,0 +1,43 @@
1
+ export class PerfettoTrace {
2
+ constructor(browser: any, index: any, storageManager: any, options: any);
3
+ /**
4
+ * @private
5
+ */
6
+ private browser;
7
+ /**
8
+ * @private
9
+ */
10
+ private storageManager;
11
+ /**
12
+ * @private
13
+ */
14
+ private options;
15
+ /**
16
+ * @private
17
+ */
18
+ private index;
19
+ uploadConfigFile(android: any, config: any): Promise<any>;
20
+ downloadTrace(): Promise<any>;
21
+ /**
22
+ * Begin Perfetto Trace Collection.
23
+ *
24
+ * @async
25
+ * @returns {Promise<void>} A promise that resolves when tracing is finished.
26
+ * @throws {Error} Throws an error if the configuration is not set for perfetto tracing.
27
+ */
28
+ start(config?: string): Promise<void>;
29
+ android: Android;
30
+ dataDir: any;
31
+ running: boolean;
32
+ /**
33
+ * Stop Perfetto Trace Collection.
34
+ *
35
+ * @async
36
+ * @returns {Promise<void>} A promise that resolves when tracing is finished and the perfetto
37
+ * trace has been collected.
38
+ * @throws {Error} Throws an error if the the perfetto session or trace was not found.
39
+ */
40
+ stop(): Promise<void>;
41
+ }
42
+ import { Android } from '../../../android/index.js';
43
+ //# sourceMappingURL=perfetto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perfetto.d.ts","sourceRoot":"","sources":["../../../../lib/core/engine/command/perfetto.js"],"names":[],"mappings":"AA2DA;IACE,yEAiBC;IAhBC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,uBAAoC;IACpC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,cAAkB;IAGpB,0DAaC;IAED,8BAOC;IAED;;;;;;OAMG;IACH,wBAHa,OAAO,CAAC,IAAI,CAAC,CA4DzB;IApDC,iBAAwC;IAapC,aAAkE;IAiClE,iBAAmB;IAQzB;;;;;;;OAOG;IACH,QAJa,OAAO,CAAC,IAAI,CAAC,CAwCzB;CACF;wBAhN4C,2BAA2B"}
@@ -0,0 +1,43 @@
1
+ export class SimplePerfProfiler {
2
+ constructor(browser: any, index: any, storageManager: any, options: any);
3
+ /**
4
+ * @private
5
+ */
6
+ private browser;
7
+ /**
8
+ * @private
9
+ */
10
+ private storageManager;
11
+ /**
12
+ * @private
13
+ */
14
+ private options;
15
+ /**
16
+ * @private
17
+ */
18
+ private index;
19
+ /**
20
+ * @private
21
+ */
22
+ private running;
23
+ /**
24
+ * Start Simpleperf profiling.
25
+ *
26
+ * @async
27
+ * @returns {Promise<void>} A promise that resolves when simpleperf has started profiling.
28
+ * @throws {Error} Throws an error if app_profiler.py fails to execute.
29
+ */
30
+ start(profilerOptions?: string, dirName?: string): Promise<void>;
31
+ dataDir: any;
32
+ simpleperfProcess: import("execa").ResultPromise<{}>;
33
+ /**
34
+ * Stop Simpleperf profiling.
35
+ *
36
+ * @async
37
+ * @returns {Promise<void>} A promise that resolves when simpleperf has stopped profiling
38
+ * and collected profile data.
39
+ * @throws {Error} Throws an error if app_profiler.py fails to execute.
40
+ */
41
+ stop(): Promise<void>;
42
+ }
43
+ //# sourceMappingURL=simpleperf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simpleperf.d.ts","sourceRoot":"","sources":["../../../../lib/core/engine/command/simpleperf.js"],"names":[],"mappings":"AA0CA;IACE,yEAqBC;IApBC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,uBAAoC;IACpC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,cAAkB;IAClB;;OAEG;IACH,gBAAoB;IAGtB;;;;;;OAMG;IAEH,mDAJa,OAAO,CAAC,IAAI,CAAC,CAsEzB;IA/CK,aAAkE;IAuBtE,qDAAyC;IA0B3C;;;;;;;OAOG;IACH,QAJa,OAAO,CAAC,IAAI,CAAC,CAiCzB;CACF"}
@@ -4,6 +4,16 @@
4
4
  */
5
5
  export class Commands {
6
6
  constructor(browser: any, engineDelegate: any, index: any, result: any, storageManager: any, pageCompleteCheck: any, context: any, videos: any, screenshotManager: any, scriptsByCategory: any, asyncScriptsByCategory: any, postURLScripts: any, options: any);
7
+ /**
8
+ * Provides functionality to collect perfetto traces.
9
+ * @type {PerfettoTrace}
10
+ */
11
+ perfetto: PerfettoTrace;
12
+ /**
13
+ * Provides functionality to collect simpleperf profiles.
14
+ * @type {SimplePerfProfiler}
15
+ */
16
+ simpleperf: SimplePerfProfiler;
7
17
  profiler: GeckoProfilerCommand;
8
18
  perfStats: PerfStatsInterface;
9
19
  /**
@@ -144,6 +154,8 @@ export class Commands {
144
154
  */
145
155
  element: Element;
146
156
  }
157
+ import { PerfettoTrace } from './command/perfetto.js';
158
+ import { SimplePerfProfiler } from './command/simpleperf.js';
147
159
  import { GeckoProfiler as GeckoProfilerCommand } from './command/geckoProfiler.js';
148
160
  import { PerfStatsInterface } from './command/perfStats.js';
149
161
  import { ChromeTrace } from './command/chromeTrace.js';
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/core/engine/commands.js"],"names":[],"mappings":"AA+BA;;;GAGG;AACH;IACE,gQAwPC;IAnNC,+BAMC;IAOD,8BAA0B;IAO1B;;;OAGG;IACH,OAFU,WAAW,CAE+C;IAEpE;;;OAGG;IACH,OAFU,KAAK,CAEmC;IAElD;;;OAGG;IACH,QAFU,MAAM,CAE0B;IAE1C;;;OAGG;IACH,SAFU,OAAO,CAEkB;IAEnC;;;OAGG;IACH,MAFU,IAAI,CAEkC;IAEhD;;;OAGG;IACH,SAFU,OAAO,CAEK;IAEtB;;;;;;;;OAQG;IACH,mBAA+C;IAE/C;;;OAGG;IACH,YAFU,UAAU,CAEwC;IAE5D;;;;;OAKG;IACH,gBAAyC;IAEzC;;;;;OAKG;IACH,wBAAmD;IAEnD;;;OAGG;IACH,IAFU,UAAU,CAEgC;IAEpD;;;OAGG;IACH,QAFU,MAAM,CAMf;IAED;;;OAGG;IACH,KAFU,GAAG,CAEc;IAE3B;;;OAGG;IACH,WAFU,SAAS,CAEoB;IAEvC;;;OAGG;IACH,OAFU,KAAK,CAEsC;IAErD;;;OAGG;IACH,MAFU,IAAI,CAEQ;IAEtB;;;OAGG;IACH,YAFU,UAAU,CAE+C;IAEnE;;;OAGG;IACH,KAFU,8BAA8B,CAE1B;IAEd;;;;OAIG;IACH,MAFU,IAAI,CAEuC;IAErD;;;OAGG;IACH,SAFU,cAAc,CAEkB;IAE1C;;;;OAIG;IACH,OAFW,KAAK,CAEwB;IAExC;;;OAGG;IACH,WA0BC;IAED;;;OAGG;IACH,QAFU,MAAM,CAEiB;IAEjC;;;;OAIG;IACH,QAHU,OAAO,CAGiB;IAElC;;;OAGG;IACH,SAFU,OAAO,CAEkB;CAEtC;sDAhQqD,4BAA4B;mCAC/C,wBAAwB;4BAZ/B,0BAA0B;sBAhBhC,oBAAoB;uBAwBnB,qBAAqB;wBAzBpB,sBAAsB;qBAGzB,mBAAmB;wBAChB,sBAAsB;2BAsBnB,yBAAyB;2BArBzB,yBAAyB;uBAC7B,qBAAqB;oBAExB,kBAAkB;mCAGH,wBAAwB;sBAFrC,oBAAoB;qBACrB,mBAAmB;2BAHb,yBAAyB;+CASL,qCAAqC;qBAF/D,mBAAmB;+BACT,sBAAsB;sBAF/B,oBAAoB;uBADnB,qBAAqB;wBAbpB,sBAAsB;wBAGtB,sBAAsB"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/core/engine/commands.js"],"names":[],"mappings":"AAiCA;;;GAGG;AACH;IACE,gQAyQC;IA1OC;;;OAGG;IACH,UAFU,aAAa,CAEmD;IAE1E;;;OAGG;IACH,YAFU,kBAAkB,CAO3B;IAQD,+BAMC;IAOD,8BAA0B;IAO1B;;;OAGG;IACH,OAFU,WAAW,CAE+C;IAEpE;;;OAGG;IACH,OAFU,KAAK,CAEmC;IAElD;;;OAGG;IACH,QAFU,MAAM,CAE0B;IAE1C;;;OAGG;IACH,SAFU,OAAO,CAEkB;IAEnC;;;OAGG;IACH,MAFU,IAAI,CAEkC;IAEhD;;;OAGG;IACH,SAFU,OAAO,CAEK;IAEtB;;;;;;;;OAQG;IACH,mBAA+C;IAE/C;;;OAGG;IACH,YAFU,UAAU,CAEwC;IAE5D;;;;;OAKG;IACH,gBAAyC;IAEzC;;;;;OAKG;IACH,wBAAmD;IAEnD;;;OAGG;IACH,IAFU,UAAU,CAEgC;IAEpD;;;OAGG;IACH,QAFU,MAAM,CAMf;IAED;;;OAGG;IACH,KAFU,GAAG,CAEc;IAE3B;;;OAGG;IACH,WAFU,SAAS,CAEoB;IAEvC;;;OAGG;IACH,OAFU,KAAK,CAEsC;IAErD;;;OAGG;IACH,MAFU,IAAI,CAEQ;IAEtB;;;OAGG;IACH,YAFU,UAAU,CAE+C;IAEnE;;;OAGG;IACH,KAFU,8BAA8B,CAE1B;IAEd;;;;OAIG;IACH,MAFU,IAAI,CAEuC;IAErD;;;OAGG;IACH,SAFU,cAAc,CAEkB;IAE1C;;;;OAIG;IACH,OAFW,KAAK,CAEwB;IAExC;;;OAGG;IACH,WA0BC;IAED;;;OAGG;IACH,QAFU,MAAM,CAEiB;IAEjC;;;;OAIG;IACH,QAHU,OAAO,CAGiB;IAElC;;;OAGG;IACH,SAFU,OAAO,CAEkB;CAEtC;8BAjR6B,uBAAuB;mCAClB,yBAAyB;sDAHN,4BAA4B;mCAC/C,wBAAwB;4BAZ/B,0BAA0B;sBAhBhC,oBAAoB;uBAwBnB,qBAAqB;wBAzBpB,sBAAsB;qBAGzB,mBAAmB;wBAChB,sBAAsB;2BAsBnB,yBAAyB;2BArBzB,yBAAyB;uBAC7B,qBAAqB;oBAExB,kBAAkB;mCAGH,wBAAwB;sBAFrC,oBAAoB;qBACrB,mBAAmB;2BAHb,yBAAyB;+CASL,qCAAqC;qBAF/D,mBAAmB;+BACT,sBAAsB;sBAF/B,oBAAoB;uBADnB,qBAAqB;wBAbpB,sBAAsB;wBAGtB,sBAAsB"}
@@ -1,7 +1,7 @@
1
1
  export class PerfStats {
2
2
  constructor(runner: any);
3
3
  runner: any;
4
- start(featureMask: any): Promise<any>;
4
+ start(features: any): Promise<any>;
5
5
  stop(): Promise<any>;
6
6
  collect(): Promise<{}>;
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"perfStats.d.ts","sourceRoot":"","sources":["../../lib/firefox/perfStats.js"],"names":[],"mappings":"AAGA;IACE,yBAEC;IADC,YAAoB;IAGtB,sCAIC;IAED,qBAIC;IAED,uBAuCC;CACF"}
1
+ {"version":3,"file":"perfStats.d.ts","sourceRoot":"","sources":["../../lib/firefox/perfStats.js"],"names":[],"mappings":"AAGA;IACE,yBAEC;IADC,YAAoB;IAGtB,mCA8BC;IAED,qBAcC;IAED,uBAuCC;CACF"}