diff --git a/.jules/bolt.md b/.jules/bolt.md index 9312d3f..0e80a59 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,7 @@ ## 2024-05-18 - Avoid unnecessary array allocations in frequent I/O paths **Learning:** `upsertSession` in `src/runtime/sessionStore.ts` is called very frequently (every time a session record is appended, which happens constantly during agent streaming). The original implementation used `.filter()` to remove the existing session and then pushed the updated one, resulting in significant garbage collection overhead and an O(N) array allocation on every single token/event stream chunk. Since this function is the bottleneck for chat interactivity, replacing `.filter()` with `.findIndex()` and in-place assignment yielded a > 2x speedup on session updates. **Action:** When updating arrays that back frequent disk I/O operations (like the session store), always prefer in-place mutation and sorting over immutable array recreation (`.filter()`, `.map()`) to minimize garbage collection pauses. + +## 2026-08-09 - Avoiding Chained Array Allocations in High-Frequency Math +**Learning:** In signal processing like `buildCandidate` (used heavily during stock discovery), chained array methods (`.map().slice().reduce()`) over hundreds of time-series data points cause rapid short-lived allocations, triggering GC pauses. +**Action:** Replace primitive array `.map()` allocations with source object arrays, and use in-place `for` loops indexed with `Math.max(0, len - N)` to emulate `.slice()` gracefully without allocations. diff --git a/package.json b/package.json index 92516ea..b88a672 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "better-sqlite3" ], "overrides": { - "undici": ">=7.28.0 <8.0.0", + "undici": ">=7.29.0 <8.0.0", "ws": ">=8.21.0" } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abb401c..5e9a243 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - undici: '>=7.28.0 <8.0.0' + undici: '>=7.29.0 <8.0.0' ws: '>=8.21.0' importers: @@ -37,8 +37,8 @@ importers: specifier: ^3.1.0 version: 3.1.0 undici: - specifier: '>=7.28.0 <8.0.0' - version: 7.28.0 + specifier: '>=7.29.0 <8.0.0' + version: 7.29.0 yaml: specifier: ^2.6.0 version: 2.8.3 @@ -1520,8 +1520,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} universalify@0.1.2: @@ -2281,7 +2281,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.28.0 + undici: 7.29.0 whatwg-mimetype: 4.0.0 chownr@1.1.4: {} @@ -2986,7 +2986,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.28.0: {} + undici@7.29.0: {} universalify@0.1.2: {} diff --git a/src/tools/discover.ts b/src/tools/discover.ts index 413e476..c5dafce 100644 --- a/src/tools/discover.ts +++ b/src/tools/discover.ts @@ -54,13 +54,14 @@ function pct(now: number, prev: number | undefined): number | null { return ((now - prev) / prev) * 100; } -function rsi14(closes: number[]): number | null { - if (closes.length < 15) return null; - const window = closes.slice(-15); +function rsi14(bars: readonly { close: number }[]): number | null { + const len = bars.length; + if (len < 15) return null; let gains = 0; let losses = 0; - for (let i = 1; i < window.length; i++) { - const d = window[i]! - window[i - 1]!; + const start = Math.max(0, len - 15); + for (let i = start + 1; i < len; i++) { + const d = bars[i]!.close - bars[i - 1]!.close; if (d > 0) gains += d; else losses -= d; } @@ -80,7 +81,8 @@ async function buildCandidate(ticker: string): Promise { 600, () => getStockOhlcv(ticker, "1D", from, to), ).catch(() => [] as Awaited>); - if (bars.length < 25) { + const len = bars.length; + if (len < 25) { return { ticker, metric: null, @@ -91,24 +93,34 @@ async function buildCandidate(ticker: string): Promise { vol_ratio: null, }; } - const closes = bars.map((b) => b.close); - const vols = bars.map((b) => b.volume); - const last = closes[closes.length - 1]!; - const prev1w = closes[closes.length - 6]; - const prev1m = closes[closes.length - 22]; - const recentVol = vols.slice(-5).reduce((a, b) => a + b, 0) / 5; - const priorVol = - vols.length >= 25 - ? vols.slice(-25, -5).reduce((a, b) => a + b, 0) / 20 - : null; + + // ⚡ Bolt: Removed .map() allocations for `closes` and `vols`, using original array directly. + const last = bars[len - 1]!.close; + const prev1w = bars[len - 6]?.close; + const prev1m = bars[len - 22]?.close; + + // ⚡ Bolt: Replaced chained .slice().reduce() with in-place for loops + let recentVolSum = 0; + for (let i = Math.max(0, len - 5); i < len; i++) { + recentVolSum += bars[i]!.volume; + } + const recentVol = recentVolSum / 5; + + let priorVolSum = 0; + for (let i = Math.max(0, len - 25); i < len - 5; i++) { + priorVolSum += bars[i]!.volume; + } + const priorVol = len >= 25 ? priorVolSum / 20 : null; + const volRatio = priorVol != null && priorVol > 0 ? recentVol / priorVol : null; + return { ticker, metric: null, latest_close: last, ret_1w: pct(last, prev1w), ret_1m: pct(last, prev1m), - rsi14: rsi14(closes), + rsi14: rsi14(bars), vol_ratio: volRatio, }; }