diff --git a/.jules/bolt.md b/.jules/bolt.md index 7985023..2d79172 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -41,3 +41,6 @@ ## 2024-05-15 - Fast cache keys using integer math **Learning:** Calling `new Date().toISOString().slice()` in hot paths (like `cached()` key generation during a backtest loop) adds up significantly (~1ms per thousand calls). **Action:** When grouping cache keys by day, use fast integer math `Math.floor(unixSecs / 86400)` instead of string-based date allocations to maintain identical semantics while avoiding object instantiation. +## 2025-01-01 - Consolidating Array Operations +**Learning:** Replacing chained `.reduce()` calls that iterate over the same array multiple times with a single `for` loop reduces iterations from O(N*K) to O(N). Additionally, avoiding object allocations inside hot loops (like in `statsFromRecords`) prevents unnecessary garbage collection overhead. +**Action:** When extracting multiple aggregates from a single array in a hot path, always favor a single `for` loop over chaining multiple `.reduce()` or `.map()` passes. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index 2e878df..628fec0 100644 --- a/src/agent/backtestRunner.ts +++ b/src/agent/backtestRunner.ts @@ -147,10 +147,13 @@ async function markToMarket( priceAt: (ticker: string) => number | null, ): Promise { const snap = await broker.snapshot(); - return snap.positions.reduce( - (sum, p) => sum + positionValue(p, priceAt(p.ticker)), - snap.cashVnd, - ); + // ⚡ Bolt: Replaced .reduce() with standard loop to avoid function allocation in hot path + let sum = snap.cashVnd; + for (let i = 0; i < snap.positions.length; i++) { + const p = snap.positions[i]!; + sum += positionValue(p, priceAt(p.ticker)); + } + return sum; } async function recordGuardrailReject( @@ -464,14 +467,25 @@ export async function runBacktestSession( const finalBench = last?.benchmark_mtm_vnd ?? opts.initialCash; const totalReturn = (finalMtm / opts.initialCash - 1) * 100; const benchReturn = (finalBench / opts.initialCash - 1) * 100; + // ⚡ Bolt: Consolidated multiple .reduce() passes into single loops for O(n) instead of O(n*k) performance + let runningPeak = opts.initialCash; - const maxDD = equityRows.reduce((mn, r) => { + let maxDD = 0; + for (let i = 0; i < equityRows.length; i++) { + const r = equityRows[i]!; runningPeak = Math.max(runningPeak, r.mtm_vnd); - return Math.min(mn, r.mtm_vnd / runningPeak - 1); - }, 0); - const totalCost = turnRows.reduce((s, r) => s + (r.cost_usd ?? 0), 0); - const totalIn = turnRows.reduce((s, r) => s + (r.in_tokens ?? 0), 0); - const totalOut = turnRows.reduce((s, r) => s + (r.out_tokens ?? 0), 0); + maxDD = Math.min(maxDD, r.mtm_vnd / runningPeak - 1); + } + + let totalCost = 0; + let totalIn = 0; + let totalOut = 0; + for (let i = 0; i < turnRows.length; i++) { + const r = turnRows[i]!; + totalCost += r.cost_usd ?? 0; + totalIn += r.in_tokens ?? 0; + totalOut += r.out_tokens ?? 0; + } const summary: SummaryPayload = { runId, diff --git a/src/tui/hooks/useAgentStream.ts b/src/tui/hooks/useAgentStream.ts index a9a3a82..eea1ef3 100644 --- a/src/tui/hooks/useAgentStream.ts +++ b/src/tui/hooks/useAgentStream.ts @@ -97,15 +97,22 @@ function blocksFromRecords(records: SessionRecord[]): ChatBlock[] { } function statsFromRecords(records: SessionRecord[]): TurnStats { - return records.reduce((acc, r) => { - if (r.type !== "result") return acc; - return { - inTokens: acc.inTokens + (r.usage?.inputTokens ?? 0), - outTokens: acc.outTokens + (r.usage?.outputTokens ?? 0), - costUsd: acc.costUsd + (r.costUsd ?? 0), - sessionId: r.sdkSessionId ?? acc.sessionId, - }; - }, { inTokens: 0, outTokens: 0, costUsd: 0 }); + // ⚡ Bolt: Replaced .reduce() with standard loop to avoid object allocation and GC overhead per record + let inTokens = 0; + let outTokens = 0; + let costUsd = 0; + let sessionId: string | undefined; + + for (let i = 0; i < records.length; i++) { + const r = records[i]!; + if (r.type !== "result") continue; + inTokens += r.usage?.inputTokens ?? 0; + outTokens += r.usage?.outputTokens ?? 0; + costUsd += r.costUsd ?? 0; + if (r.sdkSessionId) sessionId = r.sdkSessionId; + } + + return { inTokens, outTokens, costUsd, sessionId }; } function truncateText(value: string, max = 120): string {