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:
parent
dd6f0e63bb
commit
de04b7497f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue