browsertime 25.1.0 → 25.2.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 +5 -0
- package/lib/android/index.js +6 -1
- package/lib/core/engine/command/perfetto.js +215 -0
- package/lib/core/engine/command/simpleperf.js +181 -0
- package/lib/core/engine/commands.js +19 -0
- package/lib/firefox/perfStats.js +39 -3
- package/lib/firefox/webdriver/firefox.js +1 -1
- package/lib/support/cli.js +16 -5
- package/package.json +5 -5
- package/types/android/index.d.ts +1 -0
- package/types/android/index.d.ts.map +1 -1
- package/types/core/engine/command/perfetto.d.ts +43 -0
- package/types/core/engine/command/perfetto.d.ts.map +1 -0
- package/types/core/engine/command/simpleperf.d.ts +43 -0
- package/types/core/engine/command/simpleperf.d.ts.map +1 -0
- package/types/core/engine/commands.d.ts +12 -0
- package/types/core/engine/commands.d.ts.map +1 -1
- package/types/firefox/perfStats.d.ts +1 -1
- package/types/firefox/perfStats.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 25.1.0 - 2025-10-12
|
|
4
|
+
### Added
|
|
5
|
+
* 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).
|
|
6
|
+
* Updated webdriver and bidi-har [#2322](https://github.com/sitespeedio/browsertime/pull/2322).
|
|
7
|
+
|
|
3
8
|
## 25.1.0 - 2025-09-05
|
|
4
9
|
### Added
|
|
5
10
|
* Updated to Chrome/Chromedriver 140, Firefox 142 [#2318](https://github.com/sitespeedio/browsertime/pull/2318).
|
package/lib/android/index.js
CHANGED
|
@@ -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
|
|
|
@@ -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}
|
package/lib/firefox/perfStats.js
CHANGED
|
@@ -6,15 +6,51 @@ export class PerfStats {
|
|
|
6
6
|
this.runner = runner;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async start(
|
|
9
|
+
async start(features) {
|
|
10
10
|
const runner = this.runner;
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -177,7 +177,7 @@ export class Firefox {
|
|
|
177
177
|
|
|
178
178
|
if (this.firefoxConfig.perfStats) {
|
|
179
179
|
this.perfStats = new PerfStats(runner);
|
|
180
|
-
return this.perfStats.start(this.firefoxConfig.perfStatsParams
|
|
180
|
+
return this.perfStats.start(this.firefoxConfig.perfStatsParams?.features);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
this.testStartTime = Date.now();
|
package/lib/support/cli.js
CHANGED
|
@@ -549,11 +549,10 @@ export function parseCommandLine() {
|
|
|
549
549
|
type: 'boolean',
|
|
550
550
|
group: 'firefox'
|
|
551
551
|
})
|
|
552
|
-
.option('firefox.perfStatsParams.
|
|
553
|
-
describe:
|
|
554
|
-
|
|
555
|
-
|
|
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',
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browsertime",
|
|
3
3
|
"description": "Get performance metrics from your web page using Browsertime.",
|
|
4
|
-
"version": "25.
|
|
4
|
+
"version": "25.2.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": "
|
|
11
|
-
"@sitespeed.io/edgedriver": "
|
|
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
13
|
"@sitespeed.io/log": "0.2.6",
|
|
14
14
|
"@sitespeed.io/throttle": "5.0.1",
|
|
@@ -17,9 +17,9 @@
|
|
|
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.
|
|
20
|
+
"ff-test-bidi-har-export": "0.0.18",
|
|
21
21
|
"lodash.merge": "4.6.2",
|
|
22
|
-
"selenium-webdriver": "4.
|
|
22
|
+
"selenium-webdriver": "4.36.0",
|
|
23
23
|
"yargs": "18.0.0"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
package/types/android/index.d.ts
CHANGED
|
@@ -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":"
|
|
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":"
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perfStats.d.ts","sourceRoot":"","sources":["../../lib/firefox/perfStats.js"],"names":[],"mappings":"AAGA;IACE,yBAEC;IADC,YAAoB;IAGtB,
|
|
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"}
|