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 &&