Skip to content
Closed
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-08-11 - Use map-sort-map (Schwartzian transform) for slow comparators
**Learning:** `Date.parse()` called inside `Array.prototype.sort()` results in redundant string parsing because the comparator is executed `O(n log n)` times.
**Action:** Transform arrays to precompute the parsed values into primitives first (using `.map()`), sort those primitives, then use the precomputed values. This transforms `O(N log N)` parsing overhead into `O(N)`.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import React from 'react'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SessionDetail, SessionTimelineUsage } from '@argos/shared'

const formatRelativeTimeSpy = vi.hoisted(() =>
vi.fn((_timestamp: string | number, _baseTimestamp: string | number) => '+1m')
)

vi.mock('@/lib/format', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/format')>()
return { ...actual, formatRelativeTime: formatRelativeTimeSpy }
})

import { SessionTimelineChart } from './session-timeline-chart'

vi.mock('recharts', async () => {
Expand Down Expand Up @@ -275,4 +285,32 @@ describe('SessionTimelineChart', () => {
expect(usageTimeline.map(({ timestamp }) => timestamp)).toEqual(originalUsageOrder)
expect(messages.map(({ timestamp }) => timestamp)).toEqual(originalMessageOrder)
})
it('reuses parsed event and session timestamps for relative labels', () => {
const timestamp = '2023-01-01T00:01:00.000Z'
const sessionStartedAt = '2023-01-01T00:00:00.000Z'
const usageTimeline: SessionTimelineUsage[] = [
{
timestamp,
inputTokens: 100,
outputTokens: 50,
estimatedCostUsd: 0.001,
model: 'gpt-4',
isSubagent: false,
},
]

render(
<SessionTimelineChart
usageTimeline={usageTimeline}
messages={[]}
sessionStartedAt={sessionStartedAt}
/>
)

expect(formatRelativeTimeSpy).toHaveBeenCalledWith(
Date.parse(timestamp),
Date.parse(sessionStartedAt)
)
})

})
14 changes: 7 additions & 7 deletions packages/web/src/components/dashboard/session-timeline-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,19 @@ function buildChartData(
toolCalls: ToolCallPoint[],
sessionStartedAt: string
): ChartDataItem[] {
const sortedUsage = [...usageTimeline].sort(
(a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)
)
const sortedUsage = usageTimeline
.map((usage) => ({ usage, parsed: Date.parse(usage.timestamp) }))
.sort((a, b) => a.parsed - b.parsed)

const sortedTools = [...toolCalls].sort(
(a, b) => a.parsedTimestamp - b.parsedTimestamp
)

let toolIndex = 0
const cumulativeToolCounts = new Map<string, number>()
const sessionStartedAtMs = Date.parse(sessionStartedAt)

return sortedUsage.map((usage) => {
const currentTimestamp = Date.parse(usage.timestamp)

return sortedUsage.map(({ usage, parsed: currentTimestamp }) => {
while (
toolIndex < sortedTools.length &&
sortedTools[toolIndex]!.parsedTimestamp <= currentTimestamp
Expand All @@ -93,7 +93,7 @@ function buildChartData(
}

return {
relativeTime: formatRelativeTime(usage.timestamp, sessionStartedAt),
relativeTime: formatRelativeTime(currentTimestamp, sessionStartedAtMs),
input: usage.inputTokens,
output: usage.outputTokens,
cost: usage.estimatedCostUsd,
Expand Down
9 changes: 9 additions & 0 deletions packages/web/src/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ describe('formatRelativeTime (baseTimestamp 오프셋 모드)', () => {
).toBe('+0m')
})

it('이미 파싱된 숫자 timestamp도 같은 오프셋을 반환한다', () => {
expect(
formatRelativeTime(
Date.parse('2026-06-01T00:03:30Z'),
Date.parse('2026-06-01T00:00:00Z'),
),
).toBe('+3m')
})

it('60분 미만은 "+Nm"', () => {
expect(
formatRelativeTime('2026-06-01T00:03:30Z', '2026-06-01T00:00:00Z'),
Expand Down
15 changes: 10 additions & 5 deletions packages/web/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,23 @@ export function formatDateTimeFull(s: string): string {
*/
export function formatRelativeTime(timestamp: string): string
export function formatRelativeTime(timestamp: string, baseTimestamp: string): string
export function formatRelativeTime(timestamp: string, baseTimestamp?: string): string {
export function formatRelativeTime(timestamp: number, baseTimestamp: number): string
export function formatRelativeTime(
timestamp: string | number,
baseTimestamp?: string | number
): string {
if (baseTimestamp === undefined) {
try {
return formatDistanceToNow(new Date(timestamp), { addSuffix: true, locale: ko })
} catch {
return timestamp
return String(timestamp)
}
}

const timestampDate = new Date(timestamp)
const baseDate = new Date(baseTimestamp)
const diffMs = timestampDate.getTime() - baseDate.getTime()
const timestampMs = typeof timestamp === 'number' ? timestamp : Date.parse(timestamp)
const baseTimestampMs =
typeof baseTimestamp === 'number' ? baseTimestamp : Date.parse(baseTimestamp)
const diffMs = timestampMs - baseTimestampMs
const totalMinutes = Math.floor(diffMs / 60000)

if (totalMinutes < 60) {
Expand Down
Loading