flexi-bench 0.1.0 → 0.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 +19 -0
- package/README.md +276 -3
- package/dist/api-types.d.ts +20 -0
- package/dist/benchmark.js +12 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/reporters/benchmark-console-reporter.d.ts +4 -1
- package/dist/reporters/benchmark-console-reporter.js +21 -9
- package/dist/reporters/composite-reporter.d.ts +21 -0
- package/dist/reporters/composite-reporter.js +29 -0
- package/dist/reporters/json-suite-reporter.d.ts +26 -0
- package/dist/reporters/json-suite-reporter.js +38 -0
- package/dist/reporters/markdown-benchmark-reporter.d.ts +13 -1
- package/dist/reporters/markdown-benchmark-reporter.js +145 -9
- package/dist/reporters/markdown-suite-reporter.d.ts +16 -0
- package/dist/reporters/markdown-suite-reporter.js +140 -0
- package/dist/reporters/suite-console-reporter.d.ts +4 -0
- package/dist/reporters/suite-console-reporter.js +16 -1
- package/dist/results.d.ts +32 -1
- package/dist/results.js +46 -1
- package/dist/suite.js +5 -1
- package/dist/variation.d.ts +62 -0
- package/dist/variation.js +73 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 0.2.0 (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 🚀 Features
|
|
5
|
+
|
|
6
|
+
- add result metadata support (iterations, totalDuration, benchmarkName, variationName) ([22dfc33](https://github.com/agentender/flexi-bench/commit/22dfc33))
|
|
7
|
+
- improve console reporters with metadata display ([bfb4482](https://github.com/agentender/flexi-bench/commit/bfb4482))
|
|
8
|
+
- add composite and JSON suite reporters with examples ([c29f381](https://github.com/agentender/flexi-bench/commit/c29f381))
|
|
9
|
+
- include git info on results when in CI ([3b2a63d](https://github.com/agentender/flexi-bench/commit/3b2a63d))
|
|
10
|
+
- **docs-site:** upload algolia settings ([aea3eff](https://github.com/agentender/flexi-bench/commit/aea3eff))
|
|
11
|
+
|
|
12
|
+
### 🩹 Fixes
|
|
13
|
+
|
|
14
|
+
- **e2e:** ensure output directory exists and improve error handling ([c0e79ea](https://github.com/agentender/flexi-bench/commit/c0e79ea))
|
|
15
|
+
|
|
16
|
+
### ❤️ Thank You
|
|
17
|
+
|
|
18
|
+
- Craigory Coppola @AgentEnder
|
|
19
|
+
|
|
1
20
|
## 0.1.0 (2024-07-28)
|
|
2
21
|
|
|
3
22
|
|
package/README.md
CHANGED
|
@@ -332,6 +332,50 @@ const benchmark = new Benchmark('My Benchmark', {
|
|
|
332
332
|
);
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
#### Variation Context
|
|
336
|
+
|
|
337
|
+
For more complex scenarios where you need to pass objects or data directly to the benchmark action, use the context API instead of environment variables:
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const { Benchmark, Variation } = require('flexi-bench');
|
|
341
|
+
|
|
342
|
+
// Define different implementations
|
|
343
|
+
const loopProcessor = {
|
|
344
|
+
process: (data) => {
|
|
345
|
+
/* loop */
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const reduceProcessor = {
|
|
349
|
+
process: (data) => {
|
|
350
|
+
/* reduce */
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Use FromContexts for clean, declarative variation setup
|
|
355
|
+
const benchmark = new Benchmark('Process Data')
|
|
356
|
+
.withIterations(100)
|
|
357
|
+
.withVariations(
|
|
358
|
+
Variation.FromContexts('processor', [
|
|
359
|
+
['loop', loopProcessor],
|
|
360
|
+
['reduce', reduceProcessor],
|
|
361
|
+
]),
|
|
362
|
+
)
|
|
363
|
+
.withAction((variation) => {
|
|
364
|
+
// Use get() to retrieve context data
|
|
365
|
+
const processor = variation.get('processor');
|
|
366
|
+
processor.process(data);
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The context API provides:
|
|
371
|
+
|
|
372
|
+
- `Variation.FromContexts(key, [[name, value], ...])` - Create variations with context (cleanest API)
|
|
373
|
+
- `withContext(key, value)` - Attach custom data to a single variation
|
|
374
|
+
- `get(key)` - Retrieve context data (returns `T | undefined`)
|
|
375
|
+
- `getOrDefault(key, defaultValue)` - Retrieve with fallback value
|
|
376
|
+
|
|
377
|
+
Use `FromContexts` when creating multiple variations with the same context key - it's cleaner and more concise than individual `withVariation` calls with `withContext`.
|
|
378
|
+
|
|
335
379
|
Variations can also be added to suites. Variations added to a suite will be applied to all benchmarks in the suite.
|
|
336
380
|
|
|
337
381
|
For example, the below suite would run each benchmark with 'NO_DAEMON' set to true, and then with 'OTHER_VAR' set to 'value1' for a total of 4 benchmark runs in the suite:
|
|
@@ -364,10 +408,239 @@ const suite = new Suite('My Suite')
|
|
|
364
408
|
);
|
|
365
409
|
```
|
|
366
410
|
|
|
411
|
+
## Reporters
|
|
412
|
+
|
|
413
|
+
Reporters control how benchmark results are output. FlexiBench provides several built-in reporters:
|
|
414
|
+
|
|
415
|
+
### Console Reporters
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
const {
|
|
419
|
+
Benchmark,
|
|
420
|
+
BenchmarkConsoleReporter,
|
|
421
|
+
SuiteConsoleReporter,
|
|
422
|
+
} = require('flexi-bench');
|
|
423
|
+
|
|
424
|
+
// For single benchmarks
|
|
425
|
+
const benchmark = new Benchmark('My Benchmark', {
|
|
426
|
+
iterations: 10,
|
|
427
|
+
action: () => {
|
|
428
|
+
/* ... */
|
|
429
|
+
},
|
|
430
|
+
reporter: new BenchmarkConsoleReporter(),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// For suites
|
|
434
|
+
const suite = new Suite('My Suite')
|
|
435
|
+
.withReporter(new SuiteConsoleReporter())
|
|
436
|
+
.addBenchmark(benchmark);
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Both console reporters support the `NO_COLOR` environment variable for disabling colors:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
NO_COLOR=1 node my-benchmark.js
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Or explicitly via options:
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
new SuiteConsoleReporter({ noColor: true });
|
|
449
|
+
new BenchmarkConsoleReporter({ noColor: true });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Markdown Reporters
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
const {
|
|
456
|
+
MarkdownBenchmarkReporter,
|
|
457
|
+
MarkdownSuiteReporter,
|
|
458
|
+
} = require('flexi-bench');
|
|
459
|
+
|
|
460
|
+
// For single benchmark output
|
|
461
|
+
const benchmarkReporter = new MarkdownBenchmarkReporter({
|
|
462
|
+
outputFile: 'results.md',
|
|
463
|
+
fields: ['min', 'average', 'p95', 'max'],
|
|
464
|
+
append: true, // Set to true to avoid overwriting when running multiple benchmarks
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// For suite-level output (recommended)
|
|
468
|
+
const suiteReporter = new MarkdownSuiteReporter({
|
|
469
|
+
outputFile: 'results.md',
|
|
470
|
+
title: 'Benchmark Results',
|
|
471
|
+
fields: ['min', 'average', 'p95', 'max', 'iterations'],
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### Automatic Comparison Tables
|
|
476
|
+
|
|
477
|
+
When a benchmark has multiple variations, both `MarkdownBenchmarkReporter` and `MarkdownSuiteReporter` automatically generate a comparison table showing:
|
|
478
|
+
|
|
479
|
+
- Average time for each variation
|
|
480
|
+
- Percentage difference vs the fastest variation
|
|
481
|
+
- Multiplier (e.g., "2.5x" slower)
|
|
482
|
+
- Trophy emoji (🏆) marking the fastest variation
|
|
483
|
+
|
|
484
|
+
This makes it easy to see which implementation performs best at a glance.
|
|
485
|
+
|
|
486
|
+
### JSON Reporter
|
|
487
|
+
|
|
488
|
+
For CI/CD integration:
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
const { JsonSuiteReporter } = require('flexi-bench');
|
|
492
|
+
|
|
493
|
+
const reporter = new JsonSuiteReporter({
|
|
494
|
+
outputFile: 'results.json',
|
|
495
|
+
pretty: true,
|
|
496
|
+
includeMetadata: true, // Includes timestamp, platform, node version
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Composite Reporter
|
|
501
|
+
|
|
502
|
+
Use multiple reporters simultaneously:
|
|
503
|
+
|
|
504
|
+
```javascript
|
|
505
|
+
const {
|
|
506
|
+
CompositeReporter,
|
|
507
|
+
SuiteConsoleReporter,
|
|
508
|
+
MarkdownSuiteReporter,
|
|
509
|
+
JsonSuiteReporter,
|
|
510
|
+
} = require('flexi-bench');
|
|
511
|
+
|
|
512
|
+
const suite = new Suite('My Suite').withReporter(
|
|
513
|
+
new CompositeReporter([
|
|
514
|
+
new SuiteConsoleReporter(),
|
|
515
|
+
new MarkdownSuiteReporter({ outputFile: 'results.md' }),
|
|
516
|
+
new JsonSuiteReporter({ outputFile: 'results.json' }),
|
|
517
|
+
]),
|
|
518
|
+
);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Custom Reporters
|
|
522
|
+
|
|
523
|
+
Create custom reporters by implementing the `SuiteReporter` interface:
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import { SuiteReporter, Result } from 'flexi-bench';
|
|
527
|
+
|
|
528
|
+
class MyCustomReporter implements SuiteReporter {
|
|
529
|
+
// Optional lifecycle hooks
|
|
530
|
+
onSuiteStart?(suiteName: string): void {
|
|
531
|
+
console.log(`Starting suite: ${suiteName}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
onBenchmarkStart?(benchmarkName: string): void {
|
|
535
|
+
console.log(`Running: ${benchmarkName}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
onBenchmarkEnd?(benchmarkName: string, results: Result[]): void {
|
|
539
|
+
console.log(`Completed: ${benchmarkName}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Required: called after all benchmarks complete
|
|
543
|
+
report(results: Record<string, Result[]>): void {
|
|
544
|
+
// Process results here
|
|
545
|
+
for (const [name, result] of Object.entries(results)) {
|
|
546
|
+
console.log(`${name}: ${result[0].average}ms average`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Result Type
|
|
553
|
+
|
|
554
|
+
The `Result` type contains comprehensive information about benchmark runs:
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
interface Result {
|
|
558
|
+
// Basic metrics
|
|
559
|
+
label: string; // Name of benchmark or variation
|
|
560
|
+
min: number; // Minimum duration (ms)
|
|
561
|
+
max: number; // Maximum duration (ms)
|
|
562
|
+
average: number; // Average duration (ms)
|
|
563
|
+
p95: number; // 95th percentile duration (ms)
|
|
564
|
+
raw: (number | Error)[]; // Raw durations/errors
|
|
565
|
+
|
|
566
|
+
// Failure information
|
|
567
|
+
failed?: boolean; // Whether any iteration failed
|
|
568
|
+
failureRate?: number; // Rate of failures (0-1)
|
|
569
|
+
|
|
570
|
+
// Metadata (useful for custom reporters)
|
|
571
|
+
iterations?: number; // Number of iterations run
|
|
572
|
+
totalDuration?: number; // Total wall-clock time (ms)
|
|
573
|
+
benchmarkName?: string; // Name of parent benchmark
|
|
574
|
+
variationName?: string; // Name of variation
|
|
575
|
+
|
|
576
|
+
// Subresults from performance observer
|
|
577
|
+
subresults?: Result[];
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The Result type is exported from the main package:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
import { Result } from 'flexi-bench';
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Cookbook
|
|
588
|
+
|
|
589
|
+
### Benchmarking Multiple Implementations
|
|
590
|
+
|
|
591
|
+
Compare different implementations of the same interface:
|
|
592
|
+
|
|
593
|
+
```javascript
|
|
594
|
+
const {
|
|
595
|
+
Suite,
|
|
596
|
+
Benchmark,
|
|
597
|
+
Variation,
|
|
598
|
+
MarkdownSuiteReporter,
|
|
599
|
+
} = require('flexi-bench');
|
|
600
|
+
|
|
601
|
+
// Define your implementations
|
|
602
|
+
const implementations = {
|
|
603
|
+
loop: (data) => {
|
|
604
|
+
/* loop implementation */
|
|
605
|
+
},
|
|
606
|
+
reduce: (data) => {
|
|
607
|
+
/* reduce implementation */
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const suite = new Suite('Implementation Comparison')
|
|
612
|
+
.withReporter(new MarkdownSuiteReporter({ outputFile: 'results.md' }))
|
|
613
|
+
.addBenchmark(
|
|
614
|
+
new Benchmark('Process Data')
|
|
615
|
+
.withIterations(100)
|
|
616
|
+
// Create variations with context - no environment variables needed!
|
|
617
|
+
.withVariations(
|
|
618
|
+
Variation.FromContexts('impl', [
|
|
619
|
+
['loop', implementations.loop],
|
|
620
|
+
['reduce', implementations.reduce],
|
|
621
|
+
]),
|
|
622
|
+
)
|
|
623
|
+
.withAction((variation) => {
|
|
624
|
+
// Retrieve the implementation directly from context
|
|
625
|
+
const impl = variation.get('impl');
|
|
626
|
+
const data = [
|
|
627
|
+
/* test data */
|
|
628
|
+
];
|
|
629
|
+
impl(data);
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
```
|
|
633
|
+
|
|
367
634
|
## Examples
|
|
368
635
|
|
|
369
636
|
See examples folder.
|
|
370
637
|
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
-
|
|
638
|
+
- `./examples/benchmark.ts` - Full benchmark suite with environment variable variations
|
|
639
|
+
- `./examples/performance-observer.ts` - Using PerformanceObserver API
|
|
640
|
+
- `./examples/simple-command.ts` - Benchmarking CLI commands
|
|
641
|
+
- `./examples/multiple-reporters.ts` - Using CompositeReporter for multiple outputs
|
|
642
|
+
- `./examples/custom-reporter.ts` - Creating custom reporters with Result type
|
|
643
|
+
- `./examples/cookbook-different-implementations.ts` - Comparing implementations
|
|
644
|
+
- `./examples/no-color-support.ts` - Disabling colors in CI environments
|
|
645
|
+
- `./examples/markdown-reporter-append.ts` - Using append mode for MarkdownReporter
|
|
646
|
+
- `./examples/markdown-comparison.ts` - Demonstrates automatic comparison tables with variations
|
package/dist/api-types.d.ts
CHANGED
|
@@ -19,6 +19,26 @@ export interface BenchmarkReporter {
|
|
|
19
19
|
report: (benchmark: Benchmark, results: Result[]) => void;
|
|
20
20
|
}
|
|
21
21
|
export interface SuiteReporter {
|
|
22
|
+
/**
|
|
23
|
+
* Called before the suite starts running.
|
|
24
|
+
* @param suiteName - The name of the suite
|
|
25
|
+
*/
|
|
26
|
+
onSuiteStart?: (suiteName: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Called before each benchmark starts.
|
|
29
|
+
* @param benchmarkName - The name of the benchmark
|
|
30
|
+
*/
|
|
31
|
+
onBenchmarkStart?: (benchmarkName: string) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Called after each benchmark completes.
|
|
34
|
+
* @param benchmarkName - The name of the benchmark
|
|
35
|
+
* @param results - The results for the benchmark
|
|
36
|
+
*/
|
|
37
|
+
onBenchmarkEnd?: (benchmarkName: string, results: Result[]) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Called after all benchmarks complete.
|
|
40
|
+
* @param results - All benchmark results keyed by benchmark name
|
|
41
|
+
*/
|
|
22
42
|
report: (results: Record<string, Result[]>) => void;
|
|
23
43
|
}
|
|
24
44
|
/**
|
package/dist/benchmark.js
CHANGED
|
@@ -94,6 +94,7 @@ class Benchmark extends shared_api_1.BenchmarkBase {
|
|
|
94
94
|
let totalCompletedIterations = 0;
|
|
95
95
|
for (let variationIndex = 0; variationIndex < this.variations.length; variationIndex++) {
|
|
96
96
|
const variation = this.variations[variationIndex];
|
|
97
|
+
const variationStartTime = performance.now();
|
|
97
98
|
const iterationResults = [];
|
|
98
99
|
// SETUP
|
|
99
100
|
const oldEnv = { ...process.env };
|
|
@@ -175,13 +176,22 @@ class Benchmark extends shared_api_1.BenchmarkBase {
|
|
|
175
176
|
}
|
|
176
177
|
process.env = oldEnv;
|
|
177
178
|
// REPORT
|
|
178
|
-
const
|
|
179
|
+
const variationEndTime = performance.now();
|
|
180
|
+
const result = (0, results_1.calculateResultsFromDurations)(variation.name, iterationResults, {
|
|
181
|
+
iterations: iterationResults.length,
|
|
182
|
+
totalDuration: variationEndTime - variationStartTime,
|
|
183
|
+
benchmarkName: this.name,
|
|
184
|
+
variationName: variation.name,
|
|
185
|
+
});
|
|
179
186
|
// PerformanceObserver needs a chance to flush
|
|
180
187
|
if (this.watcher) {
|
|
181
188
|
const measures = await this.watcher.getMeasures();
|
|
182
189
|
for (const key in measures) {
|
|
183
190
|
result.subresults ??= [];
|
|
184
|
-
result.subresults.push((0, results_1.calculateResultsFromDurations)(key, measures[key]
|
|
191
|
+
result.subresults.push((0, results_1.calculateResultsFromDurations)(key, measures[key], {
|
|
192
|
+
benchmarkName: this.name,
|
|
193
|
+
variationName: variation.name,
|
|
194
|
+
}));
|
|
185
195
|
}
|
|
186
196
|
this.watcher.clearMeasures();
|
|
187
197
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export * from './reporters/benchmark-console-reporter';
|
|
2
2
|
export * from './reporters/markdown-benchmark-reporter';
|
|
3
|
+
export * from './reporters/markdown-suite-reporter';
|
|
3
4
|
export * from './reporters/suite-console-reporter';
|
|
4
5
|
export * from './reporters/noop-reporter';
|
|
6
|
+
export * from './reporters/composite-reporter';
|
|
7
|
+
export * from './reporters/json-suite-reporter';
|
|
5
8
|
export * from './api-types';
|
|
6
9
|
export * from './benchmark';
|
|
7
10
|
export * from './variation';
|
|
8
11
|
export * from './suite';
|
|
9
12
|
export * from './performance-observer';
|
|
10
13
|
export * from './benchmark-runner';
|
|
14
|
+
export * from './results';
|
package/dist/index.js
CHANGED
|
@@ -16,11 +16,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./reporters/benchmark-console-reporter"), exports);
|
|
18
18
|
__exportStar(require("./reporters/markdown-benchmark-reporter"), exports);
|
|
19
|
+
__exportStar(require("./reporters/markdown-suite-reporter"), exports);
|
|
19
20
|
__exportStar(require("./reporters/suite-console-reporter"), exports);
|
|
20
21
|
__exportStar(require("./reporters/noop-reporter"), exports);
|
|
22
|
+
__exportStar(require("./reporters/composite-reporter"), exports);
|
|
23
|
+
__exportStar(require("./reporters/json-suite-reporter"), exports);
|
|
21
24
|
__exportStar(require("./api-types"), exports);
|
|
22
25
|
__exportStar(require("./benchmark"), exports);
|
|
23
26
|
__exportStar(require("./variation"), exports);
|
|
24
27
|
__exportStar(require("./suite"), exports);
|
|
25
28
|
__exportStar(require("./performance-observer"), exports);
|
|
26
29
|
__exportStar(require("./benchmark-runner"), exports);
|
|
30
|
+
__exportStar(require("./results"), exports);
|
|
@@ -3,8 +3,11 @@ import { Benchmark } from '../benchmark';
|
|
|
3
3
|
import { SingleBar } from 'cli-progress';
|
|
4
4
|
import { Result } from '../results';
|
|
5
5
|
export declare class BenchmarkConsoleReporter implements BenchmarkReporter {
|
|
6
|
+
private noColor;
|
|
6
7
|
bar: SingleBar;
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(opts?: {
|
|
9
|
+
noColor?: boolean;
|
|
10
|
+
});
|
|
8
11
|
progress(name: string, percent: number, context: ProgressContext): void;
|
|
9
12
|
report(benchmark: Benchmark, results: Result[]): void;
|
|
10
13
|
}
|
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BenchmarkConsoleReporter = void 0;
|
|
4
4
|
const cli_progress_1 = require("cli-progress");
|
|
5
|
+
function getNoColorOption(explicitNoColor) {
|
|
6
|
+
if (explicitNoColor !== undefined) {
|
|
7
|
+
return explicitNoColor;
|
|
8
|
+
}
|
|
9
|
+
return process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
|
|
10
|
+
}
|
|
5
11
|
class BenchmarkConsoleReporter {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
noColor;
|
|
13
|
+
bar;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.noColor = getNoColorOption(opts?.noColor);
|
|
16
|
+
this.bar = new cli_progress_1.SingleBar({
|
|
17
|
+
format: this.noColor
|
|
18
|
+
? 'Running variation {label}: [{bar}] {percentage}% | {value}/{total} - ETA: {eta}s'
|
|
19
|
+
: 'Running variation {label}: {bar} {percentage}% | {value}/{total} - ETA: {eta}s',
|
|
20
|
+
barCompleteChar: this.noColor ? '#' : '\u2588',
|
|
21
|
+
barIncompleteChar: this.noColor ? '-' : '\u2591',
|
|
22
|
+
hideCursor: true,
|
|
23
|
+
stopOnComplete: true,
|
|
24
|
+
clearOnComplete: true,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
15
27
|
progress(name, percent, context) {
|
|
16
28
|
if (!this.bar.isActive) {
|
|
17
29
|
this.bar.start(context.totalIterations ?? 100, 0);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SuiteReporter } from '../api-types';
|
|
2
|
+
import { Result } from '../results';
|
|
3
|
+
/**
|
|
4
|
+
* A reporter that chains multiple suite reporters together.
|
|
5
|
+
* Useful when you want to output results to multiple destinations
|
|
6
|
+
* (e.g., console and file) simultaneously.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* suite.withReporter(new CompositeReporter([
|
|
11
|
+
* new SuiteConsoleReporter(),
|
|
12
|
+
* new MarkdownSuiteReporter({ outputFile: 'results.md' }),
|
|
13
|
+
* new JsonSuiteReporter({ outputFile: 'results.json' }),
|
|
14
|
+
* ]))
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare class CompositeReporter implements SuiteReporter {
|
|
18
|
+
private reporters;
|
|
19
|
+
constructor(reporters: SuiteReporter[]);
|
|
20
|
+
report: (results: Record<string, Result[]>) => void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompositeReporter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A reporter that chains multiple suite reporters together.
|
|
6
|
+
* Useful when you want to output results to multiple destinations
|
|
7
|
+
* (e.g., console and file) simultaneously.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* suite.withReporter(new CompositeReporter([
|
|
12
|
+
* new SuiteConsoleReporter(),
|
|
13
|
+
* new MarkdownSuiteReporter({ outputFile: 'results.md' }),
|
|
14
|
+
* new JsonSuiteReporter({ outputFile: 'results.json' }),
|
|
15
|
+
* ]))
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
class CompositeReporter {
|
|
19
|
+
reporters;
|
|
20
|
+
constructor(reporters) {
|
|
21
|
+
this.reporters = reporters;
|
|
22
|
+
}
|
|
23
|
+
report = (results) => {
|
|
24
|
+
for (const reporter of this.reporters) {
|
|
25
|
+
reporter.report(results);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
exports.CompositeReporter = CompositeReporter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SuiteReporter } from '../api-types';
|
|
2
|
+
import { Result } from '../results';
|
|
3
|
+
export interface JsonSuiteReporterOptions {
|
|
4
|
+
outputFile: string;
|
|
5
|
+
pretty?: boolean;
|
|
6
|
+
includeMetadata?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A reporter that outputs benchmark results as JSON.
|
|
10
|
+
* Useful for CI/CD integration and programmatic analysis.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* suite.withReporter(new JsonSuiteReporter({
|
|
15
|
+
* outputFile: 'results.json',
|
|
16
|
+
* pretty: true,
|
|
17
|
+
* }))
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class JsonSuiteReporter implements SuiteReporter {
|
|
21
|
+
private outputFile;
|
|
22
|
+
private pretty;
|
|
23
|
+
private includeMetadata;
|
|
24
|
+
constructor(opts: JsonSuiteReporterOptions);
|
|
25
|
+
report: (results: Record<string, Result[]>) => void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonSuiteReporter = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
/**
|
|
6
|
+
* A reporter that outputs benchmark results as JSON.
|
|
7
|
+
* Useful for CI/CD integration and programmatic analysis.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* suite.withReporter(new JsonSuiteReporter({
|
|
12
|
+
* outputFile: 'results.json',
|
|
13
|
+
* pretty: true,
|
|
14
|
+
* }))
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
class JsonSuiteReporter {
|
|
18
|
+
outputFile;
|
|
19
|
+
pretty;
|
|
20
|
+
includeMetadata;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.outputFile = opts.outputFile;
|
|
23
|
+
this.pretty = opts.pretty ?? false;
|
|
24
|
+
this.includeMetadata = opts.includeMetadata ?? true;
|
|
25
|
+
}
|
|
26
|
+
report = (results) => {
|
|
27
|
+
const output = this.includeMetadata
|
|
28
|
+
? {
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
platform: process.platform,
|
|
31
|
+
nodeVersion: process.version,
|
|
32
|
+
results,
|
|
33
|
+
}
|
|
34
|
+
: results;
|
|
35
|
+
(0, fs_1.writeFileSync)(this.outputFile, JSON.stringify(output, null, this.pretty ? 2 : undefined));
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
exports.JsonSuiteReporter = JsonSuiteReporter;
|
|
@@ -3,10 +3,22 @@ import { Benchmark } from '../benchmark';
|
|
|
3
3
|
import { Result } from '../results';
|
|
4
4
|
export declare class MarkdownBenchmarkReporter implements BenchmarkReporter {
|
|
5
5
|
outputFile: string;
|
|
6
|
-
fields: Array<keyof Omit<Result, 'subresults' | 'label'>>;
|
|
6
|
+
fields: Array<keyof Omit<Result, 'subresults' | 'label' | 'raw'>>;
|
|
7
|
+
append: boolean;
|
|
8
|
+
private isFirstReport;
|
|
9
|
+
private accumulatedResults;
|
|
7
10
|
constructor(opts: {
|
|
8
11
|
outputFile: string;
|
|
9
12
|
fields?: MarkdownBenchmarkReporter['fields'];
|
|
13
|
+
append?: boolean;
|
|
10
14
|
});
|
|
15
|
+
private formatDuration;
|
|
11
16
|
report: (benchmark: Benchmark, results: Result[]) => void;
|
|
17
|
+
private generateBenchmarkContent;
|
|
18
|
+
private generateResultsTable;
|
|
19
|
+
private generateComparison;
|
|
20
|
+
/**
|
|
21
|
+
* Clears the output file. Useful when re-running benchmarks.
|
|
22
|
+
*/
|
|
23
|
+
clear(): void;
|
|
12
24
|
}
|
|
@@ -6,24 +6,160 @@ const markdown_factory_1 = require("markdown-factory");
|
|
|
6
6
|
class MarkdownBenchmarkReporter {
|
|
7
7
|
outputFile;
|
|
8
8
|
fields;
|
|
9
|
+
append;
|
|
10
|
+
isFirstReport = true;
|
|
11
|
+
accumulatedResults = new Map();
|
|
9
12
|
constructor(opts) {
|
|
10
13
|
this.outputFile = opts.outputFile;
|
|
11
14
|
this.fields = opts.fields ?? ['min', 'average', 'p95', 'max'];
|
|
15
|
+
this.append = opts.append ?? false;
|
|
16
|
+
}
|
|
17
|
+
formatDuration(value) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return '';
|
|
20
|
+
const totalMs = value;
|
|
21
|
+
const hours = Math.floor(totalMs / (1000 * 60 * 60));
|
|
22
|
+
const remainingAfterHours = totalMs % (1000 * 60 * 60);
|
|
23
|
+
const minutes = Math.floor(remainingAfterHours / (1000 * 60));
|
|
24
|
+
const remainingAfterMinutes = remainingAfterHours % (1000 * 60);
|
|
25
|
+
const seconds = Math.floor(remainingAfterMinutes / 1000);
|
|
26
|
+
const milliseconds = remainingAfterMinutes % 1000;
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (hours > 0) {
|
|
29
|
+
parts.push(`${hours}h`);
|
|
30
|
+
if (minutes > 0)
|
|
31
|
+
parts.push(`${minutes}m`);
|
|
32
|
+
if (seconds > 0 || milliseconds > 0) {
|
|
33
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
34
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (minutes > 0) {
|
|
38
|
+
parts.push(`${minutes}m`);
|
|
39
|
+
if (seconds > 0 || milliseconds > 0) {
|
|
40
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
41
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (seconds > 0) {
|
|
45
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
46
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
parts.push(`${milliseconds.toFixed(1)}ms`);
|
|
50
|
+
}
|
|
51
|
+
return parts.join(' ');
|
|
12
52
|
}
|
|
13
53
|
report = (benchmark, results) => {
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
if (this.append) {
|
|
55
|
+
// In append mode, accumulate results and write incrementally
|
|
56
|
+
this.accumulatedResults.set(benchmark.name, results);
|
|
57
|
+
const content = this.generateBenchmarkContent(benchmark, results);
|
|
58
|
+
if (this.isFirstReport) {
|
|
59
|
+
// First report: clear the file and write header
|
|
60
|
+
(0, fs_1.writeFileSync)(this.outputFile, content);
|
|
61
|
+
this.isFirstReport = false;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Subsequent reports: append with a separator
|
|
65
|
+
(0, fs_1.appendFileSync)(this.outputFile, '\n\n---\n\n' + content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Legacy mode: overwrite file (useful for single benchmark reporting)
|
|
70
|
+
(0, fs_1.writeFileSync)(this.outputFile, this.generateBenchmarkContent(benchmark, results));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
generateBenchmarkContent(benchmark, results) {
|
|
74
|
+
const sections = [];
|
|
75
|
+
// Main results section
|
|
76
|
+
sections.push(this.generateResultsTable(results));
|
|
77
|
+
// Comparison section (if multiple variations)
|
|
78
|
+
if (results.length > 1) {
|
|
79
|
+
sections.push(this.generateComparison(results));
|
|
80
|
+
}
|
|
81
|
+
return (0, markdown_factory_1.h1)(benchmark.name, (0, markdown_factory_1.lines)(sections));
|
|
82
|
+
}
|
|
83
|
+
generateResultsTable(results) {
|
|
84
|
+
const fieldConfigs = [
|
|
85
|
+
{ field: 'label', label: '' },
|
|
86
|
+
...this.fields.map((field) => ({
|
|
87
|
+
field: field,
|
|
88
|
+
label: field,
|
|
89
|
+
mapFn: (item) => {
|
|
90
|
+
const value = item[field];
|
|
91
|
+
if (field === 'iterations') {
|
|
92
|
+
return String(value ?? '');
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === 'number') {
|
|
95
|
+
return this.formatDuration(value);
|
|
96
|
+
}
|
|
97
|
+
return String(value ?? '');
|
|
98
|
+
},
|
|
99
|
+
})),
|
|
100
|
+
];
|
|
101
|
+
if (results.some((r) => !!r.subresults)) {
|
|
102
|
+
return (0, markdown_factory_1.lines)(results.map((r) => {
|
|
16
103
|
const entries = [{ ...r, label: 'total' }];
|
|
17
104
|
delete entries[0].subresults;
|
|
18
105
|
for (const subresult of r.subresults ?? []) {
|
|
19
106
|
entries.push(subresult);
|
|
20
107
|
}
|
|
21
|
-
return (0, markdown_factory_1.h2)(r.label, (0, markdown_factory_1.table)(entries,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
108
|
+
return (0, markdown_factory_1.h2)(r.label, (0, markdown_factory_1.table)(entries, fieldConfigs));
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
return (0, markdown_factory_1.table)(results, fieldConfigs);
|
|
112
|
+
}
|
|
113
|
+
generateComparison(results) {
|
|
114
|
+
const sorted = [...results].sort((a, b) => a.average - b.average);
|
|
115
|
+
const fastest = sorted[0];
|
|
116
|
+
const hasVaryingIterations = results.some((r) => r.iterations !== results[0].iterations);
|
|
117
|
+
const entries = sorted.map((result, index) => {
|
|
118
|
+
const entry = {
|
|
119
|
+
label: result.label,
|
|
120
|
+
indicator: index === 0 ? '🏆' : '',
|
|
121
|
+
};
|
|
122
|
+
for (const field of this.fields) {
|
|
123
|
+
if (field === 'iterations') {
|
|
124
|
+
if (hasVaryingIterations) {
|
|
125
|
+
entry.iterations = String(result.iterations);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const value = result[field];
|
|
130
|
+
const fastestValue = fastest[field];
|
|
131
|
+
if (index === 0) {
|
|
132
|
+
entry[field] = 'baseline';
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const factor = value / fastestValue;
|
|
136
|
+
entry[field] = `${factor.toFixed(2)}x slower`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return entry;
|
|
141
|
+
});
|
|
142
|
+
const columns = [
|
|
143
|
+
{ field: 'label', label: 'Variation' },
|
|
144
|
+
...this.fields
|
|
145
|
+
.filter((field) => field !== 'iterations' || hasVaryingIterations)
|
|
146
|
+
.map((field) => ({ field, label: field })),
|
|
147
|
+
{ field: 'indicator', label: '' },
|
|
148
|
+
];
|
|
149
|
+
const header = hasVaryingIterations
|
|
150
|
+
? 'Comparison'
|
|
151
|
+
: `Comparison (${results[0].iterations} iterations)`;
|
|
152
|
+
return (0, markdown_factory_1.h3)(header, (0, markdown_factory_1.table)(entries.map((e) => ({ ...e })), columns));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Clears the output file. Useful when re-running benchmarks.
|
|
156
|
+
*/
|
|
157
|
+
clear() {
|
|
158
|
+
if ((0, fs_1.existsSync)(this.outputFile)) {
|
|
159
|
+
(0, fs_1.unlinkSync)(this.outputFile);
|
|
160
|
+
}
|
|
161
|
+
this.isFirstReport = true;
|
|
162
|
+
this.accumulatedResults.clear();
|
|
163
|
+
}
|
|
28
164
|
}
|
|
29
165
|
exports.MarkdownBenchmarkReporter = MarkdownBenchmarkReporter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SuiteReporter } from '../api-types';
|
|
2
|
+
import { Result } from '../results';
|
|
3
|
+
export declare class MarkdownSuiteReporter implements SuiteReporter {
|
|
4
|
+
outputFile: string;
|
|
5
|
+
title: string;
|
|
6
|
+
fields: Array<keyof Omit<Result, 'subresults' | 'label' | 'raw'>>;
|
|
7
|
+
constructor(opts: {
|
|
8
|
+
outputFile: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
fields?: MarkdownSuiteReporter['fields'];
|
|
11
|
+
});
|
|
12
|
+
private formatDuration;
|
|
13
|
+
report: (results: Record<string, Result[]>) => void;
|
|
14
|
+
private renderBenchmarkSection;
|
|
15
|
+
private generateComparison;
|
|
16
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MarkdownSuiteReporter = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const markdown_factory_1 = require("markdown-factory");
|
|
6
|
+
class MarkdownSuiteReporter {
|
|
7
|
+
outputFile;
|
|
8
|
+
title;
|
|
9
|
+
fields;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.outputFile = opts.outputFile;
|
|
12
|
+
this.title = opts.title ?? 'Benchmark Results';
|
|
13
|
+
this.fields = opts.fields ?? ['min', 'average', 'p95', 'max'];
|
|
14
|
+
}
|
|
15
|
+
formatDuration(value) {
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return '';
|
|
18
|
+
const totalMs = value;
|
|
19
|
+
const hours = Math.floor(totalMs / (1000 * 60 * 60));
|
|
20
|
+
const remainingAfterHours = totalMs % (1000 * 60 * 60);
|
|
21
|
+
const minutes = Math.floor(remainingAfterHours / (1000 * 60));
|
|
22
|
+
const remainingAfterMinutes = remainingAfterHours % (1000 * 60);
|
|
23
|
+
const seconds = Math.floor(remainingAfterMinutes / 1000);
|
|
24
|
+
const milliseconds = remainingAfterMinutes % 1000;
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (hours > 0) {
|
|
27
|
+
parts.push(`${hours}h`);
|
|
28
|
+
if (minutes > 0)
|
|
29
|
+
parts.push(`${minutes}m`);
|
|
30
|
+
if (seconds > 0 || milliseconds > 0) {
|
|
31
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
32
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (minutes > 0) {
|
|
36
|
+
parts.push(`${minutes}m`);
|
|
37
|
+
if (seconds > 0 || milliseconds > 0) {
|
|
38
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
39
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (seconds > 0) {
|
|
43
|
+
const totalSeconds = seconds + milliseconds / 1000;
|
|
44
|
+
parts.push(`${totalSeconds.toFixed(1)}s`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
parts.push(`${milliseconds.toFixed(1)}ms`);
|
|
48
|
+
}
|
|
49
|
+
return parts.join(' ');
|
|
50
|
+
}
|
|
51
|
+
report = (results) => {
|
|
52
|
+
const benchmarkSections = Object.entries(results).map(([benchmarkName, benchmarkResults]) => {
|
|
53
|
+
return this.renderBenchmarkSection(benchmarkName, benchmarkResults);
|
|
54
|
+
});
|
|
55
|
+
(0, fs_1.writeFileSync)(this.outputFile, (0, markdown_factory_1.h1)(this.title, (0, markdown_factory_1.lines)(benchmarkSections)));
|
|
56
|
+
};
|
|
57
|
+
renderBenchmarkSection(benchmarkName, results) {
|
|
58
|
+
const hasSubresults = results.some((r) => !!r.subresults);
|
|
59
|
+
const hasMultipleVariations = results.length > 1;
|
|
60
|
+
const sections = [];
|
|
61
|
+
const fieldConfigs = [
|
|
62
|
+
{ field: 'label', label: '' },
|
|
63
|
+
...this.fields.map((field) => ({
|
|
64
|
+
field: field,
|
|
65
|
+
label: field,
|
|
66
|
+
mapFn: (item) => {
|
|
67
|
+
const value = item[field];
|
|
68
|
+
if (field === 'iterations') {
|
|
69
|
+
return String(value ?? '');
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === 'number') {
|
|
72
|
+
return this.formatDuration(value);
|
|
73
|
+
}
|
|
74
|
+
return String(value ?? '');
|
|
75
|
+
},
|
|
76
|
+
})),
|
|
77
|
+
];
|
|
78
|
+
// Main results table
|
|
79
|
+
if (hasSubresults) {
|
|
80
|
+
sections.push((0, markdown_factory_1.lines)(results.map((r) => {
|
|
81
|
+
const entries = [{ ...r, label: 'total' }];
|
|
82
|
+
delete entries[0].subresults;
|
|
83
|
+
for (const subresult of r.subresults ?? []) {
|
|
84
|
+
entries.push(subresult);
|
|
85
|
+
}
|
|
86
|
+
return (0, markdown_factory_1.h2)(r.label, (0, markdown_factory_1.table)(entries, fieldConfigs));
|
|
87
|
+
})));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
sections.push((0, markdown_factory_1.table)(results, fieldConfigs));
|
|
91
|
+
}
|
|
92
|
+
// Comparison section (if multiple variations)
|
|
93
|
+
if (hasMultipleVariations) {
|
|
94
|
+
sections.push(this.generateComparison(results));
|
|
95
|
+
}
|
|
96
|
+
return (0, markdown_factory_1.h2)(benchmarkName, (0, markdown_factory_1.lines)(sections));
|
|
97
|
+
}
|
|
98
|
+
generateComparison(results) {
|
|
99
|
+
const sorted = [...results].sort((a, b) => a.average - b.average);
|
|
100
|
+
const fastest = sorted[0];
|
|
101
|
+
const hasVaryingIterations = results.some((r) => r.iterations !== results[0].iterations);
|
|
102
|
+
const entries = sorted.map((result, index) => {
|
|
103
|
+
const entry = {
|
|
104
|
+
label: result.label,
|
|
105
|
+
indicator: index === 0 ? '🏆' : '',
|
|
106
|
+
};
|
|
107
|
+
for (const field of this.fields) {
|
|
108
|
+
if (field === 'iterations') {
|
|
109
|
+
if (hasVaryingIterations) {
|
|
110
|
+
entry.iterations = String(result.iterations);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const value = result[field];
|
|
115
|
+
const fastestValue = fastest[field];
|
|
116
|
+
if (index === 0) {
|
|
117
|
+
entry[field] = 'baseline';
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const factor = value / fastestValue;
|
|
121
|
+
entry[field] = `${factor.toFixed(2)}x slower`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return entry;
|
|
126
|
+
});
|
|
127
|
+
const columns = [
|
|
128
|
+
{ field: 'label', label: 'Variation' },
|
|
129
|
+
...this.fields
|
|
130
|
+
.filter((field) => field !== 'iterations' || hasVaryingIterations)
|
|
131
|
+
.map((field) => ({ field, label: field })),
|
|
132
|
+
{ field: 'indicator', label: '' },
|
|
133
|
+
];
|
|
134
|
+
const header = hasVaryingIterations
|
|
135
|
+
? 'Comparison'
|
|
136
|
+
: `Comparison (${results[0].iterations} iterations)`;
|
|
137
|
+
return (0, markdown_factory_1.h3)(header, (0, markdown_factory_1.table)(entries.map((e) => ({ ...e })), columns));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.MarkdownSuiteReporter = MarkdownSuiteReporter;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { SuiteReporter } from '../api-types';
|
|
2
2
|
import { Result } from '../results';
|
|
3
3
|
export declare class SuiteConsoleReporter implements SuiteReporter {
|
|
4
|
+
private noColor;
|
|
5
|
+
constructor(opts?: {
|
|
6
|
+
noColor?: boolean;
|
|
7
|
+
});
|
|
4
8
|
report: (results: Record<string, Result[]>) => void;
|
|
5
9
|
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SuiteConsoleReporter = void 0;
|
|
4
|
+
function getNoColorOption(explicitNoColor) {
|
|
5
|
+
if (explicitNoColor !== undefined) {
|
|
6
|
+
return explicitNoColor;
|
|
7
|
+
}
|
|
8
|
+
return process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
|
|
9
|
+
}
|
|
4
10
|
class SuiteConsoleReporter {
|
|
11
|
+
noColor;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.noColor = getNoColorOption(opts?.noColor);
|
|
14
|
+
}
|
|
5
15
|
report = (results) => {
|
|
6
|
-
|
|
16
|
+
if (this.noColor) {
|
|
17
|
+
console.log('Suite Results:');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log('\x1b[1mSuite Results:\x1b[0m');
|
|
21
|
+
}
|
|
7
22
|
for (const [name, result] of Object.entries(results)) {
|
|
8
23
|
const tableEntries = result.map(({ raw, ...rest }) => ({
|
|
9
24
|
...rest,
|
package/dist/results.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export type GitInfo = {
|
|
2
|
+
head: string;
|
|
3
|
+
sha: string;
|
|
4
|
+
dirty: boolean;
|
|
5
|
+
};
|
|
1
6
|
/**
|
|
2
7
|
* A measurement result.
|
|
3
8
|
*/
|
|
@@ -38,5 +43,31 @@ export type Result = {
|
|
|
38
43
|
* Subresults, if any. Typically sourced from performance observer.
|
|
39
44
|
*/
|
|
40
45
|
subresults?: Result[];
|
|
46
|
+
/**
|
|
47
|
+
* The number of iterations that were run.
|
|
48
|
+
*/
|
|
49
|
+
iterations?: number;
|
|
50
|
+
/**
|
|
51
|
+
* The total wall-clock duration for all iterations in milliseconds.
|
|
52
|
+
*/
|
|
53
|
+
totalDuration?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The name of the benchmark that produced this result.
|
|
56
|
+
*/
|
|
57
|
+
benchmarkName?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The name of the variation that produced this result.
|
|
60
|
+
*/
|
|
61
|
+
variationName?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Git information about the repository state when this result was produced.
|
|
64
|
+
*/
|
|
65
|
+
git?: GitInfo;
|
|
41
66
|
};
|
|
42
|
-
export declare function
|
|
67
|
+
export declare function getGitInfo(): GitInfo | undefined;
|
|
68
|
+
export declare function calculateResultsFromDurations(label: string, durations: (number | Error)[], metadata?: {
|
|
69
|
+
iterations?: number;
|
|
70
|
+
totalDuration?: number;
|
|
71
|
+
benchmarkName?: string;
|
|
72
|
+
variationName?: string;
|
|
73
|
+
}): Result;
|
package/dist/results.js
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getGitInfo = getGitInfo;
|
|
3
7
|
exports.calculateResultsFromDurations = calculateResultsFromDurations;
|
|
4
|
-
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const is_ci_1 = __importDefault(require("is-ci"));
|
|
10
|
+
let cachedGitInfo = null;
|
|
11
|
+
function getGitInfo() {
|
|
12
|
+
if (cachedGitInfo !== null) {
|
|
13
|
+
return cachedGitInfo;
|
|
14
|
+
}
|
|
15
|
+
if (!is_ci_1.default) {
|
|
16
|
+
cachedGitInfo = undefined;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const getGitOutput = (args) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = (0, child_process_1.spawn)('git', args, {
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
});
|
|
24
|
+
let output = '';
|
|
25
|
+
result.stdout?.on('data', (data) => {
|
|
26
|
+
output += data.toString();
|
|
27
|
+
});
|
|
28
|
+
result.stdout?.on('end', () => {
|
|
29
|
+
return output.trim();
|
|
30
|
+
});
|
|
31
|
+
return output.trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const head = getGitOutput(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
38
|
+
const sha = getGitOutput(['rev-parse', 'HEAD']);
|
|
39
|
+
const dirty = getGitOutput(['status', '--porcelain']).length > 0;
|
|
40
|
+
cachedGitInfo = {
|
|
41
|
+
head,
|
|
42
|
+
sha,
|
|
43
|
+
dirty,
|
|
44
|
+
};
|
|
45
|
+
return cachedGitInfo;
|
|
46
|
+
}
|
|
47
|
+
function calculateResultsFromDurations(label, durations, metadata) {
|
|
5
48
|
const errors = [];
|
|
6
49
|
const results = [];
|
|
7
50
|
for (const duration of durations) {
|
|
@@ -22,5 +65,7 @@ function calculateResultsFromDurations(label, durations) {
|
|
|
22
65
|
raw: durations,
|
|
23
66
|
failed: errors.length > 0,
|
|
24
67
|
failureRate: errors.length / durations.length,
|
|
68
|
+
...metadata,
|
|
69
|
+
...(getGitInfo() ? { git: getGitInfo() } : {}),
|
|
25
70
|
};
|
|
26
71
|
}
|
package/dist/suite.js
CHANGED
|
@@ -50,8 +50,10 @@ class Suite {
|
|
|
50
50
|
return this;
|
|
51
51
|
}
|
|
52
52
|
async run() {
|
|
53
|
+
this.reporter.onSuiteStart?.(this.name);
|
|
53
54
|
const results = {};
|
|
54
55
|
for (const benchmark of this.benchmarks) {
|
|
56
|
+
this.reporter.onBenchmarkStart?.(benchmark.name);
|
|
55
57
|
benchmark.withVariations(this.variations);
|
|
56
58
|
if (this.benchmarkReporter) {
|
|
57
59
|
benchmark.withReporter(this.benchmarkReporter);
|
|
@@ -59,7 +61,9 @@ class Suite {
|
|
|
59
61
|
if (this.shouldSetErrorStrategyOnBenchmarks) {
|
|
60
62
|
benchmark.withErrorStrategy(this.errorStrategy);
|
|
61
63
|
}
|
|
62
|
-
|
|
64
|
+
const benchmarkResults = await benchmark.run();
|
|
65
|
+
results[benchmark.name] = benchmarkResults;
|
|
66
|
+
this.reporter.onBenchmarkEnd?.(benchmark.name, benchmarkResults);
|
|
63
67
|
}
|
|
64
68
|
this.reporter.report(results);
|
|
65
69
|
if (this.errorStrategy === api_types_1.ErrorStrategy.DelayedThrow) {
|
package/dist/variation.d.ts
CHANGED
|
@@ -4,7 +4,46 @@ export declare class Variation extends BenchmarkBase {
|
|
|
4
4
|
name: string;
|
|
5
5
|
environment: Partial<NodeJS.ProcessEnv>;
|
|
6
6
|
cliArgs: string[];
|
|
7
|
+
private context;
|
|
7
8
|
constructor(name: string);
|
|
9
|
+
/**
|
|
10
|
+
* Sets a value in the variation's context.
|
|
11
|
+
* This allows passing custom data to the action without using environment variables.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* new Variation('fast-impl')
|
|
16
|
+
* .withContext('implementation', fastProcessor)
|
|
17
|
+
* .withAction((variation) => {
|
|
18
|
+
* const impl = variation.get<DataProcessor>('implementation');
|
|
19
|
+
* impl.process(data);
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
withContext<T>(key: string, value: T): this;
|
|
24
|
+
/**
|
|
25
|
+
* Gets a value from the variation's context.
|
|
26
|
+
* Returns undefined if the key is not found.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* b.withAction((variation) => {
|
|
31
|
+
* const driver = variation.get<Driver>('driver');
|
|
32
|
+
* driver.connect();
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
get<T>(key: string): T | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Gets a value from the variation's context with a default value.
|
|
39
|
+
* Returns the default if the key is not found.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const iterations = variation.getOrDefault<number>('iterations', 10);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
getOrDefault<T>(key: string, defaultValue: T): T;
|
|
8
47
|
static FromEnvironmentVariables(variables: EnvironmentVariableOptions): Variation[];
|
|
9
48
|
/**
|
|
10
49
|
*
|
|
@@ -23,6 +62,29 @@ export declare class Variation extends BenchmarkBase {
|
|
|
23
62
|
* ]);
|
|
24
63
|
*/
|
|
25
64
|
static FromCliArgs(args: Array<string | string[]>): Variation[];
|
|
65
|
+
/**
|
|
66
|
+
* Creates variations with context values for a single key.
|
|
67
|
+
* Useful for swapping implementations or configurations.
|
|
68
|
+
*
|
|
69
|
+
* @param key The context key to set
|
|
70
|
+
* @param values Array of [name, value] tuples
|
|
71
|
+
* @returns An array of variations with context set
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const variations = Variation.FromContext('processor', [
|
|
76
|
+
* ['loop', loopProcessor],
|
|
77
|
+
* ['reduce', reduceProcessor],
|
|
78
|
+
* ]);
|
|
79
|
+
*
|
|
80
|
+
* benchmark.withVariations(variations);
|
|
81
|
+
* benchmark.withAction((variation) => {
|
|
82
|
+
* const processor = variation.get<DataProcessor>('processor');
|
|
83
|
+
* processor.process(data);
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
static FromContexts<T>(key: string, values: readonly (readonly [name: string, value: T])[]): Variation[];
|
|
26
88
|
withEnvironmentVariables(env: Partial<NodeJS.ProcessEnv>): this;
|
|
27
89
|
withEnvironmentVariable(name: string, value: string): this;
|
|
28
90
|
withCliArgs(...args: string[]): this;
|
package/dist/variation.js
CHANGED
|
@@ -7,10 +7,56 @@ class Variation extends shared_api_1.BenchmarkBase {
|
|
|
7
7
|
name;
|
|
8
8
|
environment = {};
|
|
9
9
|
cliArgs = [];
|
|
10
|
+
context = new Map();
|
|
10
11
|
constructor(name) {
|
|
11
12
|
super();
|
|
12
13
|
this.name = name;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Sets a value in the variation's context.
|
|
17
|
+
* This allows passing custom data to the action without using environment variables.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* new Variation('fast-impl')
|
|
22
|
+
* .withContext('implementation', fastProcessor)
|
|
23
|
+
* .withAction((variation) => {
|
|
24
|
+
* const impl = variation.get<DataProcessor>('implementation');
|
|
25
|
+
* impl.process(data);
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
withContext(key, value) {
|
|
30
|
+
this.context.set(key, value);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets a value from the variation's context.
|
|
35
|
+
* Returns undefined if the key is not found.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* b.withAction((variation) => {
|
|
40
|
+
* const driver = variation.get<Driver>('driver');
|
|
41
|
+
* driver.connect();
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
get(key) {
|
|
46
|
+
return this.context.get(key);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets a value from the variation's context with a default value.
|
|
50
|
+
* Returns the default if the key is not found.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const iterations = variation.getOrDefault<number>('iterations', 10);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
getOrDefault(key, defaultValue) {
|
|
58
|
+
return this.context.get(key) ?? defaultValue;
|
|
59
|
+
}
|
|
14
60
|
static FromEnvironmentVariables(variables) {
|
|
15
61
|
const combinations = (0, utils_1.findCombinations)(variables.map(([name, values]) => values.map((value) => [name, value])));
|
|
16
62
|
variables.reduce((acc, [name, values]) => {
|
|
@@ -50,6 +96,33 @@ class Variation extends shared_api_1.BenchmarkBase {
|
|
|
50
96
|
return new Variation(label).withCliArgs(...cliArgs);
|
|
51
97
|
});
|
|
52
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates variations with context values for a single key.
|
|
101
|
+
* Useful for swapping implementations or configurations.
|
|
102
|
+
*
|
|
103
|
+
* @param key The context key to set
|
|
104
|
+
* @param values Array of [name, value] tuples
|
|
105
|
+
* @returns An array of variations with context set
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const variations = Variation.FromContext('processor', [
|
|
110
|
+
* ['loop', loopProcessor],
|
|
111
|
+
* ['reduce', reduceProcessor],
|
|
112
|
+
* ]);
|
|
113
|
+
*
|
|
114
|
+
* benchmark.withVariations(variations);
|
|
115
|
+
* benchmark.withAction((variation) => {
|
|
116
|
+
* const processor = variation.get<DataProcessor>('processor');
|
|
117
|
+
* processor.process(data);
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
static FromContexts(key, values) {
|
|
122
|
+
return values.map(([name, value]) => {
|
|
123
|
+
return new Variation(name).withContext(key, value);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
53
126
|
withEnvironmentVariables(env) {
|
|
54
127
|
this.environment = {
|
|
55
128
|
...this.environment,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexi-bench",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@swc/core": "~1.5.7",
|
|
24
24
|
"@swc/helpers": "~0.5.11",
|
|
25
25
|
"@types/cli-progress": "^3.11.6",
|
|
26
|
+
"@types/is-ci": "^3.0.4",
|
|
26
27
|
"@types/node": "^20.14.10",
|
|
27
28
|
"nx": "19.4.2",
|
|
28
29
|
"patch-package": "^8.0.0",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"cli-progress": "^3.12.0",
|
|
38
|
+
"is-ci": "^4.1.0",
|
|
37
39
|
"markdown-factory": "^0.0.6"
|
|
38
40
|
},
|
|
39
41
|
"nx": {},
|