@jupyterlab/galata 5.0.0-alpha.2 → 5.0.0-alpha.21

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.
Files changed (100) hide show
  1. package/README.md +192 -31
  2. package/lib/benchmarkReporter.d.ts +1 -0
  3. package/lib/benchmarkReporter.js +34 -39
  4. package/lib/benchmarkReporter.js.map +1 -1
  5. package/lib/benchmarkVLTpl.js +19 -5
  6. package/lib/benchmarkVLTpl.js.map +1 -1
  7. package/lib/contents.d.ts +5 -5
  8. package/lib/contents.js +32 -36
  9. package/lib/contents.js.map +1 -1
  10. package/lib/extension/global.d.ts +197 -0
  11. package/lib/extension/global.js +601 -0
  12. package/lib/extension/global.js.map +1 -0
  13. package/lib/extension/index.d.ts +6 -0
  14. package/lib/extension/index.js +27 -0
  15. package/lib/extension/index.js.map +1 -0
  16. package/lib/extension/tokens.d.ts +232 -0
  17. package/lib/extension/tokens.js +13 -0
  18. package/lib/extension/tokens.js.map +1 -0
  19. package/lib/extension.d.ts +223 -0
  20. package/lib/{global.js → extension.js} +1 -2
  21. package/lib/extension.js.map +1 -0
  22. package/lib/fixtures.d.ts +32 -10
  23. package/lib/fixtures.js +64 -17
  24. package/lib/fixtures.js.map +1 -1
  25. package/lib/galata.d.ts +140 -19
  26. package/lib/galata.js +272 -87
  27. package/lib/galata.js.map +1 -1
  28. package/lib/helpers/activity.d.ts +6 -0
  29. package/lib/helpers/activity.js +19 -5
  30. package/lib/helpers/activity.js.map +1 -1
  31. package/lib/helpers/debuggerpanel.d.ts +4 -0
  32. package/lib/helpers/debuggerpanel.js +16 -0
  33. package/lib/helpers/debuggerpanel.js.map +1 -1
  34. package/lib/helpers/filebrowser.js +8 -2
  35. package/lib/helpers/filebrowser.js.map +1 -1
  36. package/lib/helpers/index.d.ts +1 -0
  37. package/lib/helpers/index.js +6 -1
  38. package/lib/helpers/index.js.map +1 -1
  39. package/lib/helpers/kernel.js +7 -7
  40. package/lib/helpers/kernel.js.map +1 -1
  41. package/lib/helpers/menu.d.ts +7 -0
  42. package/lib/helpers/menu.js +17 -1
  43. package/lib/helpers/menu.js.map +1 -1
  44. package/lib/helpers/notebook.d.ts +6 -4
  45. package/lib/helpers/notebook.js +127 -31
  46. package/lib/helpers/notebook.js.map +1 -1
  47. package/lib/helpers/sidebar.d.ts +8 -1
  48. package/lib/helpers/sidebar.js +33 -15
  49. package/lib/helpers/sidebar.js.map +1 -1
  50. package/lib/helpers/statusbar.js +1 -1
  51. package/lib/helpers/statusbar.js.map +1 -1
  52. package/lib/helpers/style.d.ts +42 -0
  53. package/lib/helpers/style.js +50 -0
  54. package/lib/helpers/style.js.map +1 -0
  55. package/lib/helpers/theme.js +1 -1
  56. package/lib/helpers/theme.js.map +1 -1
  57. package/lib/index.d.ts +5 -2
  58. package/lib/index.js +12 -3
  59. package/lib/index.js.map +1 -1
  60. package/lib/jupyterlabpage.d.ts +29 -4
  61. package/lib/jupyterlabpage.js +38 -22
  62. package/lib/jupyterlabpage.js.map +1 -1
  63. package/lib/playwright-config.js +5 -1
  64. package/lib/playwright-config.js.map +1 -1
  65. package/lib/utils.js +5 -1
  66. package/lib/utils.js.map +1 -1
  67. package/package.json +31 -47
  68. package/src/benchmarkReporter.ts +756 -0
  69. package/src/benchmarkVLTpl.ts +91 -0
  70. package/src/contents.ts +472 -0
  71. package/src/extension.ts +281 -0
  72. package/src/fixtures.ts +387 -0
  73. package/src/galata.ts +1035 -0
  74. package/src/helpers/activity.ts +115 -0
  75. package/src/helpers/debuggerpanel.ts +159 -0
  76. package/src/helpers/filebrowser.ts +228 -0
  77. package/src/helpers/index.ts +15 -0
  78. package/src/helpers/kernel.ts +39 -0
  79. package/src/helpers/logconsole.ts +32 -0
  80. package/src/helpers/menu.ts +228 -0
  81. package/src/helpers/notebook.ts +1217 -0
  82. package/src/helpers/performance.ts +57 -0
  83. package/src/helpers/sidebar.ts +289 -0
  84. package/src/helpers/statusbar.ts +56 -0
  85. package/src/helpers/style.ts +100 -0
  86. package/src/helpers/theme.ts +50 -0
  87. package/src/index.ts +19 -0
  88. package/src/jupyterlabpage.ts +704 -0
  89. package/src/playwright-config.ts +26 -0
  90. package/src/utils.ts +264 -0
  91. package/src/vega-statistics.d.ts +15 -0
  92. package/lib/global.d.ts +0 -23
  93. package/lib/global.js.map +0 -1
  94. package/lib/inpage/tokens.d.ts +0 -135
  95. package/lib/inpage/tokens.js +0 -9
  96. package/lib/inpage/tokens.js.map +0 -1
  97. package/lib/lib-inpage/inpage.js +0 -3957
  98. package/lib/lib-inpage/inpage.js.map +0 -1
  99. package/style/index.css +0 -10
  100. package/style/index.js +0 -10
@@ -0,0 +1,756 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { JSONObject } from '@lumino/coreutils';
7
+ import { chromium, firefox, webkit } from '@playwright/test';
8
+ import {
9
+ FullConfig,
10
+ FullResult,
11
+ Reporter,
12
+ Suite,
13
+ TestCase,
14
+ TestResult
15
+ } from '@playwright/test/reporter';
16
+ import { dists, meanpw, variancepn } from '@stdlib/stats/base';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import si from 'systeminformation';
20
+ import * as vega from 'vega';
21
+ import * as vl from 'vega-lite';
22
+ import * as vs from 'vega-statistics';
23
+ import generateVegaLiteSpec from './benchmarkVLTpl';
24
+
25
+ /**
26
+ * Benchmark namespace
27
+ */
28
+ export namespace benchmark {
29
+ /**
30
+ * Default benchmark output results folder
31
+ */
32
+ export const DEFAULT_FOLDER = 'benchmark-results';
33
+ /**
34
+ * Default Playwright test attachment name for benchmark
35
+ */
36
+ export const DEFAULT_NAME_ATTACHMENT = 'galata-benchmark';
37
+ /**
38
+ * Default number of samples per scenario
39
+ */
40
+ export const DEFAULT_NUMBER_SAMPLES = 20;
41
+ /**
42
+ * Default output file name
43
+ */
44
+ export const DEFAULT_OUTPUT = 'benchmark.json';
45
+ /**
46
+ * Data reference for expected results
47
+ */
48
+ export const DEFAULT_EXPECTED_REFERENCE = 'expected';
49
+ /**
50
+ * Data reference for actual results
51
+ */
52
+ export const DEFAULT_REFERENCE = 'actual';
53
+
54
+ /**
55
+ * Number of samples per scenario
56
+ */
57
+ export const nSamples =
58
+ parseInt(process.env['BENCHMARK_NUMBER_SAMPLES'] ?? '0', 10) ||
59
+ DEFAULT_NUMBER_SAMPLES;
60
+
61
+ /**
62
+ * Playwright test attachment for benchmark
63
+ */
64
+ export interface IAttachment {
65
+ /**
66
+ * name <string> Attachment name.
67
+ */
68
+ name: string;
69
+ /**
70
+ * contentType <string> Content type of this attachment to properly present in the report, for example 'application/json' or 'image/png'.
71
+ */
72
+ contentType: 'application/json' | 'image/png' | string;
73
+ /**
74
+ * path <void|string> Optional path on the filesystem to the attached file.
75
+ */
76
+ path?: string;
77
+ /**
78
+ * body <void|Buffer> Optional attachment body used instead of a file.
79
+ */
80
+ body?: Buffer;
81
+ }
82
+
83
+ /**
84
+ * Benchmark test record
85
+ */
86
+ export interface IRecord extends Record<string, any> {
87
+ /**
88
+ * Test kind
89
+ */
90
+ test: string;
91
+ /**
92
+ * Browser name
93
+ */
94
+ browser: string;
95
+ /**
96
+ * Number of samples
97
+ */
98
+ nSamples: number;
99
+ /**
100
+ * Tested file name
101
+ */
102
+ file: string;
103
+ /**
104
+ * Playwright project name
105
+ */
106
+ project: string;
107
+ /**
108
+ * Test duration in milliseconds
109
+ */
110
+ time: number;
111
+ }
112
+
113
+ /**
114
+ * Convert a record as test attachment
115
+ *
116
+ * @param data Data record to attach
117
+ * @returns The attachment
118
+ */
119
+ export function addAttachment<IRecord>(data: IRecord): IAttachment {
120
+ return {
121
+ name: DEFAULT_NAME_ATTACHMENT,
122
+ contentType: 'application/json',
123
+ body: Buffer.from(JSON.stringify(data))
124
+ };
125
+ }
126
+ /**
127
+ * Change between two distributions
128
+ */
129
+ export interface IDistributionChange {
130
+ /**
131
+ * Mean value
132
+ */
133
+ mean: number;
134
+ /**
135
+ * Spread around the mean value
136
+ */
137
+ confidenceInterval: number;
138
+ }
139
+
140
+ /**
141
+ * Statistical description of a distribution
142
+ */
143
+ interface IDistribution {
144
+ /**
145
+ * Mean
146
+ */
147
+ mean: number;
148
+ /**
149
+ * Variance
150
+ */
151
+ variance: number;
152
+ }
153
+
154
+ /**
155
+ * Quantifies the performance changes between two measures systems. Assumes we gathered
156
+ * n independent measurement from each, and calculated their means and variance.
157
+ *
158
+ * Based on the work by Tomas Kalibera and Richard Jones. See their paper
159
+ * "Quantifying Performance Changes with Effect Size Confidence Intervals", section 6.2,
160
+ * formula "Quantifying Performance Change".
161
+ *
162
+ * However, it simplifies it to only assume one level of benchmarks, not multiple levels.
163
+ * If you do have multiple levels, simply use the mean of the lower levels as your data,
164
+ * like they do in the paper.
165
+ *
166
+ * @param oldDistribution The old distribution description
167
+ * @param newDistribution The new distribution description
168
+ * @param n The number of samples from each system (must be equal)
169
+ * @param confidenceInterval The confidence interval for the results.
170
+ * The default is a 95% confidence interval (95% of the time the true mean will be
171
+ * between the resulting mean +- the resulting CI)
172
+ */
173
+ function performanceChange(
174
+ oldDistribution: IDistribution,
175
+ newDistribution: IDistribution,
176
+ n: number,
177
+ confidenceInterval: number = 0.95
178
+ ): IDistributionChange {
179
+ const { mean: yO, variance: sO } = oldDistribution;
180
+ const { mean: yN, variance: sN } = newDistribution;
181
+ const dof = n - 1;
182
+ const t = dists.t.quantile(1 - (1 - confidenceInterval) / 2, dof);
183
+ const oldFactor = sq(yO) - (sq(t) * sO) / n;
184
+ const newFactor = sq(yN) - (sq(t) * sN) / n;
185
+ const meanNum = yO * yN;
186
+ const ciNum = Math.sqrt(sq(yO * yN) - newFactor * oldFactor);
187
+ return {
188
+ mean: meanNum / oldFactor,
189
+ confidenceInterval: ciNum / oldFactor
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Compute the performance change based on a number of old and new measurements.
195
+ *
196
+ * Based on the work by Tomas Kalibera and Richard Jones. See their paper
197
+ * "Quantifying Performance Changes with Effect Size Confidence Intervals", section 6.2,
198
+ * formula "Quantifying Performance Change".
199
+ *
200
+ * However, it simplifies it to only assume one level of benchmarks, not multiple levels.
201
+ * If you do have multiple levels, simply use the mean of the lower levels as your data,
202
+ * like they do in the paper.
203
+ *
204
+ * Note: The measurements must have the same length. As fallback, you could use the minimum
205
+ * size of the two measurement sets.
206
+ *
207
+ * @param oldMeasures The old measurements
208
+ * @param newMeasures The new measurements
209
+ * @param confidenceInterval The confidence interval for the results.
210
+ * @param minLength Fall back to the minimum length of the two arrays
211
+ */
212
+ export function distributionChange(
213
+ oldMeasures: number[],
214
+ newMeasures: number[],
215
+ confidenceInterval: number = 0.95,
216
+ minLength = false
217
+ ): IDistributionChange {
218
+ const n = oldMeasures.length;
219
+ if (!minLength && n !== newMeasures.length) {
220
+ throw new Error('Data have different length');
221
+ }
222
+ return performanceChange(
223
+ { mean: mean(...oldMeasures), variance: variance(...oldMeasures) },
224
+ { mean: mean(...newMeasures), variance: variance(...newMeasures) },
225
+ minLength ? Math.min(n, newMeasures.length) : n,
226
+ confidenceInterval
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Format a performance changes like `between 20.1% slower and 30.3% faster`.
232
+ *
233
+ * @param distribution The distribution change
234
+ * @returns The formatted distribution change
235
+ */
236
+ export function formatChange(distribution: IDistributionChange): string {
237
+ const { mean, confidenceInterval } = distribution;
238
+ return `between ${formatPercent(
239
+ mean + confidenceInterval
240
+ )} and ${formatPercent(mean - confidenceInterval)}`;
241
+ }
242
+
243
+ function formatPercent(percent: number): string {
244
+ if (percent < 1) {
245
+ return `${((1 - percent) * 100).toFixed(1)}% faster`;
246
+ }
247
+ return `${((percent - 1) * 100).toFixed(1)}% slower`;
248
+ }
249
+
250
+ function sq(x: number): number {
251
+ return Math.pow(x, 2);
252
+ }
253
+
254
+ function mean(...x: number[]): number {
255
+ return meanpw(x.length, x, 1);
256
+ }
257
+
258
+ function variance(...x: number[]): number {
259
+ return variancepn(x.length, 1, x, 1);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Report record interface
265
+ */
266
+ export interface IReportRecord extends benchmark.IRecord {
267
+ /**
268
+ * Test suite reference
269
+ */
270
+ reference: string;
271
+ }
272
+
273
+ /**
274
+ * Test suite metadata
275
+ */
276
+ interface IMetadata {
277
+ /**
278
+ * Web browsers version
279
+ */
280
+ browsers: { [name: string]: string };
281
+ /**
282
+ * Benchmark information
283
+ */
284
+ benchmark: {
285
+ BENCHMARK_OUTPUTFILE: string;
286
+ BENCHMARK_REFERENCE: string;
287
+ };
288
+ /**
289
+ * System information
290
+ */
291
+ systemInformation: {
292
+ cpu: Record<string, any>;
293
+ mem: Record<string, any>;
294
+ osInfo: Record<string, any>;
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Report interface
300
+ */
301
+ interface IReport {
302
+ /**
303
+ * Test records
304
+ */
305
+ values: IReportRecord[];
306
+ /**
307
+ * Test metadata
308
+ */
309
+ metadata: IMetadata;
310
+ }
311
+
312
+ /**
313
+ * Custom Playwright reporter for benchmark tests
314
+ */
315
+ class BenchmarkReporter implements Reporter {
316
+ /**
317
+ * @param options
318
+ * - outputFile: Name of the output file (default to env BENCHMARK_OUTPUTFILE)
319
+ * - comparison: Logic of test comparisons: 'snapshot' or 'project'
320
+ * * 'snapshot': (default) This will compare the 'actual' result with the 'expected' one
321
+ * * 'project': This will compare the different project
322
+ * - vegaLiteConfigFactory: Function to create VegaLite configuration from test records; see https://vega.github.io/vega-lite/docs/.
323
+ * - textReportFactory: Function to create text report from test records, this function
324
+ * should return the content and extension of report file.
325
+ */
326
+ constructor(
327
+ options: {
328
+ outputFile?: string;
329
+ comparison?: 'snapshot' | 'project';
330
+ vegaLiteConfigFactory?: (
331
+ allData: Array<IReportRecord>,
332
+ comparison?: 'snapshot' | 'project'
333
+ ) => JSONObject;
334
+ textReportFactory?: (
335
+ allData: Array<IReportRecord>,
336
+ comparison?: 'snapshot' | 'project'
337
+ ) => Promise<[string, string]>;
338
+ } = {}
339
+ ) {
340
+ this._outputFile =
341
+ options.outputFile ??
342
+ process.env['BENCHMARK_OUTPUTFILE'] ??
343
+ benchmark.DEFAULT_OUTPUT;
344
+
345
+ this._comparison = options.comparison ?? 'snapshot';
346
+
347
+ this._expectedReference =
348
+ process.env['BENCHMARK_EXPECTED_REFERENCE'] ??
349
+ benchmark.DEFAULT_EXPECTED_REFERENCE;
350
+ this._reference =
351
+ process.env['BENCHMARK_REFERENCE'] ?? benchmark.DEFAULT_REFERENCE;
352
+
353
+ this._buildVegaLiteGraph =
354
+ options.vegaLiteConfigFactory ?? this.defaultVegaLiteConfigFactory;
355
+
356
+ this._buildTextReport =
357
+ options.textReportFactory ?? this.defaultTextReportFactory;
358
+ }
359
+
360
+ /**
361
+ * Called once before running tests. All tests have been already discovered and put into a hierarchy of [Suite]s.
362
+ * @param config Resolved configuration.
363
+ * @param suite The root suite that contains all projects, files and test cases.
364
+ */
365
+ onBegin(config: FullConfig, suite: Suite): void {
366
+ this.config = config;
367
+ this.suite = suite;
368
+ this._report = new Array<IReportRecord>();
369
+ // Clean up output folder if it exists
370
+ if (this._outputFile) {
371
+ const outputDir = path.resolve(
372
+ path.dirname(this._outputFile),
373
+ benchmark.DEFAULT_FOLDER
374
+ );
375
+ if (fs.existsSync(outputDir)) {
376
+ fs.rmSync(outputDir, { recursive: true, force: true });
377
+ }
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Called after a test has been finished in the worker process.
383
+ * @param test Test that has been finished.
384
+ * @param result Result of the test run.
385
+ */
386
+ onTestEnd(test: TestCase, result: TestResult): void {
387
+ if (result.status === 'passed') {
388
+ this._report.push(
389
+ ...result.attachments
390
+ .filter(a => a.name === benchmark.DEFAULT_NAME_ATTACHMENT)
391
+ .map(raw => {
392
+ const json = JSON.parse(
393
+ raw.body?.toString() ?? '{}'
394
+ ) as any as benchmark.IRecord;
395
+ return { ...json, reference: this._reference };
396
+ })
397
+ );
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Called after all tests has been run, or testing has been interrupted. Note that this method may return a [Promise] and
403
+ * Playwright Test will await it.
404
+ * @param result Result of the full test run. - `'passed'` - Everything went as expected.
405
+ * - `'failed'` - Any test has failed.
406
+ * - `'timedout'` - The
407
+ * [testConfig.globalTimeout](https://playwright.dev/docs/api/class-testconfig#test-config-global-timeout) has been
408
+ * reached.
409
+ * - `'interrupted'` - Interrupted by the user.
410
+ */
411
+ async onEnd(result: FullResult): Promise<void> {
412
+ const report = await this.buildReport();
413
+ const reportString = JSON.stringify(report, undefined, 2);
414
+ if (this._outputFile) {
415
+ const outputDir = path.resolve(
416
+ path.dirname(this._outputFile),
417
+ benchmark.DEFAULT_FOLDER
418
+ );
419
+ const baseName = path.basename(this._outputFile, '.json');
420
+ fs.mkdirSync(outputDir, { recursive: true });
421
+ fs.writeFileSync(
422
+ path.resolve(outputDir, `${baseName}.json`),
423
+ reportString,
424
+ 'utf-8'
425
+ );
426
+
427
+ const allData = [...report.values];
428
+
429
+ if (this._comparison === 'snapshot') {
430
+ // Test if expectations exists otherwise creates it depending on updateSnapshot value
431
+ const expectationsFile = path.resolve(
432
+ this.config.rootDir,
433
+ `${baseName}-expected.json`
434
+ );
435
+ const hasExpectations = fs.existsSync(expectationsFile);
436
+ let expectations: IReport;
437
+ if (!hasExpectations || this.config.updateSnapshots === 'all') {
438
+ expectations = {
439
+ values: report.values.map(d => {
440
+ return {
441
+ ...d,
442
+ reference: this._expectedReference
443
+ };
444
+ }),
445
+ metadata: report.metadata
446
+ };
447
+
448
+ if (this.config.updateSnapshots !== 'none') {
449
+ fs.writeFileSync(
450
+ expectationsFile,
451
+ JSON.stringify(expectations, undefined, 2),
452
+ 'utf-8'
453
+ );
454
+ }
455
+ } else {
456
+ const expected = fs.readFileSync(expectationsFile, 'utf-8');
457
+ expectations = JSON.parse(expected);
458
+ }
459
+
460
+ allData.push(...expectations.values);
461
+ }
462
+
463
+ // - Create report
464
+ const [reportContentString, reportExtension] =
465
+ await this._buildTextReport(allData);
466
+ const reportFile = path.resolve(
467
+ outputDir,
468
+ `${baseName}.${reportExtension}`
469
+ );
470
+ fs.writeFileSync(reportFile, reportContentString, 'utf-8');
471
+
472
+ // Generate graph file and image
473
+ const graphConfigFile = path.resolve(outputDir, `${baseName}.vl.json`);
474
+ const config = this._buildVegaLiteGraph(allData, this._comparison);
475
+ fs.writeFileSync(graphConfigFile, JSON.stringify(config), 'utf-8');
476
+ const vegaSpec = vl.compile(config as any).spec;
477
+
478
+ const view = new vega.View(vega.parse(vegaSpec), {
479
+ renderer: 'svg'
480
+ }).initialize();
481
+ const svgFigure = await view.toSVG();
482
+ const graphFile = path.resolve(outputDir, `${baseName}.svg`);
483
+ fs.writeFileSync(graphFile, svgFigure);
484
+ } else {
485
+ console.log(reportString);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Default text report factory of `BenchmarkReporter`, this method will
491
+ * be used by to generate markdown report. Users can customize the builder
492
+ * by supplying another builder to constructor's option or override this
493
+ * method on a sub-class.
494
+ *
495
+ * @param allData all test records.
496
+ * @param comparison logic of test comparisons:
497
+ * 'snapshot' or 'project'; default 'snapshot'.
498
+ * @returns A list of two strings, the first one
499
+ * is the content of report, the second one is the extension of report file.
500
+ */
501
+ protected async defaultTextReportFactory(
502
+ allData: Array<IReportRecord>,
503
+ comparison: 'snapshot' | 'project' = 'snapshot'
504
+ ): Promise<[string, string]> {
505
+ // Compute statistics
506
+ // - Groupby (test, browser, reference | project, file)
507
+ const reportExtension = 'md';
508
+
509
+ const groups = new Map<
510
+ string,
511
+ Map<string, Map<string, Map<string, number[]>>>
512
+ >();
513
+
514
+ allData.forEach(d => {
515
+ if (!groups.has(d.test)) {
516
+ groups.set(
517
+ d.test,
518
+ new Map<string, Map<string, Map<string, number[]>>>()
519
+ );
520
+ }
521
+
522
+ const testGroup = groups.get(d.test)!;
523
+
524
+ if (!testGroup.has(d.browser)) {
525
+ testGroup.set(d.browser, new Map<string, Map<string, number[]>>());
526
+ }
527
+
528
+ const browserGroup = testGroup.get(d.browser)!;
529
+
530
+ const lastLevel = comparison === 'snapshot' ? d.reference : d.project;
531
+
532
+ if (!browserGroup.has(lastLevel)) {
533
+ browserGroup.set(lastLevel, new Map<string, number[]>());
534
+ }
535
+
536
+ const fileGroup = browserGroup.get(lastLevel)!;
537
+
538
+ if (!fileGroup.has(d.file)) {
539
+ fileGroup.set(d.file, new Array<number>());
540
+ }
541
+
542
+ fileGroup.get(d.file)?.push(d.time);
543
+ });
544
+
545
+ // If the reference | project lists has two items, the intervals will be compared.
546
+ if (!groups.values().next().value) {
547
+ return ['## Benchmark report\n\nNot enough data', reportExtension];
548
+ }
549
+
550
+ const compare =
551
+ (
552
+ groups.values().next().value?.values().next().value as Map<
553
+ string,
554
+ Map<string, number[]>
555
+ >
556
+ ).size === 2;
557
+
558
+ // - Create report
559
+ const reportContent = new Array<string>(
560
+ '## Benchmark report',
561
+ '',
562
+ 'The execution time (in milliseconds) are grouped by test file, test type and browser.',
563
+ 'For each case, the following values are computed: _min_ <- [_1st quartile_ - _median_ - _3rd quartile_] -> _max_.'
564
+ );
565
+
566
+ if (compare) {
567
+ reportContent.push(
568
+ '',
569
+ 'The mean relative comparison is computed with 95% confidence.'
570
+ );
571
+ }
572
+
573
+ reportContent.push('', '<details><summary>Results table</summary>', '');
574
+
575
+ let header = '| Test file |';
576
+ let nFiles = 0;
577
+ for (const [file] of groups
578
+ .values()
579
+ .next()
580
+ .value.values()
581
+ .next()
582
+ .value.values()
583
+ .next().value) {
584
+ header += ` ${file} |`;
585
+ nFiles++;
586
+ }
587
+ reportContent.push(header);
588
+ reportContent.push(new Array(nFiles + 2).fill('|').join(' --- '));
589
+ const filler = new Array(nFiles).fill('|').join(' ');
590
+
591
+ let changeReference = this._expectedReference;
592
+
593
+ for (const [test, testGroup] of groups) {
594
+ reportContent.push(`| **${test}** | ` + filler);
595
+ for (const [browser, browserGroup] of testGroup) {
596
+ reportContent.push(`| \`${browser}\` | ` + filler);
597
+ const actual = new Map<string, number[]>();
598
+ const expected = new Map<string, number[]>();
599
+ for (const [reference, fileGroup] of browserGroup) {
600
+ let line = `| ${reference} |`;
601
+ for (const [filename, dataGroup] of fileGroup) {
602
+ const [q1, median, q3] = vs.quartiles(dataGroup);
603
+
604
+ if (compare) {
605
+ if (reference === this._reference || !actual.has(filename)) {
606
+ actual.set(filename, dataGroup);
607
+ } else {
608
+ changeReference = reference;
609
+ expected.set(filename, dataGroup);
610
+ }
611
+ }
612
+
613
+ line += ` ${Math.min(
614
+ ...dataGroup
615
+ ).toFixed()} <- [${q1.toFixed()} - ${median.toFixed()} - ${q3.toFixed()}] -> ${Math.max(
616
+ ...dataGroup
617
+ ).toFixed()} |`;
618
+ }
619
+
620
+ reportContent.push(line);
621
+ }
622
+
623
+ if (compare) {
624
+ let line = `| Mean relative change |`;
625
+ for (const [filename, oldDistribution] of expected) {
626
+ const newDistribution = actual.get(filename)!;
627
+ try {
628
+ const delta = benchmark.distributionChange(
629
+ oldDistribution,
630
+ newDistribution,
631
+ 0.95,
632
+ true
633
+ );
634
+
635
+ let unmatchWarning = '';
636
+ if (oldDistribution.length != newDistribution.length) {
637
+ unmatchWarning = `[:warning:](# "Reference size ${oldDistribution.length} != Actual size ${newDistribution.length}") `;
638
+ }
639
+
640
+ line += ` ${unmatchWarning}${((delta.mean - 1) * 100).toFixed(
641
+ 1
642
+ )}% ± ${(delta.confidenceInterval * 100).toFixed(1)}% |`;
643
+ } catch (error) {
644
+ console.error(
645
+ `Reference has length ${oldDistribution.length} and new has ${newDistribution.length}.`
646
+ );
647
+ line += ` ${error} |`;
648
+ }
649
+ }
650
+
651
+ reportContent.push(line);
652
+ }
653
+ }
654
+ }
655
+ if (compare) {
656
+ reportContent.push(
657
+ '',
658
+ `Changes are computed with _${changeReference}_ as reference.`
659
+ );
660
+ }
661
+ reportContent.push('', '</details>', '');
662
+ const reportContentString = reportContent.join('\n');
663
+ return [reportContentString, reportExtension];
664
+ }
665
+
666
+ /**
667
+ * Default Vega Lite config factory of `BenchmarkReporter`, this method will
668
+ * be used by to generate VegaLite configuration. Users can customize
669
+ * the builder by supplying another builder to constructor's option or
670
+ * override this method on a sub-class.
671
+ *
672
+ * @param allData all test records.
673
+ * @param comparison logic of test comparisons:
674
+ * 'snapshot' or 'project'; default 'snapshot'.
675
+ * @returns VegaLite configuration
676
+ */
677
+ protected defaultVegaLiteConfigFactory(
678
+ allData: Array<IReportRecord>,
679
+ comparison: 'snapshot' | 'project' = 'snapshot'
680
+ ): Record<string, any> {
681
+ const config = generateVegaLiteSpec(
682
+ [...new Set(allData.map(d => d.test))],
683
+ comparison == 'snapshot' ? 'reference' : 'project',
684
+ [...new Set(allData.map(d => d.file))]
685
+ );
686
+ config.data.values = allData;
687
+ return config;
688
+ }
689
+
690
+ protected async getMetadata(browser?: string): Promise<any> {
691
+ const cpu = await si.cpu();
692
+ // Keep only non-variable value
693
+ const totalMemory = (await si.mem()).total;
694
+ // Remove some os information
695
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
696
+ const { hostname, fqdn, ...osInfo } = await si.osInfo();
697
+
698
+ const browsers = ['chromium', 'firefox', 'webkit'];
699
+ const browserVersions: { [name: string]: string } = {};
700
+
701
+ for (const browser of browsers) {
702
+ try {
703
+ switch (browser) {
704
+ case 'chromium':
705
+ browserVersions[browser] = (await chromium.launch()).version();
706
+ break;
707
+ case 'firefox':
708
+ browserVersions[browser] = (await firefox.launch()).version();
709
+ break;
710
+ case 'webkit':
711
+ browserVersions[browser] = (await webkit.launch()).version();
712
+ break;
713
+ }
714
+ } catch (e) {
715
+ // pass not installed browser
716
+ }
717
+ }
718
+
719
+ return {
720
+ browsers: browserVersions,
721
+ benchmark: {
722
+ BENCHMARK_OUTPUTFILE: this._outputFile,
723
+ BENCHMARK_REFERENCE: this._reference
724
+ },
725
+ systemInformation: {
726
+ cpu: cpu,
727
+ mem: { total: totalMemory },
728
+ osInfo: osInfo
729
+ }
730
+ };
731
+ }
732
+
733
+ protected async buildReport(): Promise<IReport> {
734
+ return {
735
+ values: this._report,
736
+ metadata: await this.getMetadata()
737
+ };
738
+ }
739
+
740
+ protected config: FullConfig;
741
+ protected suite: Suite;
742
+ private _comparison: 'snapshot' | 'project';
743
+ private _expectedReference: string;
744
+ private _outputFile: string;
745
+ private _reference: string;
746
+ private _report: IReportRecord[];
747
+ private _buildVegaLiteGraph: (
748
+ allData: Array<IReportRecord>,
749
+ comparison: 'snapshot' | 'project'
750
+ ) => Record<string, any>;
751
+ private _buildTextReport: (
752
+ allData: Array<IReportRecord>
753
+ ) => Promise<[string, string]>;
754
+ }
755
+
756
+ export default BenchmarkReporter;