Skip to content

Commit ca9b3a5

Browse files
committed
perf(ccusage): scan Bun block JSONL bytes
Extend the Bun JSONL byte-scan path to block loading. Usage rows are still decoded and parsed through the existing fast parser, while non-usage block timestamps are extracted directly from bytes so block loading avoids decoding every JSONL line on Bun. Node keeps the existing text/file-handle path. On real local Claude data with LOG_LEVEL=0 and COLUMNS=200, the combined Bun byte-scan path improved the pre-byte baseline from daily JSON 419.9ms ± 6.2ms to 362.7ms ± 7.6ms, session JSON 450.2ms ± 20.9ms to 378.0ms ± 9.3ms, and blocks JSON 551.4ms ± 9.2ms to 462.2ms ± 13.8ms. Table output saw the same shape: daily 431.2ms ± 21.5ms to 368.1ms ± 6.9ms, session 442.7ms ± 5.7ms to 379.8ms ± 7.7ms, and blocks 552.9ms ± 12.9ms to 462.8ms ± 12.3ms. Current Rust #977 reference from the same host remains faster: JS daily 368.2ms ± 15.0ms vs Rust 292.9ms ± 4.1ms, JS session 376.4ms ± 10.9ms vs Rust 298.3ms ± 5.5ms, and JS blocks 449.1ms ± 8.0ms vs Rust 297.7ms ± 5.4ms. Validation: pnpm run format; pnpm typecheck; pnpm --filter ccusage exec vitest run src/data-loader.ts --testNamePattern loadSessionBlockData\|loadDailyUsageData\|loadSessionData; pnpm --filter ccusage run build; pnpm run test; Bun/Node JSON parity for daily/session/blocks/monthly/weekly; table parity for daily/session/blocks.
1 parent bb2ebe9 commit ca9b3a5

1 file changed

Lines changed: 53 additions & 2 deletions

File tree

apps/ccusage/src/data-loader.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const REQUEST_ID_MARKER = '"requestId":"';
8484
const SESSION_ID_MARKER = '"sessionId":"';
8585
const SPEED_MARKER = '"speed":"';
8686
const TIMESTAMP_MARKER = '"timestamp":"';
87+
const TIMESTAMP_MARKER_BUFFER = Buffer.from(TIMESTAMP_MARKER);
8788
const VERSION_MARKER = '"version":"';
8889
const VERSION_PATTERN = /^\d+\.\d+\.\d+/;
8990
function parseTwoDigits(value: string, offset: number): number {
@@ -233,6 +234,28 @@ function getTimestampFromLine(line: string): Date | null {
233234
}
234235
}
235236

237+
function getTimestampFromBytes(content: Buffer, lineStart: number, lineEnd: number): Date | null {
238+
const timestampStart = content.indexOf(TIMESTAMP_MARKER_BUFFER, lineStart);
239+
if (timestampStart === -1 || timestampStart >= lineEnd) {
240+
return null;
241+
}
242+
243+
const valueStart = timestampStart + TIMESTAMP_MARKER_BUFFER.length;
244+
const valueEnd = content.indexOf(34, valueStart);
245+
if (valueEnd === -1 || valueEnd > lineEnd) {
246+
return null;
247+
}
248+
249+
const timestamp = content.subarray(valueStart, valueEnd).toString('utf8');
250+
const parsedTimestampMs = parseIsoTimestampMs(timestamp);
251+
if (!Number.isNaN(parsedTimestampMs)) {
252+
return new Date(parsedTimestampMs);
253+
}
254+
255+
const date = new Date(timestamp);
256+
return Number.isNaN(date.getTime()) ? null : date;
257+
}
258+
236259
/**
237260
* Get Claude data directories to search for usage data
238261
* When CLAUDE_CONFIG_DIR is set: uses only those paths
@@ -2008,7 +2031,7 @@ async function collectBlockFileResult(
20082031
}
20092032
};
20102033

2011-
await processJSONLFileByLine(file, (line) => {
2034+
const processLine = (line: string): void => {
20122035
try {
20132036
if (!line.includes(USAGE_LINE_MARKER)) {
20142037
const lineTimestamp = getTimestampFromLine(line);
@@ -2083,7 +2106,35 @@ async function collectBlockFileResult(
20832106
`Skipping invalid JSON line in 5-hour blocks: ${error instanceof Error ? error.message : String(error)}`,
20842107
);
20852108
}
2086-
});
2109+
};
2110+
2111+
const bytes = await readBufferedJSONLBytes(file);
2112+
if (bytes != null) {
2113+
const content = Buffer.isBuffer(bytes)
2114+
? bytes
2115+
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2116+
let lineStart = 0;
2117+
let markerIndex = content.indexOf(USAGE_LINE_MARKER_BUFFER);
2118+
while (lineStart < content.length) {
2119+
let lineEnd = content.indexOf(10, lineStart);
2120+
if (lineEnd === -1) {
2121+
lineEnd = content.length;
2122+
}
2123+
const decodeEnd = lineEnd > lineStart && content[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
2124+
if (markerIndex !== -1 && markerIndex < decodeEnd) {
2125+
processLine(content.subarray(lineStart, decodeEnd).toString('utf8'));
2126+
markerIndex = content.indexOf(USAGE_LINE_MARKER_BUFFER, lineEnd + 1);
2127+
} else {
2128+
const lineTimestamp = getTimestampFromBytes(content, lineStart, decodeEnd);
2129+
if (lineTimestamp != null) {
2130+
setEarliestTimestamp(lineTimestamp, lineTimestamp.getTime());
2131+
}
2132+
}
2133+
lineStart = lineEnd + 1;
2134+
}
2135+
} else {
2136+
await processJSONLFileByLine(file, processLine);
2137+
}
20872138

20882139
return {
20892140
file,

0 commit comments

Comments
 (0)