perf: reduce startup request fanout

Avoid repeated sidebar session and worktree refreshes
Limit concurrent GitHub PR status checks
Reuse cached worktree root resolution
This commit is contained in:
Bohdan Triapitsyn
2026-05-25 11:58:59 +03:00
parent 68f00e4b7b
commit f8beffce32
7 changed files with 64 additions and 98 deletions
@@ -360,6 +360,22 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
syncSessionsSnapshotRef.current = liveSessions; syncSessionsSnapshotRef.current = liveSessions;
}, [syncSessionStructureSignature, liveSessions]); }, [syncSessionStructureSignature, liveSessions]);
const projectWorktreeDiscoveryKey = React.useMemo(
() => projects
.map((project) => `${project.id}:${normalizePath(project.path) ?? ''}`)
.join('|'),
[projects],
);
const initialGlobalSessionsRefreshStartedRef = React.useRef(false);
React.useEffect(() => {
if (initialGlobalSessionsRefreshStartedRef.current) {
return;
}
initialGlobalSessionsRefreshStartedRef.current = true;
void refreshGlobalSessions(syncSessionsSnapshotRef.current);
}, []);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -397,13 +413,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}); });
}; };
void refreshGlobalSessions(syncSessionsSnapshotRef.current);
void discoverWorktrees(); void discoverWorktrees();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [currentDirectory, syncSessionStructureSignature, projects]); }, [projectWorktreeDiscoveryKey]);
React.useEffect(() => { React.useEffect(() => {
let refreshTimeout: ReturnType<typeof setTimeout> | null = null; let refreshTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -1113,7 +1128,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}); });
void refreshPrStatusTargets([...uniqueTargets.values()], { void refreshPrStatusTargets([...uniqueTargets.values()], {
force: true,
silent: true, silent: true,
markInitialResolved: true, markInitialResolved: true,
}); });
@@ -1,6 +1,5 @@
import { substituteCommandVariables } from '@/lib/openchamberConfig'; import { substituteCommandVariables } from '@/lib/openchamberConfig';
import type { WorktreeMetadata } from '@/types/worktree'; import type { WorktreeMetadata } from '@/types/worktree';
import { execCommand } from '@/lib/execCommands';
import { import {
deleteRemoteBranch, deleteRemoteBranch,
git, git,
@@ -9,7 +8,7 @@ import {
clearWorktreeBootstrapState, clearWorktreeBootstrapState,
markWorktreeBootstrapPending, markWorktreeBootstrapPending,
} from '@/lib/worktrees/worktreeBootstrap'; } from '@/lib/worktrees/worktreeBootstrap';
import { invalidateResolvedProjectRootCache } from '@/lib/worktrees/worktreeStatus'; import { invalidateResolvedProjectRootCache, resolveProjectRoot } from '@/lib/worktrees/worktreeStatus';
import type { import type {
CreateGitWorktreePayload, CreateGitWorktreePayload,
GitWorktreeValidationResult, GitWorktreeValidationResult,
@@ -55,66 +54,6 @@ const normalizePath = (value: string): string => {
return replaced.length > 1 ? replaced.replace(/\/+$/, '') : replaced; return replaced.length > 1 ? replaced.replace(/\/+$/, '') : replaced;
}; };
const toAbsolutePath = (baseDir: string, maybeRelativePath: string): string => {
const normalizedBase = normalizePath(baseDir);
const normalizedInput = normalizePath(maybeRelativePath);
if (!normalizedInput) return normalizedBase;
if (normalizedInput.startsWith('/')) return normalizedInput;
const stack = normalizedBase.split('/').filter(Boolean);
const parts = normalizedInput.split('/').filter(Boolean);
for (const part of parts) {
if (part === '.') continue;
if (part === '..') {
stack.pop();
continue;
}
stack.push(part);
}
return `/${stack.join('/')}`;
};
const derivePrimaryWorktreeRootFromGitDir = (gitDir: string): string | null => {
const normalized = normalizePath(gitDir);
if (!normalized) return null;
if (normalized.endsWith('/.git')) {
return normalized.slice(0, -'/.git'.length) || null;
}
const worktreesMarker = '/.git/worktrees/';
const markerIndex = normalized.indexOf(worktreesMarker);
if (markerIndex > 0) {
return normalized.slice(0, markerIndex) || null;
}
return null;
};
const resolvePrimaryWorktreeDirectory = async (directory: string): Promise<string> => {
const normalizedDirectory = normalizePath(directory);
const absoluteGitDirResult = await execCommand('git rev-parse --absolute-git-dir', normalizedDirectory);
const absoluteGitDir = normalizePath((absoluteGitDirResult.stdout || '').trim());
if (absoluteGitDirResult.success && absoluteGitDir) {
const rootFromAbsoluteGitDir = derivePrimaryWorktreeRootFromGitDir(absoluteGitDir);
if (rootFromAbsoluteGitDir) {
return rootFromAbsoluteGitDir;
}
}
const commonDirResult = await execCommand('git rev-parse --git-common-dir', normalizedDirectory);
const rawCommonDir = normalizePath((commonDirResult.stdout || '').trim());
if (!commonDirResult.success || !rawCommonDir) {
return normalizedDirectory;
}
const commonDir = toAbsolutePath(normalizedDirectory, rawCommonDir);
const rootFromCommonDir = derivePrimaryWorktreeRootFromGitDir(commonDir);
if (rootFromCommonDir) {
return rootFromCommonDir;
}
return normalizedDirectory;
};
const slugifyWorktreeName = (value: string): string => { const slugifyWorktreeName = (value: string): string => {
return value return value
.trim() .trim()
@@ -227,7 +166,7 @@ export async function listProjectWorktrees(project: ProjectRef): Promise<Worktre
if (inflight) return inflight; if (inflight) return inflight;
const promise = (async (): Promise<WorktreeMetadata[]> => { const promise = (async (): Promise<WorktreeMetadata[]> => {
const metadataProjectDirectory = await resolvePrimaryWorktreeDirectory(projectDirectory).catch(() => projectDirectory); const metadataProjectDirectory = await resolveProjectRoot(projectDirectory).catch(() => projectDirectory);
const normalizedProjectDirectory = normalizePath(projectDirectory); const normalizedProjectDirectory = normalizePath(projectDirectory);
const worktrees = await git.worktree.list(projectDirectory).catch(() => []); const worktrees = await git.worktree.list(projectDirectory).catch(() => []);
@@ -289,7 +228,7 @@ export type CreateWorktreeArgs = {
export async function createWorktree(project: ProjectRef, args: CreateWorktreeArgs): Promise<WorktreeMetadata> { export async function createWorktree(project: ProjectRef, args: CreateWorktreeArgs): Promise<WorktreeMetadata> {
const projectDirectory = normalizePath(project.path); const projectDirectory = normalizePath(project.path);
const metadataProjectDirectory = await resolvePrimaryWorktreeDirectory(projectDirectory).catch(() => projectDirectory); const metadataProjectDirectory = await resolveProjectRoot(projectDirectory).catch(() => projectDirectory);
const payload = toCreatePayload(args, projectDirectory); const payload = toCreatePayload(args, projectDirectory);
const created = await git.worktree.create(projectDirectory, payload); const created = await git.worktree.create(projectDirectory, payload);
@@ -152,7 +152,7 @@ const computeProjectRoot = async (directory: string): Promise<string> => {
return directory; return directory;
}; };
const resolveProjectRoot = async (directory: string): Promise<string> => { export const resolveProjectRoot = async (directory: string): Promise<string> => {
const cached = resolvedRootCache.get(directory); const cached = resolvedRootCache.get(directory);
if (cached && Date.now() - cached.resolvedAt < RESOLVED_ROOT_TTL_MS) { if (cached && Date.now() - cached.resolvedAt < RESOLVED_ROOT_TTL_MS) {
// Refresh recency without extending TTL. // Refresh recency without extending TTL.
@@ -1,6 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware'; import { createJSONStorage, persist } from 'zustand/middleware';
import type { GitHubPullRequestStatus, RuntimeAPIs } from '@/lib/api/types'; import type { GitHubPullRequestStatus, RuntimeAPIs } from '@/lib/api/types';
import { mapWithConcurrency } from '@/lib/concurrency';
import { getSafeStorage } from './utils/safeStorage'; import { getSafeStorage } from './utils/safeStorage';
const PR_REVALIDATE_TTL_MS = 90_000; const PR_REVALIDATE_TTL_MS = 90_000;
@@ -10,6 +11,7 @@ const PR_BOOTSTRAP_RETRY_DELAYS_MS = [2_000, 5_000] as const;
const PR_OPEN_BUSY_INTERVAL_MS = 60_000; const PR_OPEN_BUSY_INTERVAL_MS = 60_000;
const PR_OPEN_DEFAULT_INTERVAL_MS = 2 * 60_000; const PR_OPEN_DEFAULT_INTERVAL_MS = 2 * 60_000;
const PR_OPEN_STABLE_INTERVAL_MS = 5 * 60_000; const PR_OPEN_STABLE_INTERVAL_MS = 5 * 60_000;
const PR_STATUS_REFRESH_CONCURRENCY = 4;
const PR_PERSIST_TTL_MS = 12 * 60 * 60_000; const PR_PERSIST_TTL_MS = 12 * 60 * 60_000;
const PR_STATUS_STORAGE_KEY = 'openchamber.github-pr-status'; const PR_STATUS_STORAGE_KEY = 'openchamber.github-pr-status';
@@ -581,7 +583,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
.filter((key): key is string => Boolean(key)), .filter((key): key is string => Boolean(key)),
)); ));
await Promise.all(keys.map((key) => get().refresh(key, options))); await mapWithConcurrency(keys, PR_STATUS_REFRESH_CONCURRENCY, (key) => get().refresh(key, options));
}, },
updateStatus: (key, updater) => { updateStatus: (key, updater) => {
@@ -23,7 +23,7 @@ type GlobalSessionsState = {
archiveSessions: (ids: Iterable<string>, archivedAt?: number) => void; archiveSessions: (ids: Iterable<string>, archivedAt?: number) => void;
}; };
const PAGE_SIZE = 200; const PAGE_SIZE = 500;
let inflightLoad: Promise<LoadResult> | null = null; let inflightLoad: Promise<LoadResult> | null = null;
+3
View File
@@ -1569,6 +1569,9 @@ export function SyncProvider(props: {
hasEverConnected: true, hasEverConnected: true,
connectionPhase: "connected", connectionPhase: "connected",
}) })
if (isRecentBoot()) {
return
}
for (const dir of childStores.children.keys()) { for (const dir of childStores.children.keys()) {
triggerDirectoryResync(dir) triggerDirectoryResync(dir)
} }
+36 -28
View File
@@ -35,6 +35,10 @@ const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
// session recency, not whichever component happened to call sync first. // session recency, not whichever component happened to call sync first.
const seenByDirectory = new Map<string, Set<string>>() const seenByDirectory = new Map<string, Set<string>>()
// Shared across useSync() hook instances. Chat, model controls, and sidebar can
// all request the same session during startup; coalesce them into one HTTP load.
const syncSessionInflightByKey = new Map<string, Promise<void>>()
type SyncMeta = { type SyncMeta = {
limit: number limit: number
cursor: string | undefined cursor: string | undefined
@@ -99,7 +103,6 @@ export function useSync() {
const childStores = useChildStoreManager() const childStores = useChildStoreManager()
// Refs for mutable tracking (no re-renders) // Refs for mutable tracking (no re-renders)
const inflight = useRef(new Map<string, Promise<void>>())
const optimistic = useRef(new Map<string, Map<string, OptimisticItem>>()) const optimistic = useRef(new Map<string, Map<string, OptimisticItem>>())
const meta = useRef(new Map<string, SyncMeta>()) const meta = useRef(new Map<string, SyncMeta>())
@@ -350,7 +353,7 @@ export function useSync() {
const key = keyFor(sessionID) const key = keyFor(sessionID)
// Dedup inflight requests // Dedup inflight requests
const existing = inflight.current.get(key) const existing = syncSessionInflightByKey.get(key)
if (existing) return existing if (existing) return existing
const current = store.getState() const current = store.getState()
@@ -386,35 +389,40 @@ export function useSync() {
})) return })) return
} }
const shouldFetchSession = !hasSession || force
const shouldLoadMessages = !cachedReady || force
const promise = (async () => { const promise = (async () => {
// Fetch session info if needed await Promise.all([
if (!hasSession || force) { shouldFetchSession
try { ? (async () => {
const result = await retry(() => sdk.session.get({ sessionID, directory })) try {
if (result.data) { const result = await retry(() => sdk.session.get({ sessionID, directory }))
const s = store.getState() if (result.data) {
const sessions = [...s.session] const s = store.getState()
const idx = Binary.search(sessions, sessionID, (s) => s.id) const sessions = [...s.session]
if (idx.found) { const idx = Binary.search(sessions, sessionID, (s) => s.id)
sessions[idx.index] = result.data if (idx.found) {
} else { sessions[idx.index] = result.data
sessions.splice(idx.index, 0, result.data) } else {
} sessions.splice(idx.index, 0, result.data)
store.setState({ session: sessions }) }
} store.setState({ session: sessions })
} catch (e) { }
console.error("[sync] failed to fetch session", sessionID, e) } catch (e) {
} console.error("[sync] failed to fetch session", sessionID, e)
} }
})()
// Load messages if needed : Promise.resolve(),
if (!cachedReady || force) { shouldLoadMessages ? loadMessages(sessionID) : Promise.resolve(),
await loadMessages(sessionID) ])
}
})() })()
inflight.current.set(key, promise) syncSessionInflightByKey.set(key, promise)
promise.finally(() => inflight.current.delete(key)) promise.finally(() => {
if (syncSessionInflightByKey.get(key) === promise) {
syncSessionInflightByKey.delete(key)
}
})
return promise return promise
}, },
[store, sdk, keyFor, touch, getMetaFor, setMetaFor, loadMessages, directory], [store, sdk, keyFor, touch, getMetaFor, setMetaFor, loadMessages, directory],