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 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
- - ./examples/benchmark.ts is the motivation for this project. It benchmarks the performance of Nx commands with and without a daemon.
372
- - ./examples/performance-observer.ts is a simple example of how to use the PerformanceObserver API to measure the performance of a function.
373
- - ./examples/simple-command.ts demonstrates how to benchmark a simple command.
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
@@ -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 result = (0, results_1.calculateResultsFromDurations)(variation.name, iterationResults);
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
- bar = new cli_progress_1.SingleBar({
7
- format: 'Running variation {label}: {bar} {percentage}% | {value}/{total} - ETA: {eta}s',
8
- barCompleteChar: '\u2588',
9
- barIncompleteChar: '\u2591',
10
- hideCursor: true,
11
- stopOnComplete: true,
12
- clearOnComplete: true,
13
- });
14
- constructor() { }
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
- (0, fs_1.writeFileSync)(this.outputFile, (0, markdown_factory_1.h1)(benchmark.name, results.some((r) => !!r.subresults)
15
- ? (0, markdown_factory_1.lines)(results.map((r) => {
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
- { field: 'label', label: '' },
23
- ...this.fields,
24
- ]));
25
- }))
26
- : (0, markdown_factory_1.table)(results, [{ field: 'label', label: '' }, ...this.fields])));
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
- console.log('Suite Results:');
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 calculateResultsFromDurations(label: string, durations: (number | Error)[]): Result;
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
- function calculateResultsFromDurations(label, durations) {
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
- results[benchmark.name] = await benchmark.run();
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) {
@@ -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.1.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": {},