Skip to content

Commit e1bf06a

Browse files
boneskullclaude
andcommitted
feat(reporters): display iteration counts inline with low-count warnings
- Add iteration count inline with timing stats in format: duration ±rme (N iter) | ops/sec - Highlight low iteration counts (<30) in red for human reporter - Add summary warning when tasks have insufficient iterations for reliable CV - Remove verbose-only iteration display (now always shown inline) The 30-iteration threshold is based on statistical best practices for reliable coefficient of variation calculations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d9228c6 commit e1bf06a

File tree

4 files changed

+107
-32
lines changed

4 files changed

+107
-32
lines changed

src/reporters/human.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import type {
1919
import { BaseReporter } from '../services/reporter-registry.js';
2020
import { ansiChars, colors } from '../utils/ansi.js';
2121

22+
/**
23+
* Minimum iterations required for reliable CV calculation
24+
*/
25+
const MIN_RELIABLE_ITERATIONS = 30;
26+
2227
/**
2328
* Human-readable console reporter with colorized output
2429
*/
@@ -38,6 +43,8 @@ export class HumanReporter extends BaseReporter {
3843

3944
private lastProgressLine = '';
4045

46+
private lowIterationCount = 0;
47+
4148
private maxTimePadWidth = 0; // Track maximum time padding width to prevent jitter
4249

4350
private progressWindowActive = false; // Track if progress window is rendered
@@ -244,6 +251,14 @@ export class HumanReporter extends BaseReporter {
244251
const successMessage = `${this.colorize('brightMagenta', 'Rad. ☮')}`;
245252
this.printLine(successMessage);
246253
}
254+
255+
// Show warning for low iteration counts
256+
if (this.lowIterationCount > 0) {
257+
this.printLine();
258+
this.printLine(
259+
`${this.colorize('brightYellow', ansiChars.approx)} ${this.colorize('brightYellow', 'Warning:')} ${this.lowIterationCount} ${HumanReporter.pluralize('task', this.lowIterationCount)} had low iteration counts (<${MIN_RELIABLE_ITERATIONS}) which may affect statistical reliability`,
260+
);
261+
}
247262
}
248263

249264
onError(error: Error): void {
@@ -373,6 +388,7 @@ export class HumanReporter extends BaseReporter {
373388
this.failures = []; // Reset failures for new run
374389
this.lastProgressLine = ''; // Reset for new run
375390
this.maxTimePadWidth = 0; // Reset time padding width for new run
391+
this.lowIterationCount = 0; // Reset low iteration count for new run
376392

377393
if (this.quiet) {
378394
return;
@@ -634,6 +650,9 @@ export class HumanReporter extends BaseReporter {
634650
error: boolean;
635651
errorMessage?: string;
636652
iterations: number;
653+
iterationsLen: number;
654+
iterationsStr: string;
655+
lowIterations: boolean;
637656
name: string;
638657
nameLength: number;
639658
opsPerSecLen: number;
@@ -661,6 +680,9 @@ export class HumanReporter extends BaseReporter {
661680
error: true,
662681
errorMessage: result.error?.message || String(result.error),
663682
iterations: 0,
683+
iterationsLen: 0,
684+
iterationsStr: '',
685+
lowIterations: false,
664686
name,
665687
nameLength,
666688
opsPerSecLen: 0,
@@ -674,12 +696,17 @@ export class HumanReporter extends BaseReporter {
674696
const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
675697
const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
676698
const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
699+
const iterationsStr = `(${result.iterations} iter)`;
700+
const lowIterations = result.iterations < MIN_RELIABLE_ITERATIONS;
677701

678702
return {
679703
durationLen: this.getVisibleLength(duration),
680704
durationStr: duration,
681705
error: false,
682706
iterations: result.iterations,
707+
iterationsLen: iterationsStr.length,
708+
iterationsStr,
709+
lowIterations,
683710
name,
684711
nameLength,
685712
opsPerSecLen: this.getVisibleLength(opsPerSec),
@@ -707,6 +734,10 @@ export class HumanReporter extends BaseReporter {
707734
...formatted.filter((t) => !t.error).map((t) => t.rmeLen),
708735
0,
709736
);
737+
const maxIterLen = Math.max(
738+
...formatted.filter((t) => !t.error).map((t) => t.iterationsLen),
739+
0,
740+
);
710741
const maxOpsLen = Math.max(
711742
...formatted.filter((t) => !t.error).map((t) => t.opsPerSecLen),
712743
0,
@@ -731,11 +762,13 @@ export class HumanReporter extends BaseReporter {
731762
const wrappedLines = this.wrapText(task.name, MAX_NAME_WIDTH);
732763
const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
733764

734-
// Format stats string
765+
// Format stats string with iterations
735766
const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
736767
const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
768+
const iterPad = ' '.repeat(maxIterLen - task.iterationsLen);
737769
const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
738-
const statsStr = `${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`;
770+
const iterColor = task.lowIterations ? 'brightRed' : 'cyan';
771+
const statsStr = `${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${iterPad}${this.colorize(iterColor, task.iterationsStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`;
739772

740773
// Print first line with status
741774
this.printLine(
@@ -766,26 +799,26 @@ export class HumanReporter extends BaseReporter {
766799
);
767800
}
768801

769-
if (this.verbose && task.iterations > 0) {
770-
this.printLine(
771-
` ${this.colorize('dim', `${task.iterations} iterations`)}`,
772-
);
802+
// Track low iteration count
803+
if (task.lowIterations) {
804+
this.lowIterationCount++;
773805
}
774806
} else {
775807
// Normal length - align on same line
776808
const namePad = ' '.repeat(maxNameLen - task.nameLength);
777809
const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
778810
const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
811+
const iterPad = ' '.repeat(maxIterLen - task.iterationsLen);
779812
const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
813+
const iterColor = task.lowIterations ? 'brightRed' : 'cyan';
780814

781815
this.printLine(
782-
`${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${namePad}: ${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
816+
`${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${namePad}: ${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${iterPad}${this.colorize(iterColor, task.iterationsStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
783817
);
784818

785-
if (this.verbose && task.iterations > 0) {
786-
this.printLine(
787-
` ${this.colorize('dim', `${task.iterations} iterations`)}`,
788-
);
819+
// Track low iteration count
820+
if (task.lowIterations) {
821+
this.lowIterationCount++;
789822
}
790823
}
791824
}
@@ -853,21 +886,27 @@ export class HumanReporter extends BaseReporter {
853886
const duration = BaseReporter.formatDuration(result.mean);
854887
const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
855888
const rme = BaseReporter.formatPercentage(result.marginOfError);
889+
const iterationsStr = `(${result.iterations} iter)`;
890+
const lowIterations = result.iterations < MIN_RELIABLE_ITERATIONS;
856891

857892
// Use fixed widths for stats columns (reasonable maximums)
858893
const DURATION_WIDTH = 10; // "999.99ms" max
859894
const RME_WIDTH = 8; // "±999.99%" max
895+
const ITER_WIDTH = 12; // "(99999 iter)" max
860896
const OPS_WIDTH = 15; // "999.99K ops/sec" max
861897

862898
const durationLen = this.getVisibleLength(duration);
863899
const rmeLen = this.getVisibleLength(rme);
900+
const iterLen = iterationsStr.length;
864901
const opsLen = this.getVisibleLength(opsPerSec);
865902

866903
// Stats formatting with fixed widths
867-
const durationPad = ' '.repeat(DURATION_WIDTH - durationLen);
868-
const rmePad = ' '.repeat(RME_WIDTH - rmeLen);
869-
const opsPad = ' '.repeat(OPS_WIDTH - opsLen);
870-
const statsStr = `${durationPad}${this.colorize('cyan', duration)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', rme)} ${bullet} ${opsPad}${this.colorize('magenta', opsPerSec)}`;
904+
const durationPad = ' '.repeat(Math.max(0, DURATION_WIDTH - durationLen));
905+
const rmePad = ' '.repeat(Math.max(0, RME_WIDTH - rmeLen));
906+
const iterPad = ' '.repeat(Math.max(0, ITER_WIDTH - iterLen));
907+
const opsPad = ' '.repeat(Math.max(0, OPS_WIDTH - opsLen));
908+
const iterColor = lowIterations ? 'brightRed' : 'cyan';
909+
const statsStr = `${durationPad}${this.colorize('cyan', duration)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', rme)} ${iterPad}${this.colorize(iterColor, iterationsStr)} ${bullet} ${opsPad}${this.colorize('magenta', opsPerSec)}`;
871910

872911
// Handle long names (wrap)
873912
if (nameLength > MAX_NAME_WIDTH) {
@@ -917,10 +956,9 @@ export class HumanReporter extends BaseReporter {
917956
);
918957
}
919958

920-
if (this.verbose && result.iterations > 0) {
921-
this.printLine(
922-
` ${this.colorize('dim', `${result.iterations} iterations`)}`,
923-
);
959+
// Track low iteration count
960+
if (lowIterations) {
961+
this.lowIterationCount++;
924962
}
925963
}
926964

src/reporters/simple.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ const symbols = {
2525
checkmark: '√',
2626
cross: '×',
2727
plusMinus: '±',
28+
warning: '⚠',
2829
} as const;
2930

31+
/**
32+
* Minimum iterations required for reliable CV calculation
33+
*/
34+
const MIN_RELIABLE_ITERATIONS = 30;
35+
3036
/**
3137
* Simple console reporter with plain text output (no colors or progress bars)
3238
*/
@@ -42,6 +48,8 @@ export class SimpleReporter extends BaseReporter {
4248
task: string;
4349
}> = [];
4450

51+
private lowIterationCount = 0;
52+
4553
private readonly quiet: boolean;
4654

4755
private startTime = 0;
@@ -192,6 +200,14 @@ export class SimpleReporter extends BaseReporter {
192200
} else {
193201
console.log('All benchmarks completed successfully!');
194202
}
203+
204+
// Show warning for low iteration counts
205+
if (this.lowIterationCount > 0) {
206+
console.log();
207+
console.log(
208+
`${symbols.warning} Warning: ${this.lowIterationCount} ${SimpleReporter.pluralize('task', this.lowIterationCount)} had low iteration counts (<${MIN_RELIABLE_ITERATIONS}) which may affect statistical reliability`,
209+
);
210+
}
195211
}
196212

197213
onError(error: Error): void {
@@ -248,6 +264,7 @@ export class SimpleReporter extends BaseReporter {
248264
onStart(run: BenchmarkRun): void {
249265
this.startTime = Date.now();
250266
this.failures = []; // Reset failures for new run
267+
this.lowIterationCount = 0; // Reset low iteration count for new run
251268

252269
if (this.quiet) {
253270
return;
@@ -366,6 +383,9 @@ export class SimpleReporter extends BaseReporter {
366383
error: boolean;
367384
errorMessage?: string;
368385
iterations: number;
386+
iterationsLen: number;
387+
iterationsStr: string;
388+
lowIterations: boolean;
369389
name: string;
370390
nameLength: number;
371391
opsPerSecLen: number;
@@ -388,6 +408,9 @@ export class SimpleReporter extends BaseReporter {
388408
error: true,
389409
errorMessage: result.error?.message || String(result.error),
390410
iterations: 0,
411+
iterationsLen: 0,
412+
iterationsStr: '',
413+
lowIterations: false,
391414
name,
392415
nameLength,
393416
opsPerSecLen: 0,
@@ -398,15 +421,20 @@ export class SimpleReporter extends BaseReporter {
398421
};
399422
}
400423

401-
const duration = BaseReporter.formatDuration(result.mean * 1e9);
424+
const duration = BaseReporter.formatDuration(result.mean);
402425
const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
403426
const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
427+
const iterationsStr = `(${result.iterations} iter)`;
428+
const lowIterations = result.iterations < MIN_RELIABLE_ITERATIONS;
404429

405430
return {
406431
durationLen: duration.length,
407432
durationStr: duration,
408433
error: false,
409434
iterations: result.iterations,
435+
iterationsLen: iterationsStr.length,
436+
iterationsStr,
437+
lowIterations,
410438
name,
411439
nameLength,
412440
opsPerSecLen: opsPerSec.length,
@@ -434,6 +462,10 @@ export class SimpleReporter extends BaseReporter {
434462
...formatted.filter((t) => !t.error).map((t) => t.rmeLen),
435463
0,
436464
);
465+
const maxIterLen = Math.max(
466+
...formatted.filter((t) => !t.error).map((t) => t.iterationsLen),
467+
0,
468+
);
437469
const maxOpsLen = Math.max(
438470
...formatted.filter((t) => !t.error).map((t) => t.opsPerSecLen),
439471
0,
@@ -464,28 +496,32 @@ export class SimpleReporter extends BaseReporter {
464496
const leadingPad = ' '.repeat(numbersStartPos);
465497
const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
466498
const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
499+
const iterPad = ' '.repeat(maxIterLen - task.iterationsLen);
467500
const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
468501

469502
console.log(
470-
`${leadingPad}${durationPad}${task.durationStr} ${separator} ${symbols.plusMinus}${rmePad}${task.rmeStr} ${separator} ${opsPad}${task.opsPerSecStr}`,
503+
`${leadingPad}${durationPad}${task.durationStr} ${separator} ${symbols.plusMinus}${rmePad}${task.rmeStr} ${iterPad}${task.iterationsStr} ${separator} ${opsPad}${task.opsPerSecStr}`,
471504
);
472505

473-
if (this.verbose && task.iterations > 0) {
474-
console.log(` ${task.iterations} iterations`);
506+
// Track low iteration count
507+
if (task.lowIterations) {
508+
this.lowIterationCount++;
475509
}
476510
} else {
477511
// Normal length - align on same line
478512
const namePad = ' '.repeat(maxNameLen - task.nameLength);
479513
const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
480514
const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
515+
const iterPad = ' '.repeat(maxIterLen - task.iterationsLen);
481516
const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
482517

483518
console.log(
484-
`${BASE_INDENT}${task.status} ${task.name}${namePad}: ${durationPad}${task.durationStr} ${separator} ${symbols.plusMinus}${rmePad}${task.rmeStr} ${separator} ${opsPad}${task.opsPerSecStr}`,
519+
`${BASE_INDENT}${task.status} ${task.name}${namePad}: ${durationPad}${task.durationStr} ${separator} ${symbols.plusMinus}${rmePad}${task.rmeStr} ${iterPad}${task.iterationsStr} ${separator} ${opsPad}${task.opsPerSecStr}`,
485520
);
486521

487-
if (this.verbose && task.iterations > 0) {
488-
console.log(` ${task.iterations} iterations`);
522+
// Track low iteration count
523+
if (task.lowIterations) {
524+
this.lowIterationCount++;
489525
}
490526
}
491527
}

test/integration/reporters.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,8 +806,8 @@ describe('Multiple reporter output formats', () => {
806806
]);
807807

808808
expect(result.exitCode, 'to equal', 0);
809-
// Should show iteration counts in verbose mode
810-
expect(result.stdout, 'to contain', 'iterations');
809+
// Should show iteration counts inline (now shown for all reporters)
810+
expect(result.stdout, 'to contain', 'iter)');
811811
});
812812
});
813813
});

test/integration/verbose-mode.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ export default {
178178
testDir,
179179
);
180180

181-
// The human reporter should show verbose output (e.g., iteration counts)
182-
expect(result.stdout, 'to contain', 'iterations');
181+
// The human reporter should show iteration counts inline
182+
expect(result.stdout, 'to contain', 'iter)');
183183
expect(result.exitCode, 'to equal', 0);
184184
});
185185

@@ -373,8 +373,9 @@ export default {
373373

374374
// CLI setup messages should appear
375375
expect(result.stderr, 'to contain', 'Loading configuration...');
376-
// Human reporter verbose features should be active (e.g., iteration counts)
377-
expect(result.stdout, 'to contain', 'iterations');
376+
// Human reporter should show iteration counts inline (or JSON has iterations field)
377+
// The combined output should contain iteration data in some form
378+
expect(result.stdout, 'to match', /iter\)|"iterations":/);
378379
// JSON and CSV data should be in stdout
379380
expect(result.stdout, 'to contain', '"meta":');
380381
expect(result.exitCode, 'to equal', 0);

0 commit comments

Comments
 (0)