modestbench 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cli/commands/init.cjs +12 -12
  3. package/dist/cli/commands/init.cjs.map +1 -1
  4. package/dist/cli/commands/init.js +12 -12
  5. package/dist/cli/commands/init.js.map +1 -1
  6. package/dist/cli/commands/run.cjs +15 -14
  7. package/dist/cli/commands/run.cjs.map +1 -1
  8. package/dist/cli/commands/run.d.cts.map +1 -1
  9. package/dist/cli/commands/run.d.ts.map +1 -1
  10. package/dist/cli/commands/run.js +15 -14
  11. package/dist/cli/commands/run.js.map +1 -1
  12. package/dist/core/engine.cjs +11 -2
  13. package/dist/core/engine.cjs.map +1 -1
  14. package/dist/core/engine.d.cts.map +1 -1
  15. package/dist/core/engine.d.ts.map +1 -1
  16. package/dist/core/engine.js +11 -2
  17. package/dist/core/engine.js.map +1 -1
  18. package/dist/core/engines/accurate-engine.cjs +171 -36
  19. package/dist/core/engines/accurate-engine.cjs.map +1 -1
  20. package/dist/core/engines/accurate-engine.d.cts +5 -0
  21. package/dist/core/engines/accurate-engine.d.cts.map +1 -1
  22. package/dist/core/engines/accurate-engine.d.ts +5 -0
  23. package/dist/core/engines/accurate-engine.d.ts.map +1 -1
  24. package/dist/core/engines/accurate-engine.js +171 -36
  25. package/dist/core/engines/accurate-engine.js.map +1 -1
  26. package/dist/core/engines/tinybench-engine.cjs +3 -2
  27. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  28. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  29. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  30. package/dist/core/engines/tinybench-engine.js +3 -2
  31. package/dist/core/engines/tinybench-engine.js.map +1 -1
  32. package/dist/reporters/human.cjs +226 -29
  33. package/dist/reporters/human.cjs.map +1 -1
  34. package/dist/reporters/human.d.cts +11 -0
  35. package/dist/reporters/human.d.cts.map +1 -1
  36. package/dist/reporters/human.d.ts +11 -0
  37. package/dist/reporters/human.d.ts.map +1 -1
  38. package/dist/reporters/human.js +226 -29
  39. package/dist/reporters/human.js.map +1 -1
  40. package/dist/reporters/profile-human.cjs +6 -1
  41. package/dist/reporters/profile-human.cjs.map +1 -1
  42. package/dist/reporters/profile-human.d.cts.map +1 -1
  43. package/dist/reporters/profile-human.d.ts.map +1 -1
  44. package/dist/reporters/profile-human.js +6 -1
  45. package/dist/reporters/profile-human.js.map +1 -1
  46. package/dist/reporters/simple.cjs +4 -2
  47. package/dist/reporters/simple.cjs.map +1 -1
  48. package/dist/reporters/simple.d.cts.map +1 -1
  49. package/dist/reporters/simple.d.ts.map +1 -1
  50. package/dist/reporters/simple.js +4 -2
  51. package/dist/reporters/simple.js.map +1 -1
  52. package/dist/services/config-manager.cjs +1 -1
  53. package/dist/services/config-manager.cjs.map +1 -1
  54. package/dist/services/config-manager.js +1 -1
  55. package/dist/services/config-manager.js.map +1 -1
  56. package/dist/services/profiler/profile-filter.cjs +4 -1
  57. package/dist/services/profiler/profile-filter.cjs.map +1 -1
  58. package/dist/services/profiler/profile-filter.d.cts.map +1 -1
  59. package/dist/services/profiler/profile-filter.d.ts.map +1 -1
  60. package/dist/services/profiler/profile-filter.js +4 -1
  61. package/dist/services/profiler/profile-filter.js.map +1 -1
  62. package/dist/services/progress-manager.cjs +10 -2
  63. package/dist/services/progress-manager.cjs.map +1 -1
  64. package/dist/services/progress-manager.d.cts +2 -0
  65. package/dist/services/progress-manager.d.cts.map +1 -1
  66. package/dist/services/progress-manager.d.ts +2 -0
  67. package/dist/services/progress-manager.d.ts.map +1 -1
  68. package/dist/services/progress-manager.js +10 -2
  69. package/dist/services/progress-manager.js.map +1 -1
  70. package/dist/types/core.d.cts +2 -0
  71. package/dist/types/core.d.cts.map +1 -1
  72. package/dist/types/core.d.ts +2 -0
  73. package/dist/types/core.d.ts.map +1 -1
  74. package/dist/types/interfaces.d.cts +4 -0
  75. package/dist/types/interfaces.d.cts.map +1 -1
  76. package/dist/types/interfaces.d.ts +4 -0
  77. package/dist/types/interfaces.d.ts.map +1 -1
  78. package/dist/types/profiler.d.cts +2 -0
  79. package/dist/types/profiler.d.cts.map +1 -1
  80. package/dist/types/profiler.d.ts +2 -0
  81. package/dist/types/profiler.d.ts.map +1 -1
  82. package/package.json +9 -10
  83. package/src/cli/commands/init.ts +12 -12
  84. package/src/cli/commands/run.ts +16 -15
  85. package/src/core/engine.ts +18 -2
  86. package/src/core/engines/accurate-engine.ts +195 -44
  87. package/src/core/engines/tinybench-engine.ts +3 -2
  88. package/src/reporters/human.ts +350 -72
  89. package/src/reporters/profile-human.ts +9 -3
  90. package/src/reporters/simple.ts +5 -2
  91. package/src/services/config-manager.ts +1 -1
  92. package/src/services/profiler/profile-filter.ts +5 -1
  93. package/src/services/progress-manager.ts +12 -2
  94. package/src/types/core.ts +2 -0
  95. package/src/types/interfaces.ts +8 -0
  96. package/src/types/profiler.ts +3 -0
@@ -27,6 +27,8 @@ export class HumanReporter extends BaseReporter {
27
27
 
28
28
  private currentSuite = '';
29
29
 
30
+ private currentSuiteMaxNameLen = 0; // Track max name length for current suite alignment
31
+
30
32
  private failures: Array<{
31
33
  error: string;
32
34
  file: string;
@@ -36,6 +38,8 @@ export class HumanReporter extends BaseReporter {
36
38
 
37
39
  private lastProgressLine = '';
38
40
 
41
+ private maxTimePadWidth = 0; // Track maximum time padding width to prevent jitter
42
+
39
43
  private progressWindowActive = false; // Track if progress window is rendered
40
44
 
41
45
  private readonly quiet: boolean;
@@ -164,12 +168,16 @@ export class HumanReporter extends BaseReporter {
164
168
  let totalSuites = 0;
165
169
  let totalPassed = 0;
166
170
  let totalFailed = 0;
171
+ let totalAborted = 0;
167
172
 
168
173
  for (const file of run.files) {
169
174
  totalSuites += file.suites.length;
170
175
  for (const suite of file.suites) {
171
- totalPassed += suite.tasks.filter((t: TaskResult) => !t.error).length;
176
+ totalPassed += suite.tasks.filter(
177
+ (t: TaskResult) => !t.error && !t.aborted,
178
+ ).length;
172
179
  totalFailed += suite.tasks.filter((t: TaskResult) => t.error).length;
180
+ totalAborted += suite.tasks.filter((t: TaskResult) => t.aborted).length;
173
181
  }
174
182
  }
175
183
 
@@ -185,15 +193,24 @@ export class HumanReporter extends BaseReporter {
185
193
  `${this.colorize('brightBlue', ' Suites:')} ${this.colorize('brightWhite', String(totalSuites))}`,
186
194
  );
187
195
  this.printLine(
188
- `${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed))}`,
196
+ `${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed + totalAborted))}`,
189
197
  );
190
- if (totalFailed > 0) {
191
- this.printLine(
192
- `${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`,
193
- );
194
- this.printLine(
195
- `${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
196
- );
198
+ if (totalFailed > 0 || totalAborted > 0) {
199
+ if (totalFailed > 0) {
200
+ this.printLine(
201
+ `${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`,
202
+ );
203
+ }
204
+ if (totalPassed > 0) {
205
+ this.printLine(
206
+ `${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
207
+ );
208
+ }
209
+ if (totalAborted > 0) {
210
+ this.printLine(
211
+ `${this.colorize('brightYellow', ansiChars.approx + ' Aborted:')} ${this.colorize('brightWhite', String(totalAborted))}`,
212
+ );
213
+ }
197
214
  } else {
198
215
  this.printLine(
199
216
  `${this.colorize('brightCyan', ansiChars.checkmark + ' All tasks passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
@@ -222,7 +239,8 @@ export class HumanReporter extends BaseReporter {
222
239
  this.printLine();
223
240
  }
224
241
  }
225
- } else {
242
+ } else if (totalAborted === 0) {
243
+ // Only show "Rad" if no failures AND no aborts
226
244
  const successMessage = `${this.colorize('brightMagenta', 'Rad. ☮')}`;
227
245
  this.printLine(successMessage);
228
246
  }
@@ -300,7 +318,8 @@ export class HumanReporter extends BaseReporter {
300
318
  return;
301
319
  }
302
320
 
303
- const { elapsed, percentage, tasksCompleted, totalTasks } = state;
321
+ const { currentTask, elapsed, percentage, tasksCompleted, totalTasks } =
322
+ state;
304
323
 
305
324
  // Pad task counts for alignment
306
325
  const totalTasksWidth = String(totalTasks).length;
@@ -315,7 +334,7 @@ export class HumanReporter extends BaseReporter {
315
334
 
316
335
  // Calculate ETA if we have completed tasks and determine padding width
317
336
  let etaStr = '';
318
- let padWidth = elapsedStrRaw.length;
337
+ let padWidth = Math.max(this.maxTimePadWidth, elapsedStrRaw.length);
319
338
  if (tasksCompleted > 0) {
320
339
  const avgTimePerTask = elapsed / tasksCompleted;
321
340
  const remainingTasks = totalTasks - tasksCompleted;
@@ -323,14 +342,27 @@ export class HumanReporter extends BaseReporter {
323
342
  const etaSeconds = Math.round(etaMs / 1000);
324
343
  const etaTimeStr = this.formatTimeRemaining(etaSeconds);
325
344
  padWidth = Math.max(padWidth, etaTimeStr.length);
326
- etaStr = ` ${this.colorize('dim', '|')} ${this.colorize('dim', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
345
+ etaStr = ` ${this.colorize('gray', '|')} ${this.colorize('gray', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
327
346
  }
328
347
 
348
+ // Remember the maximum width we've ever used to prevent jitter
349
+ this.maxTimePadWidth = Math.max(this.maxTimePadWidth, padWidth);
350
+
329
351
  // Pad elapsed time to match the longest time string
330
- const elapsedStr = elapsedStrRaw.padStart(padWidth, ' ');
352
+ const elapsedStr = elapsedStrRaw.padStart(this.maxTimePadWidth, ' ');
331
353
 
332
354
  const roundedPercentage = percentage.toFixed(2);
333
- const line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('dim', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('dim', 'tasks')} ${this.colorize('dim', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('dim', ')')} ${this.colorize('dim', '|')} ${this.colorize('dim', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
355
+
356
+ // Build progress line with current task if available
357
+ let line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('gray', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('gray', 'tasks')} ${this.colorize('gray', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('gray', ')')} ${this.colorize('gray', '|')} ${this.colorize('gray', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
358
+
359
+ if (currentTask) {
360
+ const truncatedTask =
361
+ currentTask.length > 60
362
+ ? currentTask.substring(0, 57) + '...'
363
+ : currentTask;
364
+ line += ` ${this.colorize('gray', '|')} ${this.colorize('white', truncatedTask)}`;
365
+ }
334
366
 
335
367
  this.lastProgressLine = line;
336
368
  this.renderProgressWindow();
@@ -340,6 +372,7 @@ export class HumanReporter extends BaseReporter {
340
372
  this.startTime = Date.now();
341
373
  this.failures = []; // Reset failures for new run
342
374
  this.lastProgressLine = ''; // Reset for new run
375
+ this.maxTimePadWidth = 0; // Reset time padding width for new run
343
376
 
344
377
  if (this.quiet) {
345
378
  return;
@@ -395,29 +428,70 @@ export class HumanReporter extends BaseReporter {
395
428
  return;
396
429
  }
397
430
 
398
- // Print all buffered task results with aligned columns
399
- this.printAlignedSuiteResults();
431
+ // Tasks are printed immediately in onTaskResult, so just print suite summary
400
432
 
401
433
  // Skip displaying summary for the implicit "default" suite
402
434
  if (result.name === 'default') {
403
435
  return;
404
436
  }
405
437
 
406
- const passed = result.tasks.filter((t) => !t.error).length;
438
+ const passed = result.tasks.filter((t) => !t.error && !t.aborted).length;
407
439
  const failed = result.tasks.filter((t) => t.error).length;
440
+ const aborted = result.tasks.filter((t) => t.aborted).length;
441
+ const durationStr = BaseReporter.formatDuration(result.duration * 1000000); // ms to ns
442
+
443
+ // Build summary parts
444
+ const parts: string[] = [];
408
445
 
409
446
  if (failed > 0) {
410
- this.printLine(
411
- ` ${this.colorize('red', `${ansiChars.cross} ${failed} failed`)}, ${this.colorize('green', `${passed} passed`)}`,
412
- );
447
+ parts.push(this.colorize('red', `${ansiChars.cross} ${failed} failed`));
448
+ }
449
+ if (passed > 0) {
450
+ parts.push(this.colorize('green', `${passed} passed`));
451
+ }
452
+ if (aborted > 0) {
453
+ parts.push(this.colorize('brightYellow', `${aborted} aborted`));
454
+ }
455
+
456
+ const summary = parts.join(', ');
457
+ const timeInfo = `${this.colorize('gray', 'in')} ${this.colorize('cyan', durationStr)}`;
458
+
459
+ if (failed > 0 || aborted > 0) {
460
+ this.printLine(` ${summary} ${timeInfo}`);
413
461
  } else {
414
462
  this.printLine(
415
- ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${HumanReporter.pluralize('task', passed)} passed`)}`,
463
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${HumanReporter.pluralize('task', passed)} passed`)} ${timeInfo}`,
416
464
  );
417
465
  }
418
466
  this.printLine();
419
467
  }
420
468
 
469
+ onSuiteInit(suite: string, taskNames: readonly string[]): void {
470
+ // Pre-calculate max name length for optimal alignment
471
+ const terminalWidth = process.stdout.columns || 80;
472
+ const STATS_RESERVED_WIDTH = 70;
473
+ const MAX_NAME_WIDTH = Math.max(
474
+ 40,
475
+ Math.min(
476
+ 60,
477
+ terminalWidth - 4 - 2 - 2 - STATS_RESERVED_WIDTH, // BASE_INDENT(4) + status(1) + space(1) + ": "(2)
478
+ ),
479
+ );
480
+
481
+ // Calculate the actual max name length from non-wrapped names
482
+ let maxLen = 0;
483
+ for (const name of taskNames) {
484
+ const nameLen = this.getVisibleLength(name.trim());
485
+ // Only count names that won't wrap
486
+ if (nameLen <= MAX_NAME_WIDTH) {
487
+ maxLen = Math.max(maxLen, nameLen);
488
+ }
489
+ }
490
+
491
+ // Use the max of actual names or MAX_NAME_WIDTH for consistency
492
+ this.currentSuiteMaxNameLen = Math.max(maxLen, MAX_NAME_WIDTH);
493
+ }
494
+
421
495
  onSuiteStart(suite: string): void {
422
496
  this.currentSuite = suite;
423
497
 
@@ -444,8 +518,16 @@ export class HumanReporter extends BaseReporter {
444
518
  return;
445
519
  }
446
520
 
447
- // Buffer the result for later printing with proper alignment
521
+ // Always buffer the result for suite summary (including aborted tasks)
448
522
  this.suiteResults.push(result);
523
+
524
+ // Skip printing aborted tasks (they're counted in summary but not shown individually)
525
+ if (result.aborted) {
526
+ return;
527
+ }
528
+
529
+ // Print immediately with current alignment
530
+ this.printTaskResult(result);
449
531
  }
450
532
 
451
533
  onTaskStart(task: string): void {
@@ -527,13 +609,24 @@ export class HumanReporter extends BaseReporter {
527
609
  return;
528
610
  }
529
611
 
530
- const MAX_NAME_WIDTH = 60;
531
612
  const BASE_INDENT = ' '; // 4 spaces
532
613
  const bullet = this.colorize(
533
614
  'dim',
534
615
  this.colorize('gray', ansiChars.bullet),
535
616
  );
536
617
 
618
+ // Calculate maximum name width based on terminal width
619
+ // Reserve space for: indent (4) + status (1) + space (1) + name + ": " (2) + stats (~60 chars)
620
+ const terminalWidth = process.stdout.columns || 80;
621
+ const STATS_RESERVED_WIDTH = 70; // Approx space for duration + rme + ops/sec with padding
622
+ const MAX_NAME_WIDTH = Math.max(
623
+ 40,
624
+ Math.min(
625
+ 60,
626
+ terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH,
627
+ ),
628
+ );
629
+
537
630
  // Prepare formatted data for each task
538
631
  interface FormattedTask {
539
632
  durationLen: number;
@@ -550,49 +643,52 @@ export class HumanReporter extends BaseReporter {
550
643
  status: string;
551
644
  }
552
645
 
553
- const formatted: FormattedTask[] = this.suiteResults.map((result) => {
554
- const status = result.error
555
- ? this.colorize('red', ansiChars.cross)
556
- : this.colorize('brightCyan', ansiChars.checkmark);
646
+ // Filter out aborted tasks (they're counted in suite summary but not printed)
647
+ const formatted: FormattedTask[] = this.suiteResults
648
+ .filter((result) => !result.aborted)
649
+ .map((result) => {
650
+ const status = result.error
651
+ ? this.colorize('red', ansiChars.cross)
652
+ : this.colorize('brightCyan', ansiChars.checkmark);
653
+
654
+ const name = result.name.trim();
655
+ const nameLength = this.getVisibleLength(name);
656
+
657
+ if (result.error) {
658
+ return {
659
+ durationLen: 0,
660
+ durationStr: '',
661
+ error: true,
662
+ errorMessage: result.error?.message || String(result.error),
663
+ iterations: 0,
664
+ name,
665
+ nameLength,
666
+ opsPerSecLen: 0,
667
+ opsPerSecStr: '',
668
+ rmeLen: 0,
669
+ rmeStr: '',
670
+ status,
671
+ };
672
+ }
557
673
 
558
- const name = result.name.trim();
559
- const nameLength = this.getVisibleLength(name);
674
+ const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
675
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
676
+ const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
560
677
 
561
- if (result.error) {
562
678
  return {
563
- durationLen: 0,
564
- durationStr: '',
565
- error: true,
566
- errorMessage: result.error?.message || String(result.error),
567
- iterations: 0,
679
+ durationLen: this.getVisibleLength(duration),
680
+ durationStr: duration,
681
+ error: false,
682
+ iterations: result.iterations,
568
683
  name,
569
684
  nameLength,
570
- opsPerSecLen: 0,
571
- opsPerSecStr: '',
572
- rmeLen: 0,
573
- rmeStr: '',
685
+ opsPerSecLen: this.getVisibleLength(opsPerSec),
686
+ opsPerSecStr: opsPerSec,
687
+ rmeLen: this.getVisibleLength(rme),
688
+ rmeStr: rme,
574
689
  status,
575
690
  };
576
- }
577
-
578
- const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
579
- const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
580
- const rme = BaseReporter.formatPercentage(result.marginOfError * 100);
581
-
582
- return {
583
- durationLen: this.getVisibleLength(duration),
584
- durationStr: duration,
585
- error: false,
586
- iterations: result.iterations,
587
- name,
588
- nameLength,
589
- opsPerSecLen: this.getVisibleLength(opsPerSec),
590
- opsPerSecStr: opsPerSec,
591
- rmeLen: this.getVisibleLength(rme),
592
- rmeStr: rme,
593
- status,
594
- };
595
- });
691
+ });
596
692
 
597
693
  // Find max widths
598
694
  const nonWrappingTasks = formatted.filter(
@@ -616,10 +712,6 @@ export class HumanReporter extends BaseReporter {
616
712
  0,
617
713
  );
618
714
 
619
- // Calculate the position where numbers start for unwrapped lines
620
- // BASE_INDENT (4) + status (1 char) + space (1) + maxNameLen + ": " (2) = 8 + maxNameLen
621
- const numbersStartPos = BASE_INDENT.length + 2 + maxNameLen + 2;
622
-
623
715
  // Print each task with aligned columns
624
716
  for (const task of formatted) {
625
717
  if (task.error) {
@@ -635,22 +727,45 @@ export class HumanReporter extends BaseReporter {
635
727
  `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)} ${this.colorize('red', 'FAILED')}`,
636
728
  );
637
729
  } else if (task.nameLength > MAX_NAME_WIDTH) {
638
- // Long name - wrap to next line, but align numbers with unwrapped lines
639
- this.printLine(
640
- `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}:`,
641
- );
730
+ // Long name - wrap to multiple lines, align last line with short names
731
+ const wrappedLines = this.wrapText(task.name, MAX_NAME_WIDTH);
732
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
642
733
 
643
- // Calculate padding to align with unwrapped lines
644
- // We need to get to numbersStartPos from the beginning of the line
645
- const leadingPad = ' '.repeat(numbersStartPos);
734
+ // Format stats string
646
735
  const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
647
736
  const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
648
737
  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)}`;
649
739
 
740
+ // Print first line with status
650
741
  this.printLine(
651
- `${leadingPad}${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
742
+ `${BASE_INDENT}${task.status} ${this.colorize('white', wrappedLines[0]!)}`,
652
743
  );
653
744
 
745
+ // Print middle continuation lines (all but first and last)
746
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
747
+ this.printLine(
748
+ `${continueIndent}${this.colorize('white', wrappedLines[i]!)}`,
749
+ );
750
+ }
751
+
752
+ // Print last line with colon and stats aligned with short names
753
+ if (wrappedLines.length > 1) {
754
+ const lastLine = wrappedLines[wrappedLines.length - 1]!;
755
+ const lastLineLen = this.getVisibleLength(lastLine);
756
+ // Pad the last line to align the ':' with short names
757
+ const lastLinePad = ' '.repeat(Math.max(0, maxNameLen - lastLineLen));
758
+ this.printLine(
759
+ `${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`,
760
+ );
761
+ } else {
762
+ // Single wrapped line
763
+ const lastLinePad = ' '.repeat(maxNameLen - task.nameLength);
764
+ this.printLine(
765
+ `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${lastLinePad}: ${statsStr}`,
766
+ );
767
+ }
768
+
654
769
  if (this.verbose && task.iterations > 0) {
655
770
  this.printLine(
656
771
  ` ${this.colorize('dim', `${task.iterations} iterations`)}`,
@@ -687,6 +802,128 @@ export class HumanReporter extends BaseReporter {
687
802
  this.renderProgressWindow();
688
803
  }
689
804
 
805
+ /**
806
+ * Print a single task result immediately with current alignment
807
+ */
808
+ private printTaskResult(result: TaskResult): void {
809
+ // Clear progress bar temporarily
810
+ this.clearProgress();
811
+
812
+ const BASE_INDENT = ' '; // 4 spaces
813
+ const bullet = this.colorize(
814
+ 'dim',
815
+ this.colorize('gray', ansiChars.bullet),
816
+ );
817
+
818
+ // Calculate terminal width constraints
819
+ const terminalWidth = process.stdout.columns || 80;
820
+ const STATS_RESERVED_WIDTH = 70;
821
+ const MAX_NAME_WIDTH = Math.max(
822
+ 40,
823
+ Math.min(
824
+ 60,
825
+ terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH,
826
+ ),
827
+ );
828
+
829
+ // Status marker
830
+ const status = result.error
831
+ ? this.colorize('red', ansiChars.cross)
832
+ : this.colorize('brightCyan', ansiChars.checkmark);
833
+
834
+ const name = result.name.trim();
835
+ const nameLength = this.getVisibleLength(name);
836
+
837
+ // Handle errors
838
+ if (result.error) {
839
+ this.failures.push({
840
+ error: result.error?.message || String(result.error),
841
+ file: this.currentFile,
842
+ suite: this.currentSuite,
843
+ task: name,
844
+ });
845
+
846
+ this.printLine(
847
+ `${BASE_INDENT}${status} ${this.colorize('white', name)} ${this.colorize('red', 'FAILED')}`,
848
+ );
849
+ return;
850
+ }
851
+
852
+ // Format stats
853
+ const duration = BaseReporter.formatDuration(result.mean);
854
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
855
+ const rme = BaseReporter.formatPercentage(result.marginOfError);
856
+
857
+ // Use fixed widths for stats columns (reasonable maximums)
858
+ const DURATION_WIDTH = 10; // "999.99ms" max
859
+ const RME_WIDTH = 8; // "±999.99%" max
860
+ const OPS_WIDTH = 15; // "999.99K ops/sec" max
861
+
862
+ const durationLen = this.getVisibleLength(duration);
863
+ const rmeLen = this.getVisibleLength(rme);
864
+ const opsLen = this.getVisibleLength(opsPerSec);
865
+
866
+ // 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)}`;
871
+
872
+ // Handle long names (wrap)
873
+ if (nameLength > MAX_NAME_WIDTH) {
874
+ const wrappedLines = this.wrapText(name, MAX_NAME_WIDTH);
875
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
876
+
877
+ // Print first line with status
878
+ this.printLine(
879
+ `${BASE_INDENT}${status} ${this.colorize('white', wrappedLines[0]!)}`,
880
+ );
881
+
882
+ // Print middle lines (all but first and last)
883
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
884
+ this.printLine(
885
+ `${continueIndent}${this.colorize('white', wrappedLines[i]!)}`,
886
+ );
887
+ }
888
+
889
+ // Print last line with colon and stats aligned
890
+ // Use pre-calculated currentSuiteMaxNameLen for perfect alignment
891
+ if (wrappedLines.length > 1) {
892
+ const lastLine = wrappedLines[wrappedLines.length - 1]!;
893
+ const lastLineLen = this.getVisibleLength(lastLine);
894
+ const lastLinePad = ' '.repeat(
895
+ Math.max(0, this.currentSuiteMaxNameLen - lastLineLen),
896
+ );
897
+ this.printLine(
898
+ `${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`,
899
+ );
900
+ } else {
901
+ // Single wrapped line (shouldn't happen if nameLength > MAX but handle it)
902
+ const lastLinePad = ' '.repeat(
903
+ Math.max(0, this.currentSuiteMaxNameLen - nameLength),
904
+ );
905
+ this.printLine(
906
+ `${BASE_INDENT}${status} ${this.colorize('white', name)}${lastLinePad}: ${statsStr}`,
907
+ );
908
+ }
909
+ } else {
910
+ // Normal length - print on same line with pre-calculated alignment
911
+ const namePad = ' '.repeat(
912
+ Math.max(0, this.currentSuiteMaxNameLen - nameLength),
913
+ );
914
+
915
+ this.printLine(
916
+ `${BASE_INDENT}${status} ${this.colorize('white', name)}${namePad}: ${statsStr}`,
917
+ );
918
+ }
919
+
920
+ if (this.verbose && result.iterations > 0) {
921
+ this.printLine(
922
+ ` ${this.colorize('dim', `${result.iterations} iterations`)}`,
923
+ );
924
+ }
925
+ }
926
+
690
927
  /**
691
928
  * Render the progress window at the bottom
692
929
  */
@@ -705,4 +942,45 @@ export class HumanReporter extends BaseReporter {
705
942
  console.log(this.lastProgressLine);
706
943
  this.progressWindowActive = true;
707
944
  }
945
+
946
+ /**
947
+ * Wrap text to a maximum width, breaking at word boundaries when possible
948
+ */
949
+ private wrapText(text: string, maxWidth: number): string[] {
950
+ if (this.getVisibleLength(text) <= maxWidth) {
951
+ return [text];
952
+ }
953
+
954
+ const lines: string[] = [];
955
+ let currentLine = '';
956
+
957
+ const words = text.split(/(\s+)/); // Keep whitespace in split
958
+
959
+ for (const word of words) {
960
+ const testLine = currentLine + word;
961
+ if (this.getVisibleLength(testLine) <= maxWidth) {
962
+ currentLine = testLine;
963
+ } else {
964
+ // If current line has content, save it
965
+ if (currentLine.trim()) {
966
+ lines.push(currentLine.trimEnd());
967
+ currentLine = word.trim() + ' ';
968
+ } else {
969
+ // Single word is too long, force break it
970
+ if (this.getVisibleLength(word) > maxWidth) {
971
+ lines.push(word.substring(0, maxWidth));
972
+ currentLine = word.substring(maxWidth);
973
+ } else {
974
+ currentLine = word;
975
+ }
976
+ }
977
+ }
978
+ }
979
+
980
+ if (currentLine.trim()) {
981
+ lines.push(currentLine.trimEnd());
982
+ }
983
+
984
+ return lines;
985
+ }
708
986
  }
@@ -197,8 +197,14 @@ export class ProfileHumanReporter {
197
197
  }
198
198
 
199
199
  private printSummary(data: FilteredProfileData): void {
200
- this.printLine(
201
- `${this.colorize('dim', `... (showing top ${data.totalShown} of ${data.totalFiltered} user functions)`)}`,
202
- );
200
+ if (data.totalShown === 0) {
201
+ this.printLine(
202
+ `${this.colorize('dim', `No functions used at least ${data.minExecutionPercent}% of the ticks`)}`,
203
+ );
204
+ } else {
205
+ this.printLine(
206
+ `${this.colorize('dim', `... (showing top ${data.totalShown} of ${data.totalFiltered} user functions)`)}`,
207
+ );
208
+ }
203
209
  }
204
210
  }
@@ -329,8 +329,11 @@ export class SimpleReporter extends BaseReporter {
329
329
  return;
330
330
  }
331
331
 
332
- // Buffer the result for later printing with proper alignment
332
+ // Always buffer the result for suite summary (including aborted tasks)
333
333
  this.suiteResults.push(result);
334
+
335
+ // Note: Aborted tasks are still printed in simple reporter for completeness
336
+ // but they'll have zero stats
334
337
  }
335
338
 
336
339
  onTaskStart(task: string): void {
@@ -397,7 +400,7 @@ export class SimpleReporter extends BaseReporter {
397
400
 
398
401
  const duration = BaseReporter.formatDuration(result.mean * 1e9);
399
402
  const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
400
- const rme = BaseReporter.formatPercentage(result.marginOfError * 100);
403
+ const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
401
404
 
402
405
  return {
403
406
  durationLen: duration.length,
@@ -55,7 +55,7 @@ const DEFAULT_CONFIG: ModestBenchConfig = {
55
55
  iterations: 100, // Sufficient iterations for reliable statistics
56
56
  limitBy: 'iterations', // Default to limiting by iteration count
57
57
  metadata: {},
58
- outputDir: './benchmark-results',
58
+ outputDir: '.modestbench',
59
59
  pattern: 'bench/**/*.bench.{js,ts,mjs,cjs,mts,cts}', // Search bench/ directory recursively
60
60
  quiet: false,
61
61
  reporterConfig: {},
@@ -60,6 +60,9 @@ export const filterProfile = (
60
60
  return true;
61
61
  });
62
62
 
63
+ // Save count of user functions before percentage filtering
64
+ const totalUserFunctions = filtered.length;
65
+
63
66
  // Apply percentage threshold
64
67
  const minPercent = config.minExecutionPercent ?? 0.5;
65
68
  filtered = filtered.filter((fn) => fn.percentage >= minPercent);
@@ -80,8 +83,9 @@ export const filterProfile = (
80
83
  return {
81
84
  functions: filtered,
82
85
  groupedByFile,
86
+ minExecutionPercent: minPercent,
83
87
  summary: data.summary,
84
- totalFiltered: data.functions.length,
88
+ totalFiltered: totalUserFunctions,
85
89
  totalShown: filtered.length,
86
90
  totalTicks: data.totalTicks,
87
91
  };