perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing cache, synchronization, and persistence correctness across runtimes, projects, directories, and worktrees. - prioritize selected and visible sessions during bootstrap and defer non-critical enrichment work - reduce redundant message loading, event processing, store publication, and hidden sidebar work - prevent stale session and message requests from overwriting newer authoritative state - preserve existing data when authoritative fetches fail instead of treating failures as successful empty responses - scope session materialization, messages, drafts, queues, todos, pins, permissions, folders, tabs, Git state, and pull request data by runtime and directory identity - harden runtime switching, reconnect, cleanup, mutation reconciliation, and persisted-state ordering - preserve live subagent Task linkage when metadata arrives after an older message request or while streaming parts are suspended - coalesce overlapping tail refreshes without losing newer refresh demand - improve cold-session loading by moving deferrable work out of the critical bootstrap path - isolate URL authentication, mobile credentials, native secrets, and other runtime-owned state across endpoint changes - bound long-lived caches and remove avoidable allocations from event and rendering hot paths - limit virtualization to archive collections where it improves rendering without disrupting active sidebar layout - stabilize session folders, pin ordering, expanded state, and persisted sidebar behavior - open skill files through the same secure editor and outside-workspace grant flow used by file navigation, including worktree sessions - expand regression coverage for stale completions, runtime collisions, reconnect behavior, persistence races, authoritative empty results, and subagent refresh ordering - document the updated synchronization, cache ownership, performance, and runtime-isolation invariants
This commit is contained in:
committed by
GitHub
parent
485efc7117
commit
85400459e9
@@ -10,120 +10,152 @@ const SESSION_PREFETCH_CONCURRENCY = 1;
|
||||
const SESSION_PREFETCH_PENDING_LIMIT = 6;
|
||||
|
||||
type Args = {
|
||||
enabled?: boolean;
|
||||
currentSessionId: string | null;
|
||||
sortedSessions: Session[];
|
||||
recentSessionIds?: string[];
|
||||
ensureSessionRenderable: (sessionId: string) => Promise<unknown>;
|
||||
recentSessions?: Session[];
|
||||
prefetchSession: (sessionId: string, directory: string) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export const useSessionPrefetch = ({ currentSessionId, sortedSessions, recentSessionIds = [], ensureSessionRenderable }: Args): void => {
|
||||
type PrefetchRequest = {
|
||||
sessionId: string;
|
||||
directory: string;
|
||||
generation: number;
|
||||
};
|
||||
|
||||
const sessionDirectory = (session: Session | null | undefined): string | null => {
|
||||
const directory = (session as (Session & { directory?: string | null }) | null | undefined)?.directory;
|
||||
return typeof directory === 'string' && directory.trim() ? directory : null;
|
||||
};
|
||||
|
||||
export const useSessionPrefetch = ({ enabled = true, currentSessionId, sortedSessions, recentSessions = [], prefetchSession }: Args): void => {
|
||||
const sessionPrefetchTimersRef = React.useRef<Map<string, number>>(new Map());
|
||||
const sessionPrefetchQueueRef = React.useRef<string[]>([]);
|
||||
const sessionPrefetchQueueRef = React.useRef<PrefetchRequest[]>([]);
|
||||
const sessionPrefetchInFlightRef = React.useRef<Set<string>>(new Set());
|
||||
const generationRef = React.useRef(0);
|
||||
const prefetchDisabled = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
const requestKey = React.useCallback((request: Pick<PrefetchRequest, 'directory' | 'sessionId'>) => (
|
||||
`${request.directory}\n${request.sessionId}`
|
||||
), []);
|
||||
|
||||
const clearPendingPrefetches = React.useCallback(() => {
|
||||
generationRef.current += 1;
|
||||
sessionPrefetchQueueRef.current = [];
|
||||
sessionPrefetchTimersRef.current.forEach((timer) => window.clearTimeout(timer));
|
||||
sessionPrefetchTimersRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const pumpSessionPrefetchQueue = React.useCallback(() => {
|
||||
if (prefetchDisabled || typeof window === 'undefined') {
|
||||
if (!enabled || prefetchDisabled || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
while (sessionPrefetchInFlightRef.current.size < SESSION_PREFETCH_CONCURRENCY && sessionPrefetchQueueRef.current.length > 0) {
|
||||
const nextSessionId = sessionPrefetchQueueRef.current.shift();
|
||||
if (!nextSessionId) {
|
||||
const request = sessionPrefetchQueueRef.current.shift();
|
||||
if (!request) {
|
||||
break;
|
||||
}
|
||||
if (request.generation !== generationRef.current) continue;
|
||||
|
||||
const state = useSessionUIStore.getState();
|
||||
if (state.currentSessionId === nextSessionId) {
|
||||
if (state.currentSessionId === request.sessionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the session is already renderable in the sync child store.
|
||||
if (getSyncSessionMaterializationStatus(nextSessionId).renderable) {
|
||||
if (getSyncSessionMaterializationStatus(request.sessionId, request.directory).renderable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sessionPrefetchInFlightRef.current.add(nextSessionId);
|
||||
void ensureSessionRenderable(nextSessionId)
|
||||
const key = requestKey(request);
|
||||
sessionPrefetchInFlightRef.current.add(key);
|
||||
void prefetchSession(request.sessionId, request.directory)
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
sessionPrefetchInFlightRef.current.delete(nextSessionId);
|
||||
sessionPrefetchInFlightRef.current.delete(key);
|
||||
pumpSessionPrefetchQueue();
|
||||
});
|
||||
}
|
||||
}, [ensureSessionRenderable, prefetchDisabled]);
|
||||
}, [enabled, prefetchDisabled, prefetchSession, requestKey]);
|
||||
|
||||
const scheduleSessionPrefetch = React.useCallback((sessionId: string | null | undefined) => {
|
||||
if (prefetchDisabled || !sessionId || sessionId === currentSessionId || typeof window === 'undefined') {
|
||||
const scheduleSessionPrefetch = React.useCallback((session: Session | null | undefined) => {
|
||||
const sessionId = session?.id;
|
||||
const directory = sessionDirectory(session);
|
||||
if (!enabled || prefetchDisabled || !sessionId || !directory || sessionId === currentSessionId || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const request = { sessionId, directory, generation: generationRef.current };
|
||||
const key = requestKey(request);
|
||||
|
||||
// Already renderable in sync
|
||||
if (getSyncSessionMaterializationStatus(sessionId).renderable) {
|
||||
if (getSyncSessionMaterializationStatus(sessionId, directory).renderable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionPrefetchInFlightRef.current.has(sessionId)) {
|
||||
if (sessionPrefetchInFlightRef.current.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionPrefetchQueueRef.current.includes(sessionId)) {
|
||||
if (sessionPrefetchQueueRef.current.some((candidate) => requestKey(candidate) === key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionPrefetchQueueRef.current.length >= SESSION_PREFETCH_PENDING_LIMIT) {
|
||||
sessionPrefetchQueueRef.current.shift();
|
||||
}
|
||||
|
||||
const existingTimer = sessionPrefetchTimersRef.current.get(sessionId);
|
||||
const existingTimer = sessionPrefetchTimersRef.current.get(key);
|
||||
if (existingTimer !== undefined) {
|
||||
window.clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
sessionPrefetchTimersRef.current.delete(sessionId);
|
||||
sessionPrefetchQueueRef.current.push(sessionId);
|
||||
sessionPrefetchTimersRef.current.delete(key);
|
||||
if (request.generation !== generationRef.current) return;
|
||||
const queue = sessionPrefetchQueueRef.current;
|
||||
if (queue.length >= SESSION_PREFETCH_PENDING_LIMIT) {
|
||||
queue.shift();
|
||||
}
|
||||
queue.push(request);
|
||||
pumpSessionPrefetchQueue();
|
||||
}, SESSION_PREFETCH_HOVER_DELAY_MS);
|
||||
sessionPrefetchTimersRef.current.set(sessionId, timer);
|
||||
}, [currentSessionId, prefetchDisabled, pumpSessionPrefetchQueue]);
|
||||
sessionPrefetchTimersRef.current.set(key, timer);
|
||||
}, [currentSessionId, enabled, prefetchDisabled, pumpSessionPrefetchQueue, requestKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
clearPendingPrefetches();
|
||||
}, [clearPendingPrefetches, currentSessionId, enabled, prefetchDisabled]);
|
||||
|
||||
// Wait for the active session to finish loading before prefetching neighbors.
|
||||
// On rapid session switches the timer resets, so only the final session triggers prefetch.
|
||||
React.useEffect(() => {
|
||||
if (prefetchDisabled || !currentSessionId || sortedSessions.length === 0) {
|
||||
if (!enabled || prefetchDisabled || !currentSessionId || sortedSessions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(() => {
|
||||
const currentIndex = sortedSessions.findIndex((session) => session.id === currentSessionId);
|
||||
if (currentIndex < 0) return;
|
||||
scheduleSessionPrefetch(sortedSessions[currentIndex - 1]?.id);
|
||||
scheduleSessionPrefetch(sortedSessions[currentIndex + 1]?.id);
|
||||
scheduleSessionPrefetch(sortedSessions[currentIndex - 1]);
|
||||
scheduleSessionPrefetch(sortedSessions[currentIndex + 1]);
|
||||
}, SESSION_PREFETCH_SETTLE_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [currentSessionId, prefetchDisabled, scheduleSessionPrefetch, sortedSessions]);
|
||||
}, [currentSessionId, enabled, prefetchDisabled, scheduleSessionPrefetch, sortedSessions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prefetchDisabled || !currentSessionId || recentSessionIds.length === 0) {
|
||||
if (!enabled || prefetchDisabled || !currentSessionId || recentSessions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(() => {
|
||||
const currentIndex = recentSessionIds.indexOf(currentSessionId);
|
||||
const currentIndex = recentSessions.findIndex((session) => session.id === currentSessionId);
|
||||
if (currentIndex < 0) return;
|
||||
scheduleSessionPrefetch(recentSessionIds[currentIndex - 1]);
|
||||
scheduleSessionPrefetch(recentSessionIds[currentIndex + 1]);
|
||||
scheduleSessionPrefetch(recentSessions[currentIndex - 1]);
|
||||
scheduleSessionPrefetch(recentSessions[currentIndex + 1]);
|
||||
}, SESSION_PREFETCH_SETTLE_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [currentSessionId, prefetchDisabled, recentSessionIds, scheduleSessionPrefetch]);
|
||||
}, [currentSessionId, enabled, prefetchDisabled, recentSessions, scheduleSessionPrefetch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const prefetchTimers = sessionPrefetchTimersRef.current;
|
||||
return () => {
|
||||
prefetchTimers.forEach((timer) => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
prefetchTimers.clear();
|
||||
sessionPrefetchQueueRef.current = [];
|
||||
};
|
||||
}, []);
|
||||
React.useEffect(() => clearPendingPrefetches, [clearPendingPrefetches]);
|
||||
};
|
||||
|
||||
export const SessionPrefetchEffect: React.FC<Omit<Args, 'currentSessionId'>> = (args) => {
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
useSessionPrefetch({ ...args, currentSessionId });
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user