Skip to content

Commit bb2ebe9

Browse files
committed
perf(ccusage): scan Bun JSONL usage bytes
For Bun usage-only JSONL loading, read buffered files with Bun.file().bytes() and scan the byte buffer for the usage marker before decoding only matching usage rows. Node keeps the existing text path, and blocks still uses the full text path because it needs non-usage timestamps. This avoids decoding the full 1.28GB local JSONL corpus for daily/session commands when most non-usage content is discarded before parsing. Built Bun JSON A/B against 5db98c7: daily --offline --json 423.3ms ± 7.0ms -> 370.6ms ± 24.7ms (~1.14x faster), session 443.3ms ± 19.1ms -> 373.9ms ± 2.7ms (~1.19x faster), blocks 556.2ms ± 22.0ms -> 549.3ms ± 5.5ms (noise-level, mostly unchanged). Built Bun table A/B: daily table 426.7ms ± 9.3ms -> 363.0ms ± 9.3ms (~1.18x faster), session table 442.3ms ± 3.8ms -> 384.2ms ± 14.0ms (~1.15x faster), blocks table 560.3ms ± 16.5ms -> 554.5ms ± 10.6ms (noise-level). Rust reference after this change: JS daily 366.1ms ± 11.9ms vs Rust #977 daily 294.4ms ± 3.5ms (Rust ~1.24x faster); JS session 388.4ms ± 20.4ms vs Rust 300.3ms ± 3.8ms (Rust ~1.29x faster). Output parity matched the previous built dist for daily/session/blocks/monthly/weekly JSON and daily/session/blocks table output. Node and Bun JSON outputs matched for daily/session/blocks/monthly/weekly. Build size is 404.85 kB. Validation: pnpm run format, pnpm typecheck, targeted data-loader tests, pnpm --filter ccusage run build, and pnpm run test (28 files, 351 passed, 1 skipped).
1 parent 5db98c7 commit bb2ebe9

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

apps/ccusage/src/data-loader.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { unreachable } from './_utils.ts';
6969
import { logger } from './logger.ts';
7070

7171
const USAGE_LINE_MARKER = '"usage":{';
72+
const USAGE_LINE_MARKER_BUFFER = Buffer.from(USAGE_LINE_MARKER);
7273
const CACHE_CREATION_INPUT_TOKENS_MARKER = '"cache_creation_input_tokens":';
7374
const CACHE_READ_INPUT_TOKENS_MARKER = '"cache_read_input_tokens":';
7475
const COST_USD_MARKER = '"costUSD":';
@@ -861,6 +862,7 @@ type UsageSummaryAccumulator = {
861862

862863
type BunFileLike = {
863864
size: number;
865+
bytes: () => Promise<Uint8Array>;
864866
text: () => Promise<string>;
865867
};
866868

@@ -1205,6 +1207,43 @@ async function processBufferedJSONLUsageContent(
12051207
}
12061208
}
12071209

1210+
async function processBufferedJSONLUsageBytes(
1211+
bytes: Uint8Array,
1212+
processLine: (line: string) => void,
1213+
): Promise<void> {
1214+
const content = Buffer.isBuffer(bytes)
1215+
? bytes
1216+
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1217+
let lineStart = 0;
1218+
let markerIndex = content.indexOf(USAGE_LINE_MARKER_BUFFER);
1219+
while (markerIndex !== -1) {
1220+
let lineEnd = content.indexOf(10, lineStart);
1221+
while (lineEnd !== -1 && lineEnd < markerIndex) {
1222+
lineStart = lineEnd + 1;
1223+
lineEnd = content.indexOf(10, lineStart);
1224+
}
1225+
if (lineEnd === -1) {
1226+
lineEnd = content.length;
1227+
}
1228+
1229+
const decodeEnd = lineEnd > lineStart && content[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
1230+
processLine(content.subarray(lineStart, decodeEnd).toString('utf8'));
1231+
1232+
lineStart = lineEnd + 1;
1233+
markerIndex = content.indexOf(USAGE_LINE_MARKER_BUFFER, lineStart);
1234+
}
1235+
}
1236+
1237+
async function readBufferedJSONLBytes(filePath: string): Promise<Uint8Array | null> {
1238+
const bun = getBunRuntime();
1239+
if (bun == null) {
1240+
return null;
1241+
}
1242+
1243+
const file = bun.file(filePath);
1244+
return file.size <= MAX_BUFFERED_JSONL_BYTES ? file.bytes() : null;
1245+
}
1246+
12081247
async function readBufferedJSONLContent(filePath: string): Promise<string | null> {
12091248
const bun = getBunRuntime();
12101249
if (bun != null) {
@@ -1264,6 +1303,12 @@ async function processJSONLUsageFileByLine(
12641303
filePath: string,
12651304
processLine: (line: string) => void,
12661305
): Promise<void> {
1306+
const bytes = await readBufferedJSONLBytes(filePath);
1307+
if (bytes != null) {
1308+
await processBufferedJSONLUsageBytes(bytes, processLine);
1309+
return;
1310+
}
1311+
12671312
const content = await readBufferedJSONLContent(filePath);
12681313
if (content != null) {
12691314
await processBufferedJSONLUsageContent(content, processLine);

0 commit comments

Comments
 (0)