perf(renderer): cut hidden polling and repeated sync scans (#1813)

* perf(renderer): cut hidden polling and repeated sync scans

* fix(renderer): keep mobile file lookup worktree scoped
This commit is contained in:
Neil 2026-05-14 00:30:48 -07:00 committed by GitHub
parent dd6f0e63bb
commit de04b7497f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 173 additions and 49 deletions

View File

@ -10,8 +10,8 @@
* bucket drops to <25% remaining (warn) or <10% (crit). At healthy levels
* the budget is not actionable information and surfacing it just trains
* users to ignore the pill (or worry needlessly about ambiguous numbers
* like "30/30"). The probe still runs in the background so we can show
* the pill the moment something becomes actionable.
* like "30/30"). The probe still refreshes while the page is active so we
* can show the pill the moment something becomes actionable.
*
* This is an indicator, not a throttle we deliberately don't block the
* user from making requests when counts are low. Blocking would hurt the
@ -130,11 +130,23 @@ export default function GitHubRateLimitPill(): React.JSX.Element | null {
}, [])
useEffect(() => {
const fetchIfVisible = (): void => {
if (document.visibilityState === 'visible' && document.hasFocus()) {
void fetchSnapshot(false)
}
}
void fetchSnapshot(false)
const handle = window.setInterval(() => {
void fetchSnapshot(false)
}, REFRESH_INTERVAL_MS)
return () => window.clearInterval(handle)
// Why: rate-limit probes are only useful while the TaskPage is visible.
// Skipping hidden-window ticks avoids spending gh/API work just to keep an
// invisible pill current; focus refreshes restore freshness before use.
const handle = window.setInterval(fetchIfVisible, REFRESH_INTERVAL_MS)
window.addEventListener('focus', fetchIfVisible)
document.addEventListener('visibilitychange', fetchIfVisible)
return () => {
window.clearInterval(handle)
window.removeEventListener('focus', fetchIfVisible)
document.removeEventListener('visibilitychange', fetchIfVisible)
}
}, [fetchSnapshot])
// Why: silently render nothing on error or before first load. The pill is

View File

@ -1035,11 +1035,20 @@ function SourceControlInner(): React.JSX.Element {
}
void refreshBranchCompareRef.current()
const intervalId = window.setInterval(
() => void refreshBranchCompareRef.current(),
BRANCH_REFRESH_INTERVAL_MS
)
return () => window.clearInterval(intervalId)
const refreshIfFocused = (): void => {
if (document.hasFocus()) {
void refreshBranchCompareRef.current()
}
}
// Why: branch compare shells out to git every tick. The panel only needs
// background freshness while Orca is focused; on focus we refresh
// immediately so hidden-window time does not burn subprocess work.
const intervalId = window.setInterval(refreshIfFocused, BRANCH_REFRESH_INTERVAL_MS)
window.addEventListener('focus', refreshIfFocused)
return () => {
window.clearInterval(intervalId)
window.removeEventListener('focus', refreshIfFocused)
}
}, [activeWorktreeId, effectiveBaseRef, isBranchVisible, isFolder, worktreePath])
useEffect(() => {

View File

@ -714,9 +714,23 @@ export function ResourceUsageStatusSegment({
}, [open, fetchSnapshot, refreshSessions])
useEffect(() => {
const interval = setInterval(() => void refreshSessions(), SESSIONS_POLL_MS)
const refreshIfVisible = (): void => {
if (document.visibilityState === 'visible' && document.hasFocus()) {
void refreshSessions()
}
}
void refreshSessions()
return () => clearInterval(interval)
// Why: the closed-popover badge is informational. Polling daemon sessions
// while the whole window is hidden keeps IPC and daemon list calls hot for
// no visible UI; focus/visibility refreshes catch the badge up immediately.
const interval = setInterval(refreshIfVisible, SESSIONS_POLL_MS)
window.addEventListener('focus', refreshIfVisible)
document.addEventListener('visibilitychange', refreshIfVisible)
return () => {
clearInterval(interval)
window.removeEventListener('focus', refreshIfVisible)
document.removeEventListener('visibilitychange', refreshIfVisible)
}
}, [refreshSessions])
const repoDisplayNameById = useMemo(() => {

View File

@ -194,16 +194,24 @@ export function buildWorkspaceSessionPayload(
.filter(([, state]) => state.status === 'connected')
.map(([targetId]) => targetId)
const worktreeById = new Map(
Object.values(snapshot.worktreesByRepo)
.flat()
.map((worktree) => [worktree.id, worktree])
)
const repoById = new Map(snapshot.repos.map((repo) => [repo.id, repo]))
// Why: the renderer already has tab.ptyId for every terminal tab and knows
// which worktrees are SSH-backed via repo.connectionId. Deriving the map
// here avoids a sync IPC round-trip during beforeunload, which is fragile
// (can be dropped by Chromium under shutdown time pressure).
// Why: this builder runs from the session-write debounce and beforeunload.
// Pre-index repo/worktree identity once so large workspaces don't rescan all
// repos/worktrees for every terminal tab while the renderer is trying to quit.
const remoteSessionIdsByTabId: Record<string, string> = {}
for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
const worktree = Object.values(snapshot.worktreesByRepo)
.flat()
.find((w) => w.id === worktreeId)
const repo = worktree ? snapshot.repos.find((r) => r.id === worktree.repoId) : null
const worktree = worktreeById.get(worktreeId)
const repo = worktree ? repoById.get(worktree.repoId) : null
if (!repo?.connectionId) {
continue
}

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
buildMobileSessionTabSnapshots,
getRuntimeMobileSessionSyncKey,
runtimeMobileSessionSyncKeysEqual
} from './sync-runtime-graph'
@ -229,3 +230,59 @@ describe('getRuntimeMobileSessionSyncKey', () => {
expect(runtimeMobileSessionSyncKeysEqual(before, after)).toBe(false)
})
})
describe('buildMobileSessionTabSnapshots', () => {
it('keeps duplicate file ids scoped to their worktree', () => {
const sharedRemotePath = '/home/dev/project/README.md'
const previewId = `markdown-preview::${sharedRemotePath}`
const state = makeState({
browserTabsByWorktree: {},
tabBarOrderByWorktree: {
'wt-1': [sharedRemotePath, previewId],
'wt-2': [sharedRemotePath]
},
openFiles: [
{
id: sharedRemotePath,
filePath: sharedRemotePath,
relativePath: 'docs/wt-one.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit',
isDirty: true
},
{
id: sharedRemotePath,
filePath: sharedRemotePath,
relativePath: 'docs/wt-two.md',
worktreeId: 'wt-2',
language: 'markdown',
mode: 'edit',
isDirty: false
},
{
id: previewId,
filePath: sharedRemotePath,
relativePath: 'docs/wt-one.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'markdown-preview',
markdownPreviewSourceFileId: sharedRemotePath,
isDirty: false
}
]
})
const snapshotsByWorktree = new Map(
buildMobileSessionTabSnapshots(state).map((snapshot) => [snapshot.worktree, snapshot])
)
expect(snapshotsByWorktree.get('wt-1')?.tabs).toMatchObject([
{ type: 'markdown', title: 'wt-one.md', sourceRelativePath: 'docs/wt-one.md' },
{ type: 'markdown', title: 'wt-one.md', sourceRelativePath: 'docs/wt-one.md' }
])
expect(snapshotsByWorktree.get('wt-2')?.tabs).toMatchObject([
{ type: 'markdown', title: 'wt-two.md', sourceRelativePath: 'docs/wt-two.md' }
])
})
})

View File

@ -24,6 +24,8 @@ type RegisteredTerminalTab = {
getPtyIdForPane: (paneId: number) => string | null
}
type OpenFileByWorktreeAndId = Map<string, Map<string, AppState['openFiles'][number]>>
const registeredTabs = new Map<string, RegisteredTerminalTab>()
// Why: track when each tab was registered so we can suppress the "no live
// transport" warning during the initial PTY connection window. The warning
@ -193,6 +195,14 @@ async function syncRuntimeGraph(): Promise<void> {
// Injecting the getter from App keeps the runtime graph path out of the
// store construction cycle and avoids test-time partial initialization.
const state = getStoreState()
// Why: sync can run after high-churn terminal/title mutations. Build lookup
// maps once per sync instead of flattening every worktree's tabs for each
// registered terminal.
const terminalTabById = new Map(
Object.values(state.tabsByWorktree)
.flat()
.map((tab) => [tab.id, tab])
)
const graph: RuntimeSyncWindowGraph = {
tabs: [],
leaves: [],
@ -200,9 +210,7 @@ async function syncRuntimeGraph(): Promise<void> {
}
for (const [tabId, registeredTab] of registeredTabs) {
const tab = Object.values(state.tabsByWorktree)
.flat()
.find((candidate) => candidate.id === tabId)
const tab = terminalTabById.get(tabId)
if (!tab) {
continue
}
@ -256,7 +264,13 @@ async function syncRuntimeGraph(): Promise<void> {
}
}
function buildMobileSessionTabSnapshots(state: AppState): RuntimeMobileSessionTabsSnapshot[] {
export function buildMobileSessionTabSnapshots(
state: AppState
): RuntimeMobileSessionTabsSnapshot[] {
// Why: mobile publication walks the tab order for every worktree. A single
// worktree-scoped file map keeps large editor sessions linear without
// collapsing SSH worktrees that expose the same absolute remote path.
const openFileByWorktreeAndId = indexOpenFilesByWorktreeAndId(state.openFiles)
const worktreeIds = new Set<string>([
...Object.keys(state.tabsByWorktree),
...Object.keys(state.groupsByWorktree),
@ -268,24 +282,28 @@ function buildMobileSessionTabSnapshots(state: AppState): RuntimeMobileSessionTa
for (const worktreeId of worktreeIds) {
const activeGroupId = state.activeGroupIdByWorktree[worktreeId] ?? null
const order = getActiveTabNavOrder(state, worktreeId)
const terminalTabByIdForWorktree = new Map(
(state.tabsByWorktree[worktreeId] ?? []).map((tab) => [tab.id, tab])
)
const tabs: RuntimeMobileSessionSnapshotTab[] = []
for (const item of order) {
if (item.type === 'terminal') {
const terminal = (state.tabsByWorktree[worktreeId] ?? []).find((tab) => tab.id === item.id)
const terminal = terminalTabByIdForWorktree.get(item.id)
if (!terminal) {
continue
}
tabs.push(...buildMobileTerminalSurfaceTabs(state, terminal.id, worktreeId, item.tabId))
tabs.push(...buildMobileTerminalSurfaceTabs(state, terminal, worktreeId, item.tabId))
} else if (item.type === 'editor') {
const file = state.openFiles.find(
(candidate) => candidate.id === item.id && candidate.worktreeId === worktreeId
)
const markdown = file ? buildMobileMarkdownTab(state, file.id, item.tabId) : null
const file = openFileByWorktreeAndId.get(worktreeId)?.get(item.id)
if (!file) {
continue
}
const markdown = buildMobileMarkdownTab(state, openFileByWorktreeAndId, file, item.tabId)
if (markdown) {
tabs.push(markdown)
} else if (file) {
tabs.push(buildMobileFileTab(state, file.id, item.tabId))
} else {
tabs.push(buildMobileFileTab(state, file, item.tabId))
}
}
}
@ -305,6 +323,21 @@ function buildMobileSessionTabSnapshots(state: AppState): RuntimeMobileSessionTa
return snapshots
}
function indexOpenFilesByWorktreeAndId(openFiles: AppState['openFiles']): OpenFileByWorktreeAndId {
const byWorktreeAndId: OpenFileByWorktreeAndId = new Map()
for (const file of openFiles) {
let filesById = byWorktreeAndId.get(file.worktreeId)
if (!filesById) {
filesById = new Map()
byWorktreeAndId.set(file.worktreeId, filesById)
}
if (!filesById.has(file.id)) {
filesById.set(file.id, file)
}
}
return byWorktreeAndId
}
function mobileTerminalSurfaceId(parentTabId: string, leafId: string): string {
return `${parentTabId}::${leafId}`
}
@ -331,15 +364,10 @@ function getRuntimeLeafIdsForTerminal(tabId: string, state: AppState): string[]
function buildMobileTerminalSurfaceTabs(
state: AppState,
terminalTabId: string,
terminal: NonNullable<AppState['tabsByWorktree'][string]>[number],
worktreeId: string,
unifiedTabId?: string
): RuntimeMobileSessionSnapshotTab[] {
const terminal = (state.tabsByWorktree[worktreeId] ?? []).find((tab) => tab.id === terminalTabId)
if (!terminal) {
return []
}
const isDesktopTabActive = unifiedTabId
? state.groupsByWorktree[worktreeId]?.some(
(group) =>
@ -348,20 +376,20 @@ function buildMobileTerminalSurfaceTabs(
) === true
: state.activeTabId === terminal.id
const liveActiveLeafId =
registeredTabs.get(terminalTabId)?.getManager()?.getActivePane()?.id ?? null
registeredTabs.get(terminal.id)?.getManager()?.getActivePane()?.id ?? null
const activeLeafId =
liveActiveLeafId !== null
? paneLeafId(liveActiveLeafId)
: (state.terminalLayoutsByTabId[terminalTabId]?.activeLeafId ?? paneLeafId(1))
const paneTitles = state.runtimePaneTitlesByTabId[terminalTabId] ?? {}
return getRuntimeLeafIdsForTerminal(terminalTabId, state).map((leafId) => {
: (state.terminalLayoutsByTabId[terminal.id]?.activeLeafId ?? paneLeafId(1))
const paneTitles = state.runtimePaneTitlesByTabId[terminal.id] ?? {}
return getRuntimeLeafIdsForTerminal(terminal.id, state).map((leafId) => {
const paneId = /^pane:(\d+)$/.exec(leafId)?.[1]
const paneTitle = paneId ? paneTitles[Number(paneId)] : undefined
return {
type: 'terminal' as const,
id: mobileTerminalSurfaceId(terminalTabId, leafId),
id: mobileTerminalSurfaceId(terminal.id, leafId),
title: paneTitle ?? terminal.customTitle ?? terminal.title ?? 'Terminal',
parentTabId: terminalTabId,
parentTabId: terminal.id,
leafId,
isActive: isDesktopTabActive && leafId === activeLeafId
}
@ -370,13 +398,10 @@ function buildMobileTerminalSurfaceTabs(
function buildMobileMarkdownTab(
state: AppState,
fileId: string,
openFileByWorktreeAndId: OpenFileByWorktreeAndId,
file: AppState['openFiles'][number],
unifiedTabId?: string
): RuntimeMobileSessionMarkdownTab | null {
const file = state.openFiles.find((candidate) => candidate.id === fileId)
if (!file) {
return null
}
if (file.mode !== 'edit' && file.mode !== 'markdown-preview') {
return null
}
@ -386,7 +411,7 @@ function buildMobileMarkdownTab(
const sourceFile =
file.mode === 'markdown-preview' && file.markdownPreviewSourceFileId
? (state.openFiles.find((candidate) => candidate.id === file.markdownPreviewSourceFileId) ??
? (openFileByWorktreeAndId.get(file.worktreeId)?.get(file.markdownPreviewSourceFileId) ??
file)
: file
const draftContent = state.editorDrafts[sourceFile.id]
@ -416,10 +441,9 @@ function buildMobileMarkdownTab(
function buildMobileFileTab(
state: AppState,
fileId: string,
file: AppState['openFiles'][number],
unifiedTabId?: string
): RuntimeMobileSessionFileTab {
const file = state.openFiles.find((candidate) => candidate.id === fileId)!
const title = file.relativePath.split(/[\\/]/).pop() || file.relativePath || 'File'
return {