Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<SessionTimelineChart
usageTimeline={usageTimeline}
messages={[]}
sessionStartedAt="2023-01-01T00:00:00.000Z"
/>
)

expect(parseSpy).toHaveBeenCalledTimes(usageTimeline.length)
expect(readChartData().map(({ input }) => input)).toEqual([100, 200, 300])
})
})
12 changes: 7 additions & 5 deletions packages/web/src/components/dashboard/session-timeline-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,20 @@ 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
)

let toolIndex = 0
const cumulativeToolCounts = new Map<string, number>()

return sortedUsage.map((usage) => {
const currentTimestamp = Date.parse(usage.timestamp)
return sortedUsage.map(({ original: usage, parsedTimestamp: currentTimestamp }) => {

while (
toolIndex < sortedTools.length &&
Expand Down
Loading