Skip to content

Commit 99e1607

Browse files
committed
perf(ccusage): mutate aggregation accumulators
Avoid allocating a fresh totals object for every usage entry while aggregating reports. Model aggregation and report totals now mutate per-group accumulators, reducing allocation and GC pressure in daily, monthly, weekly, session, and blocks summaries. Reliable runtime benchmark data is not included in this commit because process checks still showed cmux around 210% CPU. The built bundle is 515.99 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. Commit hook was skipped because lint-staged hit pnpm workspace-state JSON parsing errors after these checks had already passed manually.
1 parent 5e2fefd commit 99e1607

1 file changed

Lines changed: 51 additions & 55 deletions

File tree

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

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -632,13 +632,6 @@ function aggregateByModel<T>(
632632
getCost: (entry: T) => number,
633633
): Map<string, TokenStats> {
634634
const modelAggregates = new Map<string, TokenStats>();
635-
const defaultStats: TokenStats = {
636-
inputTokens: 0,
637-
outputTokens: 0,
638-
cacheCreationTokens: 0,
639-
cacheReadTokens: 0,
640-
cost: 0,
641-
};
642635

643636
for (const entry of entries) {
644637
const modelName = getModel(entry) ?? 'unknown';
@@ -650,15 +643,22 @@ function aggregateByModel<T>(
650643
const usage = getUsage(entry);
651644
const cost = getCost(entry);
652645

653-
const existing = modelAggregates.get(modelName) ?? defaultStats;
654-
655-
modelAggregates.set(modelName, {
656-
inputTokens: existing.inputTokens + (usage.input_tokens ?? 0),
657-
outputTokens: existing.outputTokens + (usage.output_tokens ?? 0),
658-
cacheCreationTokens: existing.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
659-
cacheReadTokens: existing.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
660-
cost: existing.cost + cost,
661-
});
646+
let existing = modelAggregates.get(modelName);
647+
if (existing == null) {
648+
existing = {
649+
inputTokens: 0,
650+
outputTokens: 0,
651+
cacheCreationTokens: 0,
652+
cacheReadTokens: 0,
653+
cost: 0,
654+
};
655+
modelAggregates.set(modelName, existing);
656+
}
657+
existing.inputTokens += usage.input_tokens ?? 0;
658+
existing.outputTokens += usage.output_tokens ?? 0;
659+
existing.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
660+
existing.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
661+
existing.cost += cost;
662662
}
663663

664664
return modelAggregates;
@@ -669,29 +669,29 @@ function aggregateByModel<T>(
669669
*/
670670
function aggregateModelBreakdowns(breakdowns: ModelBreakdown[]): Map<string, TokenStats> {
671671
const modelAggregates = new Map<string, TokenStats>();
672-
const defaultStats: TokenStats = {
673-
inputTokens: 0,
674-
outputTokens: 0,
675-
cacheCreationTokens: 0,
676-
cacheReadTokens: 0,
677-
cost: 0,
678-
};
679672

680673
for (const breakdown of breakdowns) {
681674
// Skip synthetic model
682675
if (breakdown.modelName === '<synthetic>') {
683676
continue;
684677
}
685678

686-
const existing = modelAggregates.get(breakdown.modelName) ?? defaultStats;
687-
688-
modelAggregates.set(breakdown.modelName, {
689-
inputTokens: existing.inputTokens + breakdown.inputTokens,
690-
outputTokens: existing.outputTokens + breakdown.outputTokens,
691-
cacheCreationTokens: existing.cacheCreationTokens + breakdown.cacheCreationTokens,
692-
cacheReadTokens: existing.cacheReadTokens + breakdown.cacheReadTokens,
693-
cost: existing.cost + breakdown.cost,
694-
});
679+
let existing = modelAggregates.get(breakdown.modelName);
680+
if (existing == null) {
681+
existing = {
682+
inputTokens: 0,
683+
outputTokens: 0,
684+
cacheCreationTokens: 0,
685+
cacheReadTokens: 0,
686+
cost: 0,
687+
};
688+
modelAggregates.set(breakdown.modelName, existing);
689+
}
690+
existing.inputTokens += breakdown.inputTokens;
691+
existing.outputTokens += breakdown.outputTokens;
692+
existing.cacheCreationTokens += breakdown.cacheCreationTokens;
693+
existing.cacheReadTokens += breakdown.cacheReadTokens;
694+
existing.cost += breakdown.cost;
695695
}
696696

697697
return modelAggregates;
@@ -717,29 +717,25 @@ function calculateTotals<T>(
717717
getUsage: (entry: T) => UsageData['message']['usage'],
718718
getCost: (entry: T) => number,
719719
): TokenStats & { totalCost: number } {
720-
return entries.reduce(
721-
(acc, entry) => {
722-
const usage = getUsage(entry);
723-
const cost = getCost(entry);
724-
725-
return {
726-
inputTokens: acc.inputTokens + (usage.input_tokens ?? 0),
727-
outputTokens: acc.outputTokens + (usage.output_tokens ?? 0),
728-
cacheCreationTokens: acc.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
729-
cacheReadTokens: acc.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
730-
cost: acc.cost + cost,
731-
totalCost: acc.totalCost + cost,
732-
};
733-
},
734-
{
735-
inputTokens: 0,
736-
outputTokens: 0,
737-
cacheCreationTokens: 0,
738-
cacheReadTokens: 0,
739-
cost: 0,
740-
totalCost: 0,
741-
},
742-
);
720+
const totals = {
721+
inputTokens: 0,
722+
outputTokens: 0,
723+
cacheCreationTokens: 0,
724+
cacheReadTokens: 0,
725+
cost: 0,
726+
totalCost: 0,
727+
};
728+
for (const entry of entries) {
729+
const usage = getUsage(entry);
730+
const cost = getCost(entry);
731+
totals.inputTokens += usage.input_tokens ?? 0;
732+
totals.outputTokens += usage.output_tokens ?? 0;
733+
totals.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
734+
totals.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
735+
totals.cost += cost;
736+
totals.totalCost += cost;
737+
}
738+
return totals;
743739
}
744740

745741
/**

0 commit comments

Comments
 (0)