Skip to content

Commit 335331e

Browse files
committed
perf(ccusage): cache usage date formatting
Cache formatted usage dates per UTC hour while loading reports. The hot path previously created a Date and formatted through Intl for every usage line; daily, session, and block loading now reuse a per-command formatter/cache so repeated entries in the same hour do not pay that cost again. Reliable runtime benchmark data is not included in this commit because process checks still showed cmux at roughly 150% CPU and Arc at roughly 60% CPU. The built bundle is 516.30 kB total after this change. Verified with pnpm run format, pnpm typecheck, pnpm --filter ccusage exec vitest run src/data-loader.ts src/_date-utils.ts src/debug.ts, pnpm --filter ccusage run build, pnpm run test, JS/Rust token-total parity checks for daily/session/monthly/weekly/blocks, and timezone parity checks for UTC, Asia/Tokyo, America/New_York, and Europe/London.
1 parent 641f7a1 commit 335331e

2 files changed

Lines changed: 22 additions & 6 deletions

File tree

‎apps/ccusage/src/_date-utils.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ export function formatDate(dateStr: string, timezone?: string): string {
4949
return formatter.format(date);
5050
}
5151

52+
export function createCachedDateFormatter(timezone?: string): (dateStr: string) => string {
53+
const formatter = createDateFormatter(timezone);
54+
const cache = new Map<string, string>();
55+
return (dateStr: string): string => {
56+
const cacheKey = dateStr.slice(0, 13);
57+
const cached = cache.get(cacheKey);
58+
if (cached != null) {
59+
return cached;
60+
}
61+
const formatted = formatter.format(new Date(dateStr));
62+
cache.set(cacheKey, formatted);
63+
return formatted;
64+
};
65+
}
66+
5267
/**
5368
* Generic function to sort items by date based on sort order
5469
* @param items - Array of items to sort

‎apps/ccusage/src/data-loader.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
USER_HOME_DIR,
3333
} from './_consts.ts';
3434
import {
35+
createCachedDateFormatter,
3536
filterByDateRange,
3637
formatDate,
3738
formatDateCompact,
@@ -1223,6 +1224,7 @@ export async function loadDailyUsageData(options?: LoadOptions): Promise<DailyUs
12231224

12241225
// Use PricingFetcher with using statement for automatic cleanup
12251226
using fetcher = mode === 'display' ? null : new PricingFetcher(options?.offline);
1227+
const formatUsageDate = createCachedDateFormatter(options?.timezone);
12261228

12271229
type DailyEntry = {
12281230
data: UsageData;
@@ -1254,7 +1256,7 @@ export async function loadDailyUsageData(options?: LoadOptions): Promise<DailyUs
12541256
return;
12551257
}
12561258

1257-
const date = formatDate(data.timestamp, options?.timezone);
1259+
const date = formatUsageDate(data.timestamp);
12581260
const immediateCost = getImmediateCostForEntry(data, mode);
12591261
if (immediateCost !== undefined) {
12601262
entries.push({
@@ -1413,6 +1415,7 @@ export async function loadSessionData(options?: LoadOptions): Promise<SessionUsa
14131415

14141416
// Use PricingFetcher with using statement for automatic cleanup
14151417
using fetcher = mode === 'display' ? null : new PricingFetcher(options?.offline);
1418+
const formatUsageDate = createCachedDateFormatter(options?.timezone);
14161419

14171420
// Collect all valid data entries with session info first
14181421
const allEntries: Array<{
@@ -1563,7 +1566,7 @@ export async function loadSessionData(options?: LoadOptions): Promise<SessionUsa
15631566
sessionId: createSessionId(latestEntry.sessionId),
15641567
projectPath: createProjectPath(latestEntry.projectPath),
15651568
...totals,
1566-
lastActivity: formatDate(latestEntry.timestamp, options?.timezone) as ActivityDate,
1569+
lastActivity: formatUsageDate(latestEntry.timestamp) as ActivityDate,
15671570
versions: Array.from(new Set(versions)).sort() as Version[],
15681571
modelsUsed: modelsUsed as ModelName[],
15691572
modelBreakdowns,
@@ -1912,6 +1915,7 @@ export async function loadSessionBlockData(options?: LoadOptions): Promise<Sessi
19121915

19131916
// Use PricingFetcher with using statement for automatic cleanup
19141917
using fetcher = mode === 'display' ? null : new PricingFetcher(options?.offline);
1918+
const formatUsageDate = createCachedDateFormatter(options?.timezone);
19151919

19161920
// Collect all valid data entries first
19171921
const allEntries: LoadedUsageEntry[] = [];
@@ -2045,10 +2049,7 @@ export async function loadSessionBlockData(options?: LoadOptions): Promise<Sessi
20452049
(options?.since != null && options.since !== '') ||
20462050
(options?.until != null && options.until !== '')
20472051
? blocks.filter((block) => {
2048-
const blockDateStr = formatDate(block.startTime.toISOString(), options?.timezone).replace(
2049-
/-/g,
2050-
'',
2051-
);
2052+
const blockDateStr = formatUsageDate(block.startTime.toISOString()).replace(/-/g, '');
20522053
if (options.since != null && options.since !== '' && blockDateStr < options.since) {
20532054
return false;
20542055
}

0 commit comments

Comments
 (0)