sitespeed.io 30.3.0 → 30.4.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.
@@ -24,6 +24,11 @@ jobs:
24
24
  sudo apt-get update
25
25
  sudo apt-get --only-upgrade install google-chrome-stable
26
26
  google-chrome --version
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade --user pip
30
+ python -m pip install --user scipy
31
+ python -m pip show scipy
27
32
  - name: Install Firefox
28
33
  uses: browser-actions/setup-firefox@latest
29
34
  #with:
@@ -69,4 +74,6 @@ jobs:
69
74
  - name: Run test with Influx 2.6.1
70
75
  run: bin/sitespeed.js http://127.0.0.1:3001/simple/ -n 1 --influxdb.host 127.0.0.1 --influxdb.port 8087 --influxdb.version 2 --influxdb.organisation sitespeed --influxdb.token sitespeed --xvfb
71
76
  - name: Run Chrome test with config
72
- run: node bin/sitespeed.js --config test/exampleConfig.json http://127.0.0.1:3001/simple/ --xvfb
77
+ run: node bin/sitespeed.js --config test/exampleConfig.json http://127.0.0.1:3001/simple/ --xvfb
78
+ - name: Run Chrome test using compare plugin
79
+ run: node bin/sitespeed.js --compare.id compare --compare.saveBaseline --compare.baselinePath test/ http://127.0.0.1:3001/simple/ --xvfb
package/CHANGELOG.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # CHANGELOG - sitespeed.io (we use [semantic versioning](https://semver.org))
2
2
 
3
+ ## 30.4.1 - 2023-11-28
4
+ ### Fixed
5
+ * Fix for Firefox when generating the result HTML. It was broken since we where missing CPU data.
6
+
7
+ ## 30.4.0 - 2023-11-27
8
+ ### Fixed
9
+ * Upgrade to Browsretime 19.1.0 with a fix for Geckodriver so that the correct ARM version is installed on Mac Arm machines.
10
+ ### Added
11
+ * The new compare plugin [PR 4009](https://github.com/sitespeedio/sitespeed.io/pull/4009) makes it easy to use Mann Whitney U/Wilcox for support to find performance egressions. Read all about the plugin in the [documentation](https://www.sitespeed.io/documentation/sitespeed.io/compare/).
12
+ * Firefox 120 in the Docker container [#4010](https://github.com/sitespeedio/sitespeed.io/pull/4010).
13
+ * Button to download the console logs, thank you [bairov pavel](https://github.com/Amerousful) for PR [#4007](https://github.com/sitespeedio/sitespeed.io/pull/4007).
14
+
3
15
  ## 30.3.0 - 2023-11-09
4
16
 
5
17
  ### Added
6
18
  * Upgrade to Browsertime 18.0.0.
7
- * Added suupport to run user journeys with WebPageReplay [#4005](https://github.com/sitespeedio/sitespeed.io/pull/4005).
19
+ * Added support to run user journeys with WebPageReplay [#4005](https://github.com/sitespeedio/sitespeed.io/pull/4005).
8
20
 
9
21
  ### Fixed
10
22
  * Downgrade puppeteer in the +1 container for Lighthouse, thank you [bairov pavel](https://github.com/Amerousful) for PR [#123](https://github.com/sitespeedio/plugin-lighthouse/pull/123).
package/CONTRIBUTORS.md CHANGED
@@ -45,3 +45,4 @@ Many many many thanks to:
45
45
  * [Devrim Tufan](https://github.com/tufandevrim)
46
46
  * [Keith Cirkel](https://github.com/keithamus)
47
47
  * [Jonathan Lee](https://github.com/beenanner)
48
+ * [Pavel Bairov](https://github.com/Amerousful)
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM sitespeedio/webbrowsers:chrome-119.0-firefox-119.0-edge-119.0
1
+ FROM sitespeedio/webbrowsers:chrome-119.0-firefox-120.0-edge-119.0
2
2
 
3
3
  ARG TARGETPLATFORM=linux/amd64
4
4
 
@@ -44,4 +44,6 @@ RUN echo 'ALL ALL=NOPASSWD: /usr/sbin/tc, /usr/sbin/route, /usr/sbin/ip' > /etc/
44
44
 
45
45
  ENTRYPOINT ["/start.sh"]
46
46
  VOLUME /sitespeed.io
47
+ VOLUME /baseline
48
+
47
49
  WORKDIR /sitespeed.io
package/lib/cli/cli.js CHANGED
@@ -1870,6 +1870,68 @@ export async function parseCommandLine() {
1870
1870
  group: 'API'
1871
1871
  });
1872
1872
 
1873
+ parsed
1874
+ .option('compare.id', {
1875
+ type: 'string',
1876
+ describe:
1877
+ 'The id of the test. Will be used to find the baseline test, that is using the id as a part of the name.',
1878
+ group: 'compare'
1879
+ })
1880
+ .option('compare.baselinePath', {
1881
+ type: 'string',
1882
+ describe:
1883
+ 'Specifies the path to the baseline data file. This file is used as a reference for comparison against the current test data.',
1884
+ group: 'compare'
1885
+ })
1886
+ .option('compare.saveBaseline', {
1887
+ type: 'boolean',
1888
+ default: false,
1889
+ describe:
1890
+ 'Determines whether to save the current test data as the new baseline. Set to true to save the current data as baseline for future comparisons.',
1891
+ group: 'compare'
1892
+ })
1893
+ .option('compare.testType', {
1894
+ describe:
1895
+ 'Selects the statistical test type to be used for comparison. Options are mannwhitneyu for the Mann-Whitney U test and wilcoxon for the Wilcoxon signed-rank test.',
1896
+ choices: ['mannwhitneyu', ' wilcoxon'],
1897
+ default: 'mannwhitneyu',
1898
+ group: 'compare'
1899
+ })
1900
+ .option('compare.alternative', {
1901
+ choices: ['less', ' greater', 'two-sided'],
1902
+ default: 'less',
1903
+ describe:
1904
+ 'Specifies the alternative hypothesis to be tested. Options are less for one-sided test where the first group is expected to be less than the second, greater for one-sided test with the first group expected to be greater, or two-sided for a two-sided test.',
1905
+ group: 'compare'
1906
+ })
1907
+ .option('compare.wilcoxon.correction', {
1908
+ type: 'boolean',
1909
+ describe:
1910
+ 'Enables or disables the continuity correction in the Wilcoxon signed-rank test. Set to true to enable the correction.',
1911
+ default: false,
1912
+ group: 'compare'
1913
+ })
1914
+ .option('compare.wilcoxon.zeroMethod', {
1915
+ choices: ['wilcox', ' pratt', 'zsplit'],
1916
+ describe:
1917
+ 'Specifies the method for handling zero differences in the Wilcoxon test. wilcox discards all zero-difference pairs, pratt includes all, and zsplit splits them evenly among positive and negative ranks.',
1918
+ default: 'zsplit',
1919
+ group: 'compare'
1920
+ })
1921
+ .option('compare.mannwhitneyu.useContinuity', {
1922
+ type: 'boolean',
1923
+ default: false,
1924
+ describe:
1925
+ 'Determines whether to use continuity correction in the Mann-Whitney U test. Set to true to apply the correction.',
1926
+ group: 'compare'
1927
+ })
1928
+ .option('compare.mannwhitneyu.method', {
1929
+ choices: ['auto', ' exact', 'symptotic'],
1930
+ escribe:
1931
+ 'Selects the method for calculating the Mann-Whitney U test. auto automatically selects between exact and asymptotic based on sample size, exact uses the exact distribution of U, and symptotic uses a normal approximation.',
1932
+ default: 'auto',
1933
+ group: 'compare'
1934
+ });
1873
1935
  parsed
1874
1936
  .option('mobile', {
1875
1937
  describe:
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ export async function getBaseline(id, compareOptions) {
5
+ try {
6
+ return JSON.parse(
7
+ await fs.readFile(
8
+ resolve(
9
+ join(compareOptions.baselinePath || process.cwd(), `${id}.json`)
10
+ )
11
+ )
12
+ );
13
+ } catch {
14
+ return;
15
+ }
16
+ }
17
+ /*
18
+ async function getBaselineFromInternet(url) {
19
+ try {
20
+ const response = await fetch(url);
21
+ return response.json();
22
+ } catch (error) {
23
+ log.error('Could not fetch', error);
24
+ }
25
+ }
26
+
27
+ async function getBaselineFromFile(path) {}
28
+
29
+ /*
30
+ export async function saveBaseline(json, options) {
31
+
32
+ }*/
33
+ export async function saveBaseline(json, name) {
34
+ return fs.writeFile(resolve(name), JSON.stringify(json));
35
+ }
@@ -0,0 +1,347 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { join } from 'node:path';
3
+ import path from 'node:path';
4
+
5
+ import { execa } from 'execa';
6
+ import { Stats } from 'fast-stats';
7
+ import intel from 'intel';
8
+ import { decimals } from '../../support/helpers/index.js';
9
+ const log = intel.getLogger('sitespeedio.plugin.compare');
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ class Metric {
14
+ constructor(name, values) {
15
+ this.name = name;
16
+ this.stats = new Stats().push(values);
17
+ }
18
+
19
+ getName() {
20
+ return this.name;
21
+ }
22
+ getValues() {
23
+ return this.stats.data;
24
+ }
25
+ getStats() {
26
+ return this.stats;
27
+ }
28
+ }
29
+
30
+ export async function runStatisticalTests(data) {
31
+ let extras = '';
32
+ try {
33
+ const { stdout } = await execa(
34
+ process.env.PYTHON || 'python',
35
+ [join(__dirname, 'statistical.py')],
36
+ {
37
+ input: JSON.stringify(data)
38
+ }
39
+ );
40
+ extras = stdout;
41
+ const results = JSON.parse(stdout);
42
+ log.verbose('Result from the python script %j'.results);
43
+ return results;
44
+ } catch (error) {
45
+ log.error(error);
46
+ log.error(extras);
47
+ }
48
+ }
49
+
50
+ export function getStatistics(arrayOfValues) {
51
+ return new Stats().push(arrayOfValues);
52
+ }
53
+
54
+ function getExtras(data) {
55
+ const metrics = {};
56
+ const results = {};
57
+
58
+ for (const run of data.extras) {
59
+ for (const name of Object.keys(run)) {
60
+ if (!metrics[name]) {
61
+ metrics[name] = [];
62
+ }
63
+ metrics[name].push(run[name]);
64
+ }
65
+ }
66
+
67
+ for (const [metricName, values] of Object.entries(metrics)) {
68
+ results[metricName] = new Metric(metricName, values);
69
+ }
70
+
71
+ return results;
72
+ }
73
+
74
+ function getTimings(data) {
75
+ const timingMetrics = {
76
+ ttfb: [],
77
+ loadEventEnd: [],
78
+ firstContentfulPaint: [],
79
+ fullyLoaded: []
80
+ };
81
+
82
+ for (const run of data.browserScripts) {
83
+ timingMetrics['ttfb'].push(run.timings.ttfb);
84
+ timingMetrics['loadEventEnd'].push(run.timings.loadEventEnd);
85
+ timingMetrics['firstContentfulPaint'].push(
86
+ run.timings.paintTiming['first-contentful-paint']
87
+ );
88
+ }
89
+
90
+ for (const run of data.fullyLoaded) {
91
+ timingMetrics['fullyLoaded'].push(run);
92
+ }
93
+
94
+ const results = {};
95
+ for (const [metricName, values] of Object.entries(timingMetrics)) {
96
+ if (!results.timings) {
97
+ results.timings = {};
98
+ }
99
+ results.timings[metricName] = new Metric(`${metricName}`, values);
100
+ }
101
+ return results;
102
+ }
103
+
104
+ function getUserTimings(data) {
105
+ const userTimingMetrics = {};
106
+ for (const run of data.browserScripts) {
107
+ if (run.timings.userTimings) {
108
+ const { marks, measures } = run.timings.userTimings;
109
+
110
+ for (const mark of marks) {
111
+ if (!userTimingMetrics[mark.name]) {
112
+ userTimingMetrics[mark.name] = [];
113
+ }
114
+ userTimingMetrics[mark.name].push(decimals(mark.startTime));
115
+ }
116
+
117
+ for (const measure of measures) {
118
+ if (!userTimingMetrics[measure.name]) {
119
+ userTimingMetrics[measure.name] = [];
120
+ }
121
+ userTimingMetrics[measure.name].push(decimals(measure.startTime));
122
+ }
123
+ }
124
+ }
125
+
126
+ const results = {};
127
+ for (const [metricName, values] of Object.entries(userTimingMetrics)) {
128
+ if (!results.userTimings) {
129
+ results.userTimings = {};
130
+ }
131
+ results.userTimings[metricName] = new Metric(`${metricName}`, values);
132
+ }
133
+ return results;
134
+ }
135
+
136
+ function getElementTimings(data) {
137
+ const elementTimingMetrics = {};
138
+ for (const run of data.browserScripts) {
139
+ if (run.timings.elementTimings) {
140
+ for (const [name, timing] of Object.entries(run.timings.elementTimings)) {
141
+ if (!elementTimingMetrics[name]) {
142
+ elementTimingMetrics[name] = [];
143
+ }
144
+ elementTimingMetrics[name].push(timing.renderTime);
145
+ }
146
+ }
147
+ }
148
+
149
+ const results = {};
150
+ for (const [metricName, values] of Object.entries(elementTimingMetrics)) {
151
+ if (!results.elementTimings) {
152
+ results.elementTimings = {};
153
+ }
154
+ results.elementTimings[metricName] = new Metric(`${metricName}`, values);
155
+ }
156
+ return results;
157
+ }
158
+
159
+ function getGoogleWebVitals(data) {
160
+ const googleWebVitalsMetrics = {};
161
+ for (const run of data.googleWebVitals) {
162
+ for (const [name, value] of Object.entries(run)) {
163
+ if (!googleWebVitalsMetrics[name]) {
164
+ googleWebVitalsMetrics[name] = [];
165
+ }
166
+ googleWebVitalsMetrics[name].push(value);
167
+ }
168
+ }
169
+
170
+ const results = {};
171
+ for (const [metricName, values] of Object.entries(googleWebVitalsMetrics)) {
172
+ if (!results.googleWebVitals) {
173
+ results.googleWebVitals = {};
174
+ }
175
+ results.googleWebVitals[metricName] = new Metric(`${metricName}`, values);
176
+ }
177
+ return results;
178
+ }
179
+
180
+ function getVisualMetrics(data) {
181
+ const DO_NOT_USE = new Set([
182
+ 'VisualProgress',
183
+ 'videoRecordingStart',
184
+ 'VisualComplete85',
185
+ 'VisualComplete95',
186
+ 'VisualComplete99'
187
+ ]);
188
+
189
+ const visualMetrics = {};
190
+ for (const run of data.visualMetrics) {
191
+ for (const [name, value] of Object.entries(run)) {
192
+ if (!DO_NOT_USE.has(name)) {
193
+ if (!visualMetrics[name]) {
194
+ visualMetrics[name] = [];
195
+ }
196
+ visualMetrics[name].push(value);
197
+ }
198
+ }
199
+ }
200
+
201
+ const results = {};
202
+ for (const [metricName, values] of Object.entries(visualMetrics)) {
203
+ if (!results.visualMetrics) {
204
+ results.visualMetrics = {};
205
+ }
206
+ results.visualMetrics[metricName] = new Metric(`${metricName}`, values);
207
+ }
208
+ return results;
209
+ }
210
+
211
+ function getCDPPerformance(data) {
212
+ const metricsToKeep = new Set([
213
+ 'JSEventListeners',
214
+ 'LayoutCount',
215
+ 'RecalcStyleCount',
216
+ 'LayoutDuration',
217
+ 'RecalcStyleDuration',
218
+ 'ScriptDuration',
219
+ 'V8CompileDuration',
220
+ 'TaskDuration',
221
+ 'TaskOtherDuration',
222
+ 'JSHeapUsedSize'
223
+ ]);
224
+ const cdpPerformance = {};
225
+ for (const run of data.cdp.performance) {
226
+ for (const name of Object.keys(run)) {
227
+ if (metricsToKeep.has(name)) {
228
+ if (!cdpPerformance[name]) {
229
+ cdpPerformance[name] = [];
230
+ }
231
+ cdpPerformance[name].push(decimals(run[name]));
232
+ }
233
+ }
234
+ }
235
+
236
+ // Convert to Metric objects
237
+ const results = {};
238
+ for (const [metricName, values] of Object.entries(cdpPerformance)) {
239
+ if (!results.cdp) {
240
+ results.cdp = {};
241
+ }
242
+ results.cdp[metricName] = new Metric(`${metricName}`, values);
243
+ }
244
+ return results;
245
+ }
246
+
247
+ function getCPU(data) {
248
+ const cpuMetrics = {
249
+ tasks: [],
250
+ totalDuration: [],
251
+ lastLongTask: [],
252
+ beforeFirstContentfulPaint: [],
253
+ beforeLargestContentfulPaint: []
254
+ };
255
+
256
+ for (const run of data.cpu) {
257
+ const longTasks = run.longTasks;
258
+ cpuMetrics['tasks'].push(longTasks['tasks']);
259
+ cpuMetrics['totalDuration'].push(longTasks['totalDuration']);
260
+ cpuMetrics['lastLongTask'].push(longTasks['lastLongTask']);
261
+ cpuMetrics['beforeFirstContentfulPaint'].push(
262
+ longTasks['beforeFirstContentfulPaint'].totalDuration
263
+ );
264
+ cpuMetrics['beforeLargestContentfulPaint'].push(
265
+ longTasks['beforeLargestContentfulPaint'].totalDuration
266
+ );
267
+ }
268
+
269
+ const isEmpty = Object.values(cpuMetrics).every(arr => arr.length === 0);
270
+ if (isEmpty) {
271
+ return {}; // Return an empty object if no data
272
+ }
273
+
274
+ const results = {};
275
+ for (const [metricName, values] of Object.entries(cpuMetrics)) {
276
+ if (!results.cpu) {
277
+ results.cpu = {};
278
+ }
279
+ results.cpu[metricName] = new Metric(`${metricName}`, values);
280
+ }
281
+ return results;
282
+ }
283
+
284
+ function getRenderBlocking(data) {
285
+ const renderBlockingMetrics = {
286
+ beforeFCPms: [],
287
+ beforeLCPms: [],
288
+ beforeFCPelements: [],
289
+ beforeLCPelements: []
290
+ };
291
+
292
+ for (const run of data.renderBlocking) {
293
+ renderBlockingMetrics['beforeFCPms'].push(
294
+ run.recalculateStyle.beforeFCP.durationInMillis
295
+ );
296
+ renderBlockingMetrics['beforeFCPelements'].push(
297
+ run.recalculateStyle.beforeFCP.elements
298
+ );
299
+ renderBlockingMetrics['beforeLCPms'].push(
300
+ run.recalculateStyle.beforeLCP.durationInMillis
301
+ );
302
+ renderBlockingMetrics['beforeLCPelements'].push(
303
+ run.recalculateStyle.beforeLCP.elements
304
+ );
305
+ }
306
+
307
+ // Check if all arrays in renderBlockingMetrics are empty
308
+ const isEmpty = Object.values(renderBlockingMetrics).every(
309
+ arr => arr.length === 0
310
+ );
311
+ if (isEmpty) {
312
+ return {}; // Return an empty object if no data
313
+ }
314
+
315
+ const results = {};
316
+ for (const [metricName, values] of Object.entries(renderBlockingMetrics)) {
317
+ if (!results.renderBlocking) {
318
+ results.renderBlocking = {};
319
+ }
320
+ results.renderBlocking[metricName] = new Metric(`${metricName}`, values);
321
+ }
322
+
323
+ return results;
324
+ }
325
+
326
+ export function getMetrics(data) {
327
+ const userTimings = getUserTimings(data);
328
+ const elementTimings = getElementTimings(data);
329
+ const visualMetrics = getVisualMetrics(data);
330
+ const rb = getRenderBlocking(data);
331
+ const gWV = getGoogleWebVitals(data);
332
+ const cdp = getCDPPerformance(data);
333
+ const cpu = getCPU(data);
334
+ const timings = getTimings(data);
335
+ const extras = getExtras(data);
336
+ return {
337
+ ...extras,
338
+ ...timings,
339
+ ...cpu,
340
+ ...cdp,
341
+ ...visualMetrics,
342
+ ...gWV,
343
+ ...rb,
344
+ ...elementTimings,
345
+ ...userTimings
346
+ };
347
+ }