fm-bench 0.4.3 → 0.4.4
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/README.md +1 -1
- package/package.json +1 -1
- package/src/table.js +84 -11
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Token counts come from `fm token-count --quiet`. If `fm` cannot count a response
|
|
|
149
149
|
|
|
150
150
|
Measured runs stream by default so `fm-bench` can capture TTFT and streaming smoothness. Use `--no-stream` if you need buffered `fm respond` behavior; TTFT, TPOT, second-chunk, and chunk-gap fields that depend on streaming will be blank.
|
|
151
151
|
|
|
152
|
-
Terminal output is responsive. Wide terminals show full scoreboard and detail tables, medium terminals show a tighter operating-point table, and narrow terminals switch to compact model cards. Use `--width` to preview a layout and `--ascii` for log systems that do not render Unicode borders well.
|
|
152
|
+
Terminal output is responsive. Wide terminals show full scoreboard and detail tables, medium terminals show a tighter operating-point table, and narrow terminals switch to compact model cards. Long `NOTE`, `DESCRIPTION`, and model quota cells wrap so error context stays visible instead of being hidden behind ellipses. Use `--width` to preview a layout and `--ascii` for log systems that do not render Unicode borders well.
|
|
153
153
|
|
|
154
154
|
## Table Legend
|
|
155
155
|
|
package/package.json
CHANGED
package/src/table.js
CHANGED
|
@@ -3,24 +3,30 @@ import { stripAnsi } from './ansi.js';
|
|
|
3
3
|
export function renderTable(headers, rows, options = {}) {
|
|
4
4
|
const ascii = Boolean(options.ascii);
|
|
5
5
|
const maxCellWidth = options.maxCellWidth || 60;
|
|
6
|
-
const
|
|
6
|
+
const wrapColumns = normalizeWrapColumns(headers, options.wrapColumns);
|
|
7
|
+
const normalizedRows = rows.map((row) => row.map((cell, index) => {
|
|
7
8
|
const normalized = normalizeCell(cell);
|
|
8
9
|
return {
|
|
9
10
|
...normalized,
|
|
10
|
-
text: truncate(normalized.text, maxCellWidth)
|
|
11
|
+
text: wrapColumns.has(index) ? normalized.text : truncate(normalized.text, maxCellWidth)
|
|
11
12
|
};
|
|
12
13
|
}));
|
|
13
|
-
const widths = headers.map((header, index) => {
|
|
14
|
+
const widths = fitTableWidths(headers, headers.map((header, index) => {
|
|
14
15
|
const values = [truncate(header, maxCellWidth), ...normalizedRows.map((row) => row[index]?.text ?? '')];
|
|
16
|
+
if (wrapColumns.has(index)) {
|
|
17
|
+
return Math.max(...values.map((value) => Math.min(maxCellWidth, visibleLength(value))));
|
|
18
|
+
}
|
|
15
19
|
return Math.max(...values.map(visibleLength));
|
|
16
|
-
});
|
|
20
|
+
}), wrapColumns, terminalWidth(options));
|
|
17
21
|
const style = ascii ? ASCII_TABLE : UNICODE_TABLE;
|
|
18
22
|
|
|
19
23
|
const top = rule(style.topLeft, style.topJoin, style.topRight, style.horizontal, widths);
|
|
20
24
|
const middle = rule(style.midLeft, style.midJoin, style.midRight, style.horizontal, widths);
|
|
21
25
|
const bottom = rule(style.bottomLeft, style.bottomJoin, style.bottomRight, style.horizontal, widths);
|
|
22
26
|
const headerLine = rowLine(headers.map((header) => truncate(header, maxCellWidth)), widths, style, true, options);
|
|
23
|
-
const bodyLines =
|
|
27
|
+
const bodyLines = wrapColumns.size > 0
|
|
28
|
+
? normalizedRows.flatMap((row) => wrappedRowLines(row, widths, style, wrapColumns, options))
|
|
29
|
+
: normalizedRows.map((row) => rowLine(row, widths, style, false, options));
|
|
24
30
|
|
|
25
31
|
return [top, headerLine, middle, ...bodyLines, bottom].join('\n');
|
|
26
32
|
}
|
|
@@ -155,7 +161,7 @@ export function renderSummaryTable(summary, options = {}) {
|
|
|
155
161
|
cell(formatNumber(item.outputTokenThroughput), tones.systemTps),
|
|
156
162
|
cell(formatNumber(item.rps), tones.rps),
|
|
157
163
|
cell(formatPercent(item.latency.cv), cvTone(item.latency.cv)),
|
|
158
|
-
cell(item.available ? '' :
|
|
164
|
+
cell(item.available ? '' : cleanReason(item.skippedReason), item.available ? null : 'yellow')
|
|
159
165
|
];
|
|
160
166
|
|
|
161
167
|
if (mode === 'medium') {
|
|
@@ -194,7 +200,7 @@ export function renderSummaryTable(summary, options = {}) {
|
|
|
194
200
|
? (hasGoodput ? mediumHeaders : mediumHeaders.filter((header) => header !== 'good' && header !== 'good rps'))
|
|
195
201
|
: (hasGoodput ? wideHeaders : wideHeaders.filter((header) => header !== 'good' && header !== 'good rps'));
|
|
196
202
|
|
|
197
|
-
return renderTable(headers, rows, { ...options, maxCellWidth: mode === 'medium' ? 24 : 52 });
|
|
203
|
+
return renderTable(headers, rows, { ...options, maxCellWidth: mode === 'medium' ? 24 : 52, wrapColumns: ['note'] });
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
export function renderDetailTable(summary, options = {}) {
|
|
@@ -241,7 +247,7 @@ export function renderDetailTable(summary, options = {}) {
|
|
|
241
247
|
'description'
|
|
242
248
|
];
|
|
243
249
|
|
|
244
|
-
return renderTable(headers, rows, { ...options, maxCellWidth: mode === 'medium' ? 34 : 52 });
|
|
250
|
+
return renderTable(headers, rows, { ...options, maxCellWidth: mode === 'medium' ? 34 : 52, wrapColumns: ['description'] });
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
export function renderCompactSummary(summary, options = {}) {
|
|
@@ -286,8 +292,8 @@ export function renderModelsTable(models, options = {}) {
|
|
|
286
292
|
cell(model.name),
|
|
287
293
|
cell(model.available ? 'yes' : 'no', model.available ? 'green' : 'yellow'),
|
|
288
294
|
model.description || '-',
|
|
289
|
-
|
|
290
|
-
]), options);
|
|
295
|
+
cleanReason(model.quota || model.reason || '-')
|
|
296
|
+
]), { ...options, wrapColumns: ['description', 'quota'] });
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
function formatRangeMs(low, high) {
|
|
@@ -372,6 +378,69 @@ function renderWrappedTable(headers, rows, widths, options = {}) {
|
|
|
372
378
|
return [top, headerLine, middle, ...bodyLines, bottom].join('\n');
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
function normalizeWrapColumns(headers, wrapColumns = []) {
|
|
382
|
+
const normalized = new Set();
|
|
383
|
+
for (const column of wrapColumns || []) {
|
|
384
|
+
if (Number.isInteger(column)) {
|
|
385
|
+
if (column >= 0 && column < headers.length) normalized.add(column);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const index = headers.findIndex((header) => String(header).toLowerCase() === String(column).toLowerCase());
|
|
390
|
+
if (index >= 0) normalized.add(index);
|
|
391
|
+
}
|
|
392
|
+
return normalized;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function fitTableWidths(headers, widths, wrapColumns, targetWidth) {
|
|
396
|
+
if (!targetWidth || wrapColumns.size === 0 || renderedTableWidth(widths) <= targetWidth) return widths;
|
|
397
|
+
|
|
398
|
+
const fitted = [...widths];
|
|
399
|
+
const minimums = fitted.map((width, index) => (
|
|
400
|
+
wrapColumns.has(index)
|
|
401
|
+
? Math.max(visibleLength(String(headers[index]).toUpperCase()), Math.min(12, width))
|
|
402
|
+
: width
|
|
403
|
+
));
|
|
404
|
+
let excess = renderedTableWidth(fitted) - targetWidth;
|
|
405
|
+
|
|
406
|
+
while (excess > 0) {
|
|
407
|
+
const candidates = [...wrapColumns]
|
|
408
|
+
.filter((index) => fitted[index] > minimums[index])
|
|
409
|
+
.sort((a, b) => fitted[b] - fitted[a]);
|
|
410
|
+
if (candidates.length === 0) break;
|
|
411
|
+
|
|
412
|
+
for (const index of candidates) {
|
|
413
|
+
if (excess <= 0) break;
|
|
414
|
+
fitted[index] -= 1;
|
|
415
|
+
excess -= 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return fitted;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function renderedTableWidth(widths) {
|
|
423
|
+
return widths.reduce((sum, width) => sum + width, 0) + (widths.length * 3) + 1;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function wrappedRowLines(row, widths, style, wrapColumns, options = {}) {
|
|
427
|
+
const wrappedCells = row.map((value, index) => {
|
|
428
|
+
const normalized = normalizeCell(value);
|
|
429
|
+
return {
|
|
430
|
+
tone: normalized.tone,
|
|
431
|
+
lines: wrapColumns.has(index) ? wrapText(normalized.text, widths[index]) : [normalized.text]
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
const height = Math.max(...wrappedCells.map((item) => item.lines.length));
|
|
435
|
+
const lines = [];
|
|
436
|
+
|
|
437
|
+
for (let lineIndex = 0; lineIndex < height; lineIndex += 1) {
|
|
438
|
+
lines.push(rowLine(wrappedCells.map((item) => cell(item.lines[lineIndex] ?? '', item.tone)), widths, style, false, options));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
443
|
+
|
|
375
444
|
function rowLine(row, widths, style, header = false, options = {}) {
|
|
376
445
|
return `${style.vertical}${row.map((cell, index) => {
|
|
377
446
|
const normalized = header ? { text: String(cell).toUpperCase(), tone: 'header' } : normalizeCell(cell);
|
|
@@ -417,8 +486,12 @@ function visibleLength(value) {
|
|
|
417
486
|
return stripAnsi(value).length;
|
|
418
487
|
}
|
|
419
488
|
|
|
489
|
+
function cleanReason(value) {
|
|
490
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
420
493
|
function compactReason(value) {
|
|
421
|
-
const clean =
|
|
494
|
+
const clean = cleanReason(value);
|
|
422
495
|
if (clean.length <= 58) return clean;
|
|
423
496
|
return `${clean.slice(0, 55)}...`;
|
|
424
497
|
}
|