Reduce idle git status polling (#7069)
This commit is contained in:
parent
7c66f37632
commit
171d32d8f7
|
|
@ -46,6 +46,7 @@ async function usePollingOnce(
|
|||
sshStatus?: string
|
||||
enabled?: boolean
|
||||
expectStatusCall?: boolean
|
||||
stateOverrides?: Partial<PollState>
|
||||
} = {}
|
||||
): Promise<{ state: PollState; gitStatus: ReturnType<typeof vi.fn> }> {
|
||||
vi.resetModules()
|
||||
|
|
@ -67,6 +68,7 @@ async function usePollingOnce(
|
|||
rightSidebarTab: 'source-control',
|
||||
openFiles: []
|
||||
}
|
||||
Object.assign(state, options.stateOverrides)
|
||||
const mockedRepo = { ...repo, connectionId: options.connectionId ?? null }
|
||||
const gitStatus = vi.fn().mockResolvedValue(status)
|
||||
|
||||
|
|
@ -226,6 +228,27 @@ describe('useGitStatusPolling', () => {
|
|||
|
||||
expect(gitStatus).not.toHaveBeenCalled()
|
||||
expect(state.setGitStatus).not.toHaveBeenCalled()
|
||||
expect(globalThis.setInterval).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses a slower git status cadence when only terminal branch detection needs polling', async () => {
|
||||
const { gitStatus } = await usePollingOnce(
|
||||
{
|
||||
entries: [],
|
||||
conflictOperation: 'unknown',
|
||||
head: 'abc123',
|
||||
branch: 'refs/heads/main'
|
||||
},
|
||||
{
|
||||
stateOverrides: {
|
||||
rightSidebarOpen: false,
|
||||
openFiles: []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(gitStatus).toHaveBeenCalledTimes(1)
|
||||
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 30_000)
|
||||
})
|
||||
|
||||
it('does not install the visible git status poll while disabled', async () => {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,18 @@ import { getRuntimeGitConflictOperation } from '@/runtime/runtime-git-client'
|
|||
import { refreshGitStatusForWorktree } from './git-status-refresh'
|
||||
import { type CoalescedPollRunner, createCoalescedPollRunner } from './coalesced-poll-runner'
|
||||
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
|
||||
import { shouldPollActiveGitStatus } from '@/lib/passive-macos-app-data-access'
|
||||
import {
|
||||
hasInteractiveActiveGitStatusConsumer,
|
||||
shouldPollActiveGitStatus
|
||||
} from '@/lib/passive-macos-app-data-access'
|
||||
import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner'
|
||||
import { useGitStatusFileWatchRefresh } from './git-status-file-watch-refresh'
|
||||
|
||||
const POLL_INTERVAL_MS = 3000
|
||||
const MIN_STATUS_REFRESH_INTERVAL_MS = 3000
|
||||
const INTERACTIVE_STATUS_POLL_INTERVAL_MS = MIN_STATUS_REFRESH_INTERVAL_MS
|
||||
// Why: file-watch refreshes cover content changes; terminal-only polling is
|
||||
// just a fallback for branch switches made inside shells.
|
||||
const TERMINAL_ONLY_STATUS_POLL_INTERVAL_MS = 30_000
|
||||
|
||||
export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
||||
const enabled = options.enabled ?? true
|
||||
|
|
@ -44,6 +51,29 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
!connectionId || sshConnectionStates.get(connectionId)?.status === 'connected',
|
||||
[sshConnectionStates]
|
||||
)
|
||||
const activeGitStatusPollingArgs = {
|
||||
activeWorktreeId,
|
||||
worktreePath,
|
||||
rightSidebarOpen,
|
||||
rightSidebarTab,
|
||||
rightSidebarExplorerView,
|
||||
openFiles
|
||||
}
|
||||
const isActiveConnectionReady = isConnectionReady(activeConnectionId)
|
||||
const shouldPollActiveWorktreeGitStatus =
|
||||
enabled &&
|
||||
!!activeWorktreeId &&
|
||||
!!worktreePath &&
|
||||
activeRepoSupportsGit &&
|
||||
shouldPollActiveGitStatus(activeGitStatusPollingArgs) &&
|
||||
isActiveConnectionReady &&
|
||||
!gitStatusHugeByWorktree?.[activeWorktreeId]
|
||||
const activeStatusPollIntervalMs = hasInteractiveActiveGitStatusConsumer(
|
||||
activeGitStatusPollingArgs
|
||||
)
|
||||
? INTERACTIVE_STATUS_POLL_INTERVAL_MS
|
||||
: TERMINAL_ONLY_STATUS_POLL_INTERVAL_MS
|
||||
const activeStatusPollScope = shouldPollActiveWorktreeGitStatus ? activeWorktreeId : null
|
||||
|
||||
// Why: build a list of non-active worktrees that still have a known conflict
|
||||
// operation (merge/rebase/cherry-pick). These need lightweight polling so
|
||||
|
|
@ -68,34 +98,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
}, [allWorktrees, conflictOperationByWorktree, activeWorktreeId, repoMap])
|
||||
|
||||
const runFetchStatus = useCallback(async () => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
if (!activeWorktreeId || !worktreePath) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!shouldPollActiveGitStatus({
|
||||
activeWorktreeId,
|
||||
worktreePath,
|
||||
rightSidebarOpen,
|
||||
rightSidebarTab,
|
||||
rightSidebarExplorerView,
|
||||
openFiles
|
||||
}) ||
|
||||
!activeRepoSupportsGit
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (!isConnectionReady(activeConnectionId)) {
|
||||
return
|
||||
}
|
||||
// Why: once a repo's status was truncated at the entry limit, re-running git
|
||||
// status every 3s just re-does expensive work and re-truncates. Pause the
|
||||
// automatic poll while huge (a manual refresh still goes through its own
|
||||
// path); resolving the changes (e.g. .gitignoring the huge folder) clears
|
||||
// the flag and polling resumes. Mirrors a "huge repo" disabling auto status.
|
||||
if (gitStatusHugeByWorktree?.[activeWorktreeId]) {
|
||||
if (!shouldPollActiveWorktreeGitStatus || !activeWorktreeId || !worktreePath) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
|
|
@ -117,18 +120,10 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
// ignore
|
||||
}
|
||||
}, [
|
||||
activeRepoSupportsGit,
|
||||
activeConnectionId,
|
||||
activePushTarget,
|
||||
activeWorktreeId,
|
||||
enabled,
|
||||
fetchUpstreamStatus,
|
||||
gitStatusHugeByWorktree,
|
||||
isConnectionReady,
|
||||
openFiles,
|
||||
rightSidebarExplorerView,
|
||||
rightSidebarOpen,
|
||||
rightSidebarTab,
|
||||
shouldPollActiveWorktreeGitStatus,
|
||||
worktreePath,
|
||||
setGitStatus,
|
||||
setUpstreamStatus,
|
||||
|
|
@ -147,7 +142,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
const statusPollRunnerRef = useRef<CoalescedPollRunner | null>(null)
|
||||
useEffect(() => {
|
||||
const runner = createCoalescedPollRunner(() => runFetchStatusRef.current(), {
|
||||
minIntervalMs: POLL_INTERVAL_MS
|
||||
minIntervalMs: MIN_STATUS_REFRESH_INTERVAL_MS
|
||||
})
|
||||
statusPollRunnerRef.current = runner
|
||||
return () => {
|
||||
|
|
@ -158,16 +153,19 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
|
||||
const fetchStatus = useCallback(() => {
|
||||
statusPollRunnerRef.current?.run()
|
||||
}, [activeWorktreeId])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
if (!activeStatusPollScope) {
|
||||
return
|
||||
}
|
||||
// Why: this root-level poll should pause while hidden, but visible
|
||||
// unfocused windows still need fresh status for second-display workflows.
|
||||
return installWindowVisibilityInterval({ run: fetchStatus, intervalMs: POLL_INTERVAL_MS })
|
||||
}, [enabled, fetchStatus])
|
||||
return installWindowVisibilityInterval({
|
||||
run: fetchStatus,
|
||||
intervalMs: activeStatusPollIntervalMs
|
||||
})
|
||||
}, [activeStatusPollIntervalMs, activeStatusPollScope, fetchStatus])
|
||||
|
||||
useGitStatusFileWatchRefresh({
|
||||
activeConnectionId,
|
||||
|
|
@ -225,7 +223,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
|||
// visible unfocused windows, but do not poll disconnected hidden windows.
|
||||
const stopVisiblePoll = installWindowVisibilityInterval({
|
||||
run: () => pollRunner.run(),
|
||||
intervalMs: POLL_INTERVAL_MS
|
||||
intervalMs: MIN_STATUS_REFRESH_INTERVAL_MS
|
||||
})
|
||||
return () => {
|
||||
pollRunner.dispose()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function isMacAppDataPath(path: string | null | undefined, userAgent?: st
|
|||
return MAC_APP_DATA_SEGMENT_RE.test(path.replace(/\\/g, '/'))
|
||||
}
|
||||
|
||||
export function shouldPollActiveGitStatus(args: {
|
||||
export type ActiveGitStatusPollingArgs = {
|
||||
activeWorktreeId: string | null
|
||||
worktreePath: string | null
|
||||
rightSidebarOpen: boolean
|
||||
|
|
@ -28,7 +28,9 @@ export function shouldPollActiveGitStatus(args: {
|
|||
rightSidebarExplorerView?: RightSidebarExplorerView
|
||||
openFiles?: OpenFile[]
|
||||
userAgent?: string
|
||||
}): boolean {
|
||||
}
|
||||
|
||||
export function hasInteractiveActiveGitStatusConsumer(args: ActiveGitStatusPollingArgs): boolean {
|
||||
if (!args.activeWorktreeId || !args.worktreePath) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -43,6 +45,16 @@ export function shouldPollActiveGitStatus(args: {
|
|||
if ((args.openFiles ?? []).some((file) => file.worktreeId === args.activeWorktreeId)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function shouldPollActiveGitStatus(args: ActiveGitStatusPollingArgs): boolean {
|
||||
if (!args.activeWorktreeId || !args.worktreePath) {
|
||||
return false
|
||||
}
|
||||
if (hasInteractiveActiveGitStatusConsumer(args)) {
|
||||
return true
|
||||
}
|
||||
// Why: macOS app-container paths can trigger the "data from other apps"
|
||||
// prompt. Keep terminal-only workspace switching from passively probing them.
|
||||
return !isMacAppDataPath(args.worktreePath, args.userAgent)
|
||||
|
|
|
|||
Loading…
Reference in New Issue