fix(web,cli): carry duration and byte abbreviations into the next unit (#112)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e0a47254af
commit
cd1630ae8f
6 files changed
+75
-8
No files matched your search
@@ -134,8 +134,10 @@ function humanizeDuration(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
const s = ms / 1000;
|
||||
if (s < 60) return `${trimZero(s)}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
return `${m}m${Math.round(s % 60)}s`;
|
||||
// The minute form rounds the total before splitting it: rounding the remainder while
|
||||
// flooring the minutes lets 119.7s print as `1m60s` instead of `2m0s`.
|
||||
const whole = Math.round(s);
|
||||
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
||||
}
|
||||
|
||||
export function formatAbort(p: AbortPayload, t: Messages): string {
|
||||
|
||||
@@ -396,6 +396,23 @@ describe("StreamRenderer", () => {
|
||||
expect(last).toBe("[stats] context 7k (+3k) · tokens 11k (+7k) · 5s (+3s)");
|
||||
});
|
||||
|
||||
it("an elapsed remainder that rounds to 60s carries into the minute", () => {
|
||||
const { stream, text } = collector();
|
||||
const r = new StreamRenderer(stream, t);
|
||||
r.handle(
|
||||
tokenUsage(
|
||||
{ cache_read: 0, cache_write: 0, output: 0, total: 4000 },
|
||||
{ cache_read: 0, cache_write: 0, output: 0, total: 4000 },
|
||||
),
|
||||
);
|
||||
// 119.7s: rounding the remainder against floored minutes would read 1m60s.
|
||||
r.endTask(119_700);
|
||||
const lines = stripAnsi(text()).trim().split("\n");
|
||||
expect(lines[lines.length - 1]).toBe(
|
||||
"[stats] context 4k (+4k) · tokens 4k (+4k) · 2m0s (+2m0s)",
|
||||
);
|
||||
});
|
||||
|
||||
it("context delta goes negative after compaction shrinks the context (no clamping)", () => {
|
||||
const { stream, text } = collector();
|
||||
const r = new StreamRenderer(stream, t);
|
||||
|
||||
@@ -31,9 +31,12 @@ export function humanizeTokens(n: number): string {
|
||||
export function humanizeDuration(ms: number, opts?: { compact?: boolean }): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
const s = ms / 1000;
|
||||
if (s < 60) return opts?.compact && s >= 10 ? `${Math.round(s)}s` : `${trimZero(s)}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
return `${m}m${Math.round(s % 60)}s`;
|
||||
if (s < 60 && !(opts?.compact && s >= 10)) return `${trimZero(s)}s`;
|
||||
// Every whole-second form rounds the total before splitting it: rounding the remainder
|
||||
// while flooring the minutes lets 119.7s print as `1m60s` instead of `2m0s`.
|
||||
const whole = Math.round(s);
|
||||
if (whole < 60) return `${whole}s`;
|
||||
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,12 +131,21 @@ export function formatScore(n: number): string {
|
||||
return Number.isInteger(n) ? `${n}` : trimZero(n);
|
||||
}
|
||||
|
||||
/** True while the one-decimal rendering of `v` still fits its unit: 1023.9 does, 1023.99 does not. */
|
||||
function fitsUnit(v: number): boolean {
|
||||
return Number(v.toFixed(1)) < 1024;
|
||||
}
|
||||
|
||||
/** Abbreviate a byte count: `812B`, `3.4KB`, `1.2MB`. */
|
||||
export function formatBytes(n: number): string {
|
||||
if (n < 1024) return `${n}B`;
|
||||
if (n < 1024 * 1024) return `${trimZero(n / 1024)}KB`;
|
||||
if (n < 1024 * 1024 * 1024) return `${trimZero(n / (1024 * 1024))}MB`;
|
||||
return `${trimZero(n / (1024 * 1024 * 1024))}GB`;
|
||||
// The unit is picked from the rounded value, not the raw one: 1048570 renders as `1024.0`
|
||||
// KB at one decimal, which has to carry into `1MB` rather than print an out-of-range unit.
|
||||
const kb = n / 1024;
|
||||
if (fitsUnit(kb)) return `${trimZero(kb)}KB`;
|
||||
const mb = kb / 1024;
|
||||
if (fitsUnit(mb)) return `${trimZero(mb)}MB`;
|
||||
return `${trimZero(mb / 1024)}GB`;
|
||||
}
|
||||
|
||||
function pad2(n: number): string {
|
||||
|
||||
@@ -57,6 +57,22 @@ describe("humanizeDuration", () => {
|
||||
expect(humanizeDuration(820, { compact: true })).toBe("820ms");
|
||||
expect(humanizeDuration(63000, { compact: true })).toBe("1m3s");
|
||||
});
|
||||
|
||||
it("a seconds remainder that rounds to 60 carries into the minute", () => {
|
||||
// Rounding the remainder against floored minutes would read 1m60s / 59m60s.
|
||||
expect(humanizeDuration(119_500)).toBe("2m0s");
|
||||
expect(humanizeDuration(119_700)).toBe("2m0s");
|
||||
expect(humanizeDuration(179_600)).toBe("3m0s");
|
||||
expect(humanizeDuration(3_599_700)).toBe("60m0s");
|
||||
// Below the carry the remainder still rounds normally.
|
||||
expect(humanizeDuration(119_400)).toBe("1m59s");
|
||||
});
|
||||
|
||||
it("compact promotes a sub-minute value that rounds to 60s into the minute form", () => {
|
||||
expect(humanizeDuration(59_600, { compact: true })).toBe("1m0s");
|
||||
// Without compact the tenths are kept, so there is nothing to carry.
|
||||
expect(humanizeDuration(59_600)).toBe("59.6s");
|
||||
});
|
||||
});
|
||||
|
||||
describe("signedDelta", () => {
|
||||
@@ -139,6 +155,14 @@ describe("formatBytes", () => {
|
||||
expect(formatBytes(3481)).toBe("3.4KB");
|
||||
expect(formatBytes(2 * 1024 * 1024)).toBe("2MB");
|
||||
});
|
||||
|
||||
it("a value that rounds up to 1024 carries into the next unit", () => {
|
||||
// Picking the unit from the raw value would print 1024KB / 1024MB.
|
||||
expect(formatBytes(1024 * 1024 - 6)).toBe("1MB");
|
||||
expect(formatBytes(1024 * 1024 * 1024 - 800)).toBe("1GB");
|
||||
// Just below the carry the unit is unchanged.
|
||||
expect(formatBytes(1024 * 1024 - 60)).toBe("1023.9KB");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeTps", () => {
|
||||
|
||||
Reference in new issue
Block a user