authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-12 09:26:56+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-12 11:56:56+05:30
logca6416ec2d1d194738bc78a81df4b82a75cb72e2
tree34e10d2ed9956c4088506ba654cb86ca2e38a39b
parent4a4c6dd2adc350a68ba18bb28b24b5a7e4660dab
signature Signed by PGP key B5690EEEBB952194

perf(client): reduce repeated sorting and date formatting (#11019)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

7 files changed, 226 insertions(+), 37 deletions(-)

apps/web/src/performance.bench.ts created+65
......@@ -0,0 +1,65 @@
1import { EventId, ProjectId, TurnId, type OrchestrationThreadActivity } from "@t3tools/contracts";
2import {
3 getLatestThreadForProject,
4 sortActiveThreadsByOrderKey,
5 sortPinnedThreadsByOrderKey,
6 sortThreads,
7} from "@t3tools/client-runtime/state/thread-sort";
8import { formatHourShort, formatRelativeHourShort } from "@t3tools/shared/usageFormat";
9import { bench, describe } from "vite-plus/test";
10
11import { deriveActivePlanState } from "./session-logic";
12
13const projectId = ProjectId.make("benchmark-project");
14const turnId = TurnId.make("benchmark-turn");
15const start = Date.parse("2026-08-11T00:00:00.000Z");
16const threads = Array.from({ length: 1_000 }, (_, index) => {
17 const timestamp = new Date(start + ((index * 997) % 1_000) * 60_000).toISOString();
18 return {
19 id: `thread-${index}`,
20 projectId,
21 archivedAt: null,
22 createdAt: timestamp,
23 updatedAt: timestamp,
24 latestUserMessageAt: timestamp,
25 unsettledAt: null,
26 };
27});
28const activities: OrchestrationThreadActivity[] = Array.from({ length: 500 }, (_, index) => ({
29 id: EventId.make(`activity-${index}`),
30 turnId,
31 sequence: index,
32 createdAt: new Date(start + index * 1_000).toISOString(),
33 kind: index % 100 === 0 ? "turn.plan.updated" : "tool.completed",
34 summary: "Benchmark activity",
35 tone: "info",
36 payload: index % 100 === 0 ? { plan: [{ step: "Run checks", status: "inProgress" }] } : {},
37}));
38const hours = Array.from({ length: 24 }, (_, index) =>
39 new Date(start + index * 3_600_000).toISOString(),
40);
41const referenceTime = "2026-08-12T00:00:00.000Z";
42
43describe("client performance", () => {
44 bench("sort 1000 threads by recent activity", () => {
45 sortThreads(threads, "updated_at");
46 });
47 bench("sort 1000 active threads", () => {
48 sortActiveThreadsByOrderKey(threads);
49 });
50 bench("sort 1000 keyless pinned threads", () => {
51 sortPinnedThreadsByOrderKey(threads);
52 });
53 bench("select latest project thread from 1000 threads", () => {
54 getLatestThreadForProject(threads, projectId, "updated_at");
55 });
56 bench("derive plan from 500 activities with 5 plan updates", () => {
57 deriveActivePlanState(activities, turnId);
58 });
59 bench("format 24 hourly usage labels and tooltips", () => {
60 hours.map((hour) => [
61 formatHourShort(hour, "America/New_York"),
62 formatRelativeHourShort(hour, referenceTime, "America/New_York"),
63 ]);
64 });
65});
apps/web/src/session-logic.test.ts+27
......@@ -62,6 +62,33 @@ function makeActivity(overrides: {
6262}
6363
6464describe("deriveActivePlanState", () => {
65 it("orders plan snapshots by sequence while ignoring unrelated activities", () => {
66 const activities = Object.freeze([
67 makeActivity({
68 id: "completed",
69 kind: "turn.plan.updated",
70 turnId: "turn-1",
71 sequence: 3,
72 createdAt: "2026-02-23T00:00:05.000Z",
73 payload: { plan: [{ step: "Check", status: "completed" }] },
74 }),
75 makeActivity({ sequence: 4, kind: "tool.completed" }),
76 makeActivity({
77 id: "started",
78 kind: "turn.plan.updated",
79 turnId: "turn-1",
80 sequence: 1,
81 payload: { plan: [{ step: "Check", status: "inProgress" }] },
82 }),
83 makeActivity({ sequence: 2, kind: "context-window.updated" }),
84 ]);
85 expect(deriveActivePlanState(activities, TurnId.make("turn-1"))?.steps).toEqual([
86 { step: "Check", status: "completed", durationMs: 5_000 },
87 ]);
88 expect(activities[0]?.id).toBe("completed");
89 expect(deriveActivePlanState([makeActivity({ kind: "tool.completed" })], undefined)).toBeNull();
90 });
91
6592 it("returns the latest plan update for the active turn", () => {
6693 const activities: OrchestrationThreadActivity[] = [
6794 makeActivity({
apps/web/src/session-logic.ts+3-2
......@@ -326,8 +326,9 @@ export function deriveActivePlanState(
326326 activities: ReadonlyArray<OrchestrationThreadActivity>,
327327 latestTurnId: TurnId | undefined,
328328): ActivePlanState | null {
329 const ordered = [...activities].toSorted(compareActivitiesByOrder);
330 const allPlanActivities = ordered.filter((activity) => activity.kind === "turn.plan.updated");
329 const allPlanActivities = activities
330 .filter((activity) => activity.kind === "turn.plan.updated")
331 .sort(compareActivitiesByOrder);
331332 // Prefer plan from the current turn; fall back to the most recent plan from any turn
332333 // so that TodoWrite tasks persist across follow-up messages.
333334 const latest = Option.firstSomeOf([
packages/client-runtime/src/state/threadSort.test.ts+45
......@@ -1,7 +1,9 @@
1import { ProjectId } from "@t3tools/contracts";
12import { describe, expect, it } from "vite-plus/test";
23
34import {
45 generateSpreadPinOrderKeys,
6 getLatestThreadForProject,
57 pinOrderKeyBetween,
68 planPinnedMove,
79 planPinnedReorder,
......@@ -58,6 +60,22 @@ describe("resolveSettledThreadTimestamp", () => {
5860});
5961
6062describe("sortThreads", () => {
63 it.each(["created_at", "updated_at"] as const)(
64 "preserves references, input order and descending id ties for %s",
65 (sortOrder) => {
66 const threads = Object.freeze([
67 makeThread({ id: "a" }),
68 makeThread({ id: "z" }),
69 makeThread({ id: "invalid-a", createdAt: "invalid", updatedAt: "invalid" }),
70 makeThread({ id: "invalid-z", createdAt: "invalid", updatedAt: "invalid" }),
71 ]);
72 const sorted = sortThreads(threads, sortOrder);
73 expect(sorted).toEqual([threads[1], threads[0], threads[3], threads[2]]);
74 expect(sorted[0]).toBe(threads[1]);
75 expect(threads[0]?.id).toBe("a");
76 },
77 );
78
6179 it("falls back to updatedAt and createdAt when latestUserMessageAt is invalid and there are no messages", () => {
6280 const sorted = sortThreads(
6381 [
......@@ -112,6 +130,33 @@ describe("sortThreads", () => {
112130 });
113131});
114132
133describe("getLatestThreadForProject", () => {
134 it.each(["created_at", "updated_at"] as const)(
135 "matches the first sorted eligible thread for %s",
136 (sortOrder) => {
137 const projectId = ProjectId.make("project");
138 const threads = [
139 { ...makeThread({ id: "a" }), projectId, archivedAt: null },
140 { ...makeThread({ id: "z" }), projectId, archivedAt: null },
141 { ...makeThread({ id: "zz" }), projectId, archivedAt: "2026-03-10T00:00:00Z" },
142 { ...makeThread({ id: "zzz" }), projectId: ProjectId.make("other"), archivedAt: null },
143 ];
144 expect(getLatestThreadForProject(threads, projectId, sortOrder)).toBe(threads[1]);
145 expect(getLatestThreadForProject([], projectId, sortOrder)).toBeNull();
146 expect(getLatestThreadForProject(threads, ProjectId.make("missing"), sortOrder)).toBeNull();
147 const invalid = threads.slice(0, 2).map((thread) => ({
148 ...thread,
149 createdAt: "invalid",
150 updatedAt: "invalid",
151 }));
152 expect(getLatestThreadForProject(invalid, projectId, sortOrder)).toBe(invalid[1]);
153 expect(
154 getLatestThreadForProject([threads[1]!, { ...threads[1]! }], projectId, sortOrder),
155 ).toBe(threads[1]);
156 },
157 );
158});
159
115160describe("planPinnedReorder with hidden rows", () => {
116161 it("keeps hidden slots available when inserting between visible neighbors", () => {
117162 const midpoint = pinOrderKeyBetween("f", "t")!;
packages/client-runtime/src/state/threadSort.ts+40-30
......@@ -1,7 +1,5 @@
11import type { OrchestrationThreadShell, ProjectId } from "@t3tools/contracts";
22import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
3import * as Arr from "effect/Array";
4import * as Order from "effect/Order";
53
64export interface ThreadSortInput {
75 readonly createdAt: string;
......@@ -121,19 +119,15 @@ export function sortThreads<T extends { readonly id: string } & ThreadSortInput>
121119 threads: readonly T[],
122120 sortOrder: SidebarThreadSortOrder,
123121): T[] {
124 return Arr.sort(
125 threads,
126 Order.mapInput(
127 Order.Struct({
128 timestamp: Order.flip(Order.Number),
129 id: Order.flip(Order.String),
130 }),
131 (thread: T) => ({
132 timestamp: getThreadSortTimestamp(thread, sortOrder),
133 id: thread.id,
134 }),
135 ),
136 );
122 if (threads.length < 2) return [...threads];
123 return threads
124 .map((thread) => ({ thread, timestamp: getThreadSortTimestamp(thread, sortOrder) }))
125 .sort(
126 (left, right) =>
127 right.timestamp - left.timestamp ||
128 (left.thread.id < right.thread.id ? 1 : left.thread.id > right.thread.id ? -1 : 0),
129 )
130 .map(({ thread }) => thread);
137131}
138132
139133export function getLatestThreadForProject<
......@@ -143,12 +137,21 @@ export function getLatestThreadForProject<
143137 readonly archivedAt: string | null;
144138 } & ThreadSortInput,
145139>(threads: readonly T[], projectId: ProjectId, sortOrder: SidebarThreadSortOrder): T | null {
146 return (
147 sortThreads(
148 threads.filter((thread) => thread.projectId === projectId && thread.archivedAt === null),
149 sortOrder,
150 )[0] ?? null
151 );
140 let latest: T | null = null;
141 let latestTimestamp = Number.NEGATIVE_INFINITY;
142 for (const thread of threads) {
143 if (thread.projectId !== projectId || thread.archivedAt !== null) continue;
144 const timestamp = getThreadSortTimestamp(thread, sortOrder);
145 if (
146 latest === null ||
147 timestamp > latestTimestamp ||
148 (timestamp === latestTimestamp && thread.id > latest.id)
149 ) {
150 latest = thread;
151 latestTimestamp = timestamp;
152 }
153 }
154 return latest;
152155}
153156
154157// ── Pinned reorder: fractional index keys ──────────────────────────────
......@@ -290,6 +293,7 @@ export function sortPinnedThreadsByOrderKey<
290293 readonly environmentId?: string | undefined;
291294 },
292295>(threads: readonly T[]): T[] {
296 if (threads.length < 2) return [...threads];
293297 const keyed: T[] = [];
294298 const keyless: T[] = [];
295299 for (const thread of threads) {
......@@ -303,14 +307,13 @@ export function sortPinnedThreadsByOrderKey<
303307 const rightKey = right.pinOrderKey!;
304308 return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : identityTiebreak(left, right);
305309 });
306 keyless.sort((left, right) => {
307 const leftMs = Date.parse(left.createdAt);
308 const rightMs = Date.parse(right.createdAt);
309 return (
310 (Number.isNaN(rightMs) ? 0 : rightMs) - (Number.isNaN(leftMs) ? 0 : leftMs) ||
311 identityTiebreak(left, right)
312 );
313 });
310 const timestamps = new Map(
311 keyless.map((thread) => [thread, toSortableTimestamp(thread.createdAt) ?? 0]),
312 );
313 keyless.sort(
314 (left, right) =>
315 timestamps.get(right)! - timestamps.get(left)! || identityTiebreak(left, right),
316 );
314317 return [...keyed, ...keyless];
315318}
316319
......@@ -325,6 +328,13 @@ export function sortActiveThreadsByOrderKey<
325328 readonly environmentId?: string | undefined;
326329 },
327330>(threads: readonly T[]): T[] {
331 if (threads.length < 2) return [...threads];
332 const timestamps = new Map<T, number>();
333 for (const thread of threads) {
334 if (thread.activeOrderKey == null) {
335 timestamps.set(thread, activeThreadAnchorTimestampMs(thread));
336 }
337 }
328338 return [...threads].sort((left, right) => {
329339 const leftKey = left.activeOrderKey;
330340 const rightKey = right.activeOrderKey;
......@@ -334,7 +344,7 @@ export function sortActiveThreadsByOrderKey<
334344 if (leftKey != null && rightKey != null) {
335345 order = leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
336346 } else {
337 order = activeThreadAnchorTimestampMs(right) - activeThreadAnchorTimestampMs(left);
347 order = timestamps.get(right)! - timestamps.get(left)!;
338348 }
339349 return (
340350 order ||
packages/shared/src/usageFormat.test.ts+24
......@@ -10,6 +10,30 @@ import {
1010} from "./usageFormat.ts";
1111
1212describe("hourly usage formatting", () => {
13 it("keeps requested zones separate when formatting repeated calls", () => {
14 const instant = "2026-08-11T12:37:00.000Z";
15 for (const zone of ["UTC", "America/New_York", "Asia/Kathmandu", "UTC"]) {
16 expect(formatHourShort(instant, zone)).toBe(
17 new Intl.DateTimeFormat("en-US", { timeZone: zone, hour: "numeric" }).format(
18 new Date(instant),
19 ),
20 );
21 }
22 expect(() => formatHourShort(instant, "Etc/Unknown")).toThrow(RangeError);
23 expect(formatHourShort("invalid", "UTC")).toBe("invalid");
24 });
25
26 it("uses the current system zone when no zone is supplied", () => {
27 try {
28 vi.stubEnv("TZ", "UTC");
29 expect(formatHourShort("2026-08-11T12:37:00.000Z")).toBe("12 PM");
30 vi.stubEnv("TZ", "America/New_York");
31 expect(formatHourShort("2026-08-11T12:37:00.000Z")).toBe("8 AM");
32 } finally {
33 vi.unstubAllEnvs();
34 }
35 });
36
1337 it("enumerates 24 fixed buckets across a rolling window", () => {
1438 const hours = enumerateHourStarts("2026-08-10T12:37:00.000Z", "2026-08-11T12:37:00.000Z");
1539
packages/shared/src/usageFormat.ts+22-5
......@@ -82,6 +82,23 @@ export function enumerateDays(sinceDay: string, untilDay: string): readonly stri
8282
8383const HOUR_MS = 60 * 60 * 1000;
8484
85const dateTimeFormatters = new Map<string, Intl.DateTimeFormat>();
86
87function dateTimeFormatter(
88 locale: string,
89 options: Intl.DateTimeFormatOptions,
90): Intl.DateTimeFormat {
91 if (options.timeZone === undefined) return new Intl.DateTimeFormat(locale, options);
92 const key = JSON.stringify([locale, options]);
93 let formatter = dateTimeFormatters.get(key);
94 if (formatter === undefined) {
95 formatter = new Intl.DateTimeFormat(locale, options);
96 if (dateTimeFormatters.size >= 16) dateTimeFormatters.clear();
97 dateTimeFormatters.set(key, formatter);
98 }
99 return formatter;
100}
101
85102/** Every fixed-duration bucket start in an hourly rolling window. */
86103export function enumerateHourStarts(sinceTime: string, untilTime: string): readonly string[] {
87104 const starts: string[] = [];
......@@ -105,11 +122,11 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
105122 const instant = new Date(hourStart);
106123 if (Number.isNaN(instant.getTime())) return hourStart;
107124 const options = timeZone === undefined ? {} : { timeZone };
108 const hourFormat = new Intl.DateTimeFormat("en-US", {
125 const hourFormat = dateTimeFormatter("en-US", {
109126 ...options,
110127 hour: "numeric",
111128 });
112 const wallHourFormat = new Intl.DateTimeFormat("en-CA", {
129 const wallHourFormat = dateTimeFormatter("en-CA", {
113130 ...options,
114131 year: "numeric",
115132 month: "2-digit",
......@@ -123,7 +140,7 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
123140 );
124141
125142 if (!isRepeatedHour) return hourFormat.format(instant);
126 return new Intl.DateTimeFormat("en-US", {
143 return dateTimeFormatter("en-US", {
127144 ...(timeZone === undefined ? {} : { timeZone }),
128145 hour: "numeric",
129146 timeZoneName: "short",
......@@ -134,7 +151,7 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
134151export function formatDateTimeShort(instant: string, timeZone?: string): string {
135152 const date = new Date(instant);
136153 if (Number.isNaN(date.getTime())) return instant;
137 return new Intl.DateTimeFormat("en-US", {
154 return dateTimeFormatter("en-US", {
138155 ...(timeZone === undefined ? {} : { timeZone }),
139156 month: "short",
140157 day: "numeric",
......@@ -154,7 +171,7 @@ export function formatRelativeHourShort(
154171 return formatDateTimeShort(hourStart, timeZone);
155172 }
156173
157 const dayFormat = new Intl.DateTimeFormat("en-CA", {
174 const dayFormat = dateTimeFormatter("en-CA", {
158175 ...(timeZone === undefined ? {} : { timeZone }),
159176 year: "numeric",
160177 month: "2-digit",