perf(ui): index and batch session updates
Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once. Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { isSessionPinned } from '@/stores/useSessionPinnedStore';
|
||||
import { normalizePath } from '@/lib/pathNormalization';
|
||||
import { countSyncPerformance } from './performance-diagnostics';
|
||||
|
||||
type SessionActivityPhase = 'active' | 'settled';
|
||||
export type SessionActivityPhase = 'active' | 'settled';
|
||||
|
||||
export type SessionOrderingMutation =
|
||||
| { type: 'observe'; sessionId: string; phase: SessionActivityPhase }
|
||||
| { type: 'remove'; sessionId: string };
|
||||
|
||||
type SessionOrderingState = {
|
||||
rankById: Map<string, number>;
|
||||
@@ -18,6 +22,7 @@ let lastRank = 0;
|
||||
export const useSessionOrderingStore = create<SessionOrderingState>(() => ({
|
||||
rankById: new Map(),
|
||||
}));
|
||||
useSessionOrderingStore.subscribe(() => countSyncPerformance('orderingPublications'));
|
||||
|
||||
const nextRank = (): number => {
|
||||
lastRank = Math.max(lastRank + 1, Date.now());
|
||||
@@ -42,12 +47,36 @@ export const observeSessionActivityEvent = (
|
||||
sessionId: string,
|
||||
phase: SessionActivityPhase,
|
||||
): void => {
|
||||
const previous = phaseById.get(sessionId);
|
||||
phaseById.set(sessionId, phase);
|
||||
applySessionOrderingMutations([{ type: 'observe', sessionId, phase }]);
|
||||
};
|
||||
|
||||
if (previous === phase) return;
|
||||
if (previous === undefined && phase === 'settled') return;
|
||||
promoteSessions([sessionId]);
|
||||
export const applySessionOrderingMutations = (
|
||||
mutations: readonly SessionOrderingMutation[],
|
||||
): void => {
|
||||
if (mutations.length === 0) return;
|
||||
const currentRanks = useSessionOrderingStore.getState().rankById;
|
||||
let rankById: Map<string, number> | null = null;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'remove') {
|
||||
phaseById.delete(mutation.sessionId);
|
||||
baselineRankById.delete(mutation.sessionId);
|
||||
if ((rankById ?? currentRanks).has(mutation.sessionId)) {
|
||||
rankById ??= new Map(currentRanks);
|
||||
rankById.delete(mutation.sessionId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const previous = phaseById.get(mutation.sessionId);
|
||||
phaseById.set(mutation.sessionId, mutation.phase);
|
||||
if (previous === mutation.phase) continue;
|
||||
if (previous === undefined && mutation.phase === 'settled') continue;
|
||||
rankById ??= new Map(currentRanks);
|
||||
rankById.set(mutation.sessionId, nextRank());
|
||||
}
|
||||
|
||||
if (rankById) useSessionOrderingStore.setState({ rankById });
|
||||
};
|
||||
|
||||
export const reconcileSessionActivitySnapshot = (
|
||||
@@ -71,14 +100,7 @@ export const reconcileSessionActivitySnapshot = (
|
||||
};
|
||||
|
||||
export const removeSessionOrdering = (sessionId: string): void => {
|
||||
phaseById.delete(sessionId);
|
||||
baselineRankById.delete(sessionId);
|
||||
useSessionOrderingStore.setState((state) => {
|
||||
if (!state.rankById.has(sessionId)) return state;
|
||||
const rankById = new Map(state.rankById);
|
||||
rankById.delete(sessionId);
|
||||
return { rankById };
|
||||
});
|
||||
applySessionOrderingMutations([{ type: 'remove', sessionId }]);
|
||||
};
|
||||
|
||||
export const resetSessionOrdering = (): void => {
|
||||
@@ -107,7 +129,7 @@ const sessionDirectory = (session: Session): string | null => {
|
||||
directory?: string | null;
|
||||
project?: { worktree?: string | null } | null;
|
||||
};
|
||||
return normalizePath(record.directory ?? null) ?? normalizePath(record.project?.worktree ?? null);
|
||||
return record.directory ?? record.project?.worktree ?? null;
|
||||
};
|
||||
|
||||
const baselineRank = (session: Session, pinned: boolean): number => {
|
||||
@@ -197,12 +219,39 @@ export const orderSessionsByLifecycleScopes = (
|
||||
sessions: Session[],
|
||||
pinnedSessionIds: Set<string>,
|
||||
rankById: ReadonlyMap<string, number>,
|
||||
hierarchy?: {
|
||||
rootIds: readonly string[];
|
||||
childrenByParentId: ReadonlyMap<string, readonly string[]>;
|
||||
},
|
||||
): Session[] => {
|
||||
countSyncPerformance('sidebarOrderBuilds');
|
||||
const sessionIds = new Set(sessions.map((session) => session.id));
|
||||
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
||||
const roots: Session[] = [];
|
||||
const childrenByParent = new Map<string, Session[]>();
|
||||
const indexedIds = new Set<string>();
|
||||
|
||||
if (hierarchy) {
|
||||
for (const sessionId of hierarchy.rootIds) {
|
||||
const session = sessionById.get(sessionId);
|
||||
if (!session) continue;
|
||||
indexedIds.add(sessionId);
|
||||
roots.push(session);
|
||||
}
|
||||
for (const [parentId, childIds] of hierarchy.childrenByParentId) {
|
||||
if (!sessionIds.has(parentId)) continue;
|
||||
const children = childIds.flatMap((sessionId) => {
|
||||
const session = sessionById.get(sessionId);
|
||||
if (!session) return [];
|
||||
indexedIds.add(sessionId);
|
||||
return [session];
|
||||
});
|
||||
if (children.length > 0) childrenByParent.set(parentId, children);
|
||||
}
|
||||
}
|
||||
|
||||
for (const session of sessions) {
|
||||
if (indexedIds.has(session.id)) continue;
|
||||
const parentId = parentIdOf(session);
|
||||
if (!parentId || !sessionIds.has(parentId)) {
|
||||
roots.push(session);
|
||||
@@ -217,9 +266,34 @@ export const orderSessionsByLifecycleScopes = (
|
||||
}
|
||||
}
|
||||
|
||||
const compare = (left: Session, right: Session) => (
|
||||
compareSessionsByLifecycleOrder(left, right, pinnedSessionIds, rankById)
|
||||
);
|
||||
const metadataById = new Map(sessions.map((session) => {
|
||||
const parentId = parentIdOf(session);
|
||||
const pinned = isSessionPinned(pinnedSessionIds, sessionDirectory(session), session.id);
|
||||
const fallback = baselineRank(session, pinned);
|
||||
return [session.id, {
|
||||
parentId,
|
||||
pinned,
|
||||
fallback,
|
||||
lifecycle: rankById.get(session.id) ?? fallback,
|
||||
created: baselineRank(session, true),
|
||||
}] as const;
|
||||
}));
|
||||
countSyncPerformance('sidebarOrderMetadataEntries', metadataById.size);
|
||||
const compare = (left: Session, right: Session): number => {
|
||||
const leftMetadata = metadataById.get(left.id);
|
||||
const rightMetadata = metadataById.get(right.id);
|
||||
if (!leftMetadata || !rightMetadata) return left.id.localeCompare(right.id);
|
||||
if (leftMetadata.pinned !== rightMetadata.pinned) return leftMetadata.pinned ? -1 : 1;
|
||||
if (leftMetadata.parentId === rightMetadata.parentId) {
|
||||
const rankDelta = rightMetadata.lifecycle - leftMetadata.lifecycle;
|
||||
if (rankDelta !== 0) return rankDelta;
|
||||
}
|
||||
const baselineDelta = rightMetadata.fallback - leftMetadata.fallback;
|
||||
if (baselineDelta !== 0) return baselineDelta;
|
||||
const createdDelta = rightMetadata.created - leftMetadata.created;
|
||||
if (createdDelta !== 0) return createdDelta;
|
||||
return left.id.localeCompare(right.id);
|
||||
};
|
||||
roots.sort(compare);
|
||||
for (const siblings of childrenByParent.values()) {
|
||||
siblings.sort(compare);
|
||||
|
||||
Reference in New Issue
Block a user