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.
- package/CHANGELOG.md +7 -0
- package/dist/cli/commands/init.cjs +12 -12
- package/dist/cli/commands/init.cjs.map +1 -1
- package/dist/cli/commands/init.js +12 -12
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.cjs +15 -14
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +15 -14
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/core/engine.cjs +11 -2
- package/dist/core/engine.cjs.map +1 -1
- package/dist/core/engine.d.cts.map +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +11 -2
- package/dist/core/engine.js.map +1 -1
- package/dist/core/engines/accurate-engine.cjs +171 -36
- package/dist/core/engines/accurate-engine.cjs.map +1 -1
- package/dist/core/engines/accurate-engine.d.cts +5 -0
- package/dist/core/engines/accurate-engine.d.cts.map +1 -1
- package/dist/core/engines/accurate-engine.d.ts +5 -0
- package/dist/core/engines/accurate-engine.d.ts.map +1 -1
- package/dist/core/engines/accurate-engine.js +171 -36
- package/dist/core/engines/accurate-engine.js.map +1 -1
- package/dist/core/engines/tinybench-engine.cjs +3 -2
- package/dist/core/engines/tinybench-engine.cjs.map +1 -1
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
- package/dist/core/engines/tinybench-engine.js +3 -2
- package/dist/core/engines/tinybench-engine.js.map +1 -1
- package/dist/reporters/human.cjs +226 -29
- package/dist/reporters/human.cjs.map +1 -1
- package/dist/reporters/human.d.cts +11 -0
- package/dist/reporters/human.d.cts.map +1 -1
- package/dist/reporters/human.d.ts +11 -0
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +226 -29
- package/dist/reporters/human.js.map +1 -1
- package/dist/reporters/profile-human.cjs +6 -1
- package/dist/reporters/profile-human.cjs.map +1 -1
- package/dist/reporters/profile-human.d.cts.map +1 -1
- package/dist/reporters/profile-human.d.ts.map +1 -1
- package/dist/reporters/profile-human.js +6 -1
- package/dist/reporters/profile-human.js.map +1 -1
- package/dist/reporters/simple.cjs +4 -2
- package/dist/reporters/simple.cjs.map +1 -1
- package/dist/reporters/simple.d.cts.map +1 -1
- package/dist/reporters/simple.d.ts.map +1 -1
- package/dist/reporters/simple.js +4 -2
- package/dist/reporters/simple.js.map +1 -1
- package/dist/services/config-manager.cjs +1 -1
- package/dist/services/config-manager.cjs.map +1 -1
- package/dist/services/config-manager.js +1 -1
- package/dist/services/config-manager.js.map +1 -1
- package/dist/services/profiler/profile-filter.cjs +4 -1
- package/dist/services/profiler/profile-filter.cjs.map +1 -1
- package/dist/services/profiler/profile-filter.d.cts.map +1 -1
- package/dist/services/profiler/profile-filter.d.ts.map +1 -1
- package/dist/services/profiler/profile-filter.js +4 -1
- package/dist/services/profiler/profile-filter.js.map +1 -1
- package/dist/services/progress-manager.cjs +10 -2
- package/dist/services/progress-manager.cjs.map +1 -1
- package/dist/services/progress-manager.d.cts +2 -0
- package/dist/services/progress-manager.d.cts.map +1 -1
- package/dist/services/progress-manager.d.ts +2 -0
- package/dist/services/progress-manager.d.ts.map +1 -1
- package/dist/services/progress-manager.js +10 -2
- package/dist/services/progress-manager.js.map +1 -1
- package/dist/types/core.d.cts +2 -0
- package/dist/types/core.d.cts.map +1 -1
- package/dist/types/core.d.ts +2 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/interfaces.d.cts +4 -0
- package/dist/types/interfaces.d.cts.map +1 -1
- package/dist/types/interfaces.d.ts +4 -0
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/profiler.d.cts +2 -0
- package/dist/types/profiler.d.cts.map +1 -1
- package/dist/types/profiler.d.ts +2 -0
- package/dist/types/profiler.d.ts.map +1 -1
- package/package.json +9 -10
- package/src/cli/commands/init.ts +12 -12
- package/src/cli/commands/run.ts +16 -15
- package/src/core/engine.ts +18 -2
- package/src/core/engines/accurate-engine.ts +195 -44
- package/src/core/engines/tinybench-engine.ts +3 -2
- package/src/reporters/human.ts +350 -72
- package/src/reporters/profile-human.ts +9 -3
- package/src/reporters/simple.ts +5 -2
- package/src/services/config-manager.ts +1 -1
- package/src/services/profiler/profile-filter.ts +5 -1
- package/src/services/progress-manager.ts +12 -2
- package/src/types/core.ts +2 -0
- package/src/types/interfaces.ts +8 -0
- package/src/types/profiler.ts +3 -0
package/src/reporters/human.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 } =
|
|
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('
|
|
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(
|
|
352
|
+
const elapsedStr = elapsedStrRaw.padStart(this.maxTimePadWidth, ' ');
|
|
331
353
|
|
|
332
354
|
const roundedPercentage = percentage.toFixed(2);
|
|
333
|
-
|
|
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
|
-
//
|
|
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.
|
|
411
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
|
|
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:
|
|
564
|
-
durationStr:
|
|
565
|
-
error:
|
|
566
|
-
|
|
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:
|
|
571
|
-
opsPerSecStr:
|
|
572
|
-
rmeLen:
|
|
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
|
|
639
|
-
this.
|
|
640
|
-
|
|
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
|
-
//
|
|
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
|
-
`${
|
|
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
|
-
|
|
201
|
-
|
|
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
|
}
|
package/src/reporters/simple.ts
CHANGED
|
@@ -329,8 +329,11 @@ export class SimpleReporter extends BaseReporter {
|
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
//
|
|
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
|
|
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: '
|
|
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:
|
|
88
|
+
totalFiltered: totalUserFunctions,
|
|
85
89
|
totalShown: filtered.length,
|
|
86
90
|
totalTicks: data.totalTicks,
|
|
87
91
|
};
|