diff --git a/.jules/bolt.md b/.jules/bolt.md index 57daf471..bd3d478f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -3,3 +3,7 @@ **Learning:** `Date.parse(value)` returns the timestamp primitive directly, while `new Date(value).getTime()` also constructs a `Date` object. Both use the same ECMAScript string-parsing semantics for these call sites. **Action:** In frequently executed paths that only need a timestamp primitive, prefer `Date.parse(value)`. Treat the allocation reduction as a bounded micro-optimization unless a committed benchmark establishes a larger runtime effect. + +## 2026-09-04 - Avoid Expensive Operations in Sort Comparators +**Learning:** Operations like `Date.parse()` execute `O(N log N)` times when placed directly inside `Array.prototype.sort()` comparators. This creates a severe performance bottleneck when sorting large datasets like timelines. +**Action:** Use a Schwartzian transform (map-sort-map) to pre-compute the transformed values in O(N) time before sorting, passing a wrapper object `{ original, parsedValue }` to preserve memory efficiency. diff --git a/packages/web/src/components/dashboard/session-timeline-chart.test.tsx b/packages/web/src/components/dashboard/session-timeline-chart.test.tsx index 31d6758b..d7e7d81b 100644 --- a/packages/web/src/components/dashboard/session-timeline-chart.test.tsx +++ b/packages/web/src/components/dashboard/session-timeline-chart.test.tsx @@ -30,7 +30,7 @@ function readChartData(): Array<{ describe('SessionTimelineChart', () => { afterEach(() => { cleanup() - vi.clearAllMocks() + vi.restoreAllMocks() }) it('renders "No timeline data available" when usageTimeline is empty', () => { @@ -275,4 +275,48 @@ describe('SessionTimelineChart', () => { expect(usageTimeline.map(({ timestamp }) => timestamp)).toEqual(originalUsageOrder) expect(messages.map(({ timestamp }) => timestamp)).toEqual(originalMessageOrder) }) + + it('parses each usage timestamp once while sorting and merging chart rows', () => { + const usageTimeline: SessionTimelineUsage[] = [ + { + timestamp: '2023-01-01T00:03:00.000Z', + inputTokens: 300, + outputTokens: 30, + estimatedCostUsd: 0.003, + model: 'gpt-4', + isSubagent: false, + }, + { + timestamp: '2023-01-01T00:01:00.000Z', + inputTokens: 100, + outputTokens: 10, + estimatedCostUsd: 0.001, + model: 'gpt-4', + isSubagent: false, + }, + { + timestamp: '2023-01-01T00:02:00.000Z', + inputTokens: 200, + outputTokens: 20, + estimatedCostUsd: 0.002, + model: 'gpt-4', + isSubagent: false, + }, + ] + const originalDateParse = Date.parse.bind(Date) + const parseSpy = vi + .spyOn(Date, 'parse') + .mockImplementation((timestamp) => originalDateParse(timestamp)) + + render( + + ) + + expect(parseSpy).toHaveBeenCalledTimes(usageTimeline.length) + expect(readChartData().map(({ input }) => input)).toEqual([100, 200, 300]) + }) }) diff --git a/packages/web/src/components/dashboard/session-timeline-chart.tsx b/packages/web/src/components/dashboard/session-timeline-chart.tsx index 222d0b22..9370fd36 100644 --- a/packages/web/src/components/dashboard/session-timeline-chart.tsx +++ b/packages/web/src/components/dashboard/session-timeline-chart.tsx @@ -67,9 +67,12 @@ function buildChartData( toolCalls: ToolCallPoint[], sessionStartedAt: string ): ChartDataItem[] { - const sortedUsage = [...usageTimeline].sort( - (a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) - ) + // [Bolt: Performance Optimization] Use Schwartzian transform (map-sort-map) to avoid + // O(N log N) Date.parse() calls during sort. This reduces parsing overhead to O(N). + const sortedUsage = usageTimeline + .map(usage => ({ original: usage, parsedTimestamp: Date.parse(usage.timestamp) })) + .sort((a, b) => a.parsedTimestamp - b.parsedTimestamp) + const sortedTools = [...toolCalls].sort( (a, b) => a.parsedTimestamp - b.parsedTimestamp ) @@ -77,8 +80,7 @@ function buildChartData( let toolIndex = 0 const cumulativeToolCounts = new Map() - return sortedUsage.map((usage) => { - const currentTimestamp = Date.parse(usage.timestamp) + return sortedUsage.map(({ original: usage, parsedTimestamp: currentTimestamp }) => { while ( toolIndex < sortedTools.length &&