mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
* perf(git): only schedule the upstream-ref poll for the repo that has one A single global binding means at most one worktree holds a selected upstream ref at a time, so every other repo's 2s poll woke only to stat an empty set. Rebinding is synchronous and in-process, so reacting to it detects a newly selected ref exactly as fast as polling did — the wake-ups were pure waste. Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle. * fix(git-watch): re-read ref selection after building the poller A rebind can flip back while the poller is being constructed. The concurrent unbind sees statusRefPolling still null and correctly does nothing, so without re-reading selection the in-flight build adopts a poller for a repo that no longer holds a ref — reinstating the idle wake-ups this change removes. * fix(git-watch): fence concurrent ref-poller starts with a generation token Startup and each rebind could be mid-build at once, and the startup path adopted its poller unconditionally. A rebind that won the slot first was then overwritten without being unsubscribed, stranding that poller's timer and visibility listener for the process lifetime. The generation names which attempt still owns the slot so every loser tears down what it built. Adds a regression test driving the real binding path; it fails if the poller is dropped without unsubscribing. Reported by CodeRabbit on #16443.
243 lines
7.8 KiB
TypeScript
243 lines
7.8 KiB
TypeScript
import { join } from 'node:path'
|
|
import { subscribeViaWatcherProcess } from './parcel-watcher-process'
|
|
import type { WatcherProcessSubscription } from './parcel-watcher-process-subscription'
|
|
import type {
|
|
WorktreeBasePollEvent,
|
|
WorktreeBaseSubscription,
|
|
WorktreePollerWindowVisibility
|
|
} from './worktree-base-directory-poller'
|
|
import { createSingleFlight } from './single-flight-promise'
|
|
import { onActiveGitStatusRefBindingChanged } from './worktree-git-status-ref-watch'
|
|
import { PRIMARY_CHECKOUT_METADATA_FILES } from './worktree-git-common-metadata-files'
|
|
import { startGitCommonPrimaryPolling } from './worktree-git-common-primary-polling'
|
|
|
|
const PRIMARY_WATCH_OPTIONS = {
|
|
mode: 'shallow' as const,
|
|
include: PRIMARY_CHECKOUT_METADATA_FILES
|
|
}
|
|
|
|
// Why: the shallow watcher can stop reporting without ever erroring — a lossy
|
|
// notification path (network mount, inotify queue overflow in the shared child),
|
|
// a dropped event batch, or a binding that went deaf between rebind sweeps. None
|
|
// of those raise, so the error-driven fallback never fires. A bounded re-stat is
|
|
// the only thing that turns "silently stale forever" into "stale for one tick".
|
|
// At 15 ticks this is ~0.2 stats/s/repo against the 3/s the old poll cost.
|
|
const PRIMARY_BACKSTOP_TICKS = 15
|
|
|
|
function primaryMetadataEvents(commonDirPath: string): WorktreeBasePollEvent[] {
|
|
return PRIMARY_CHECKOUT_METADATA_FILES.map((name) => ({
|
|
type: 'update',
|
|
path: join(commonDirPath, name)
|
|
}))
|
|
}
|
|
|
|
export async function startGitCommonPrimaryWatch(
|
|
commonDirPath: string,
|
|
getStatusRefPaths: () => readonly string[],
|
|
onEvents: (events: WorktreeBasePollEvent[]) => void,
|
|
pollIntervalMs: number,
|
|
visibility: WorktreePollerWindowVisibility,
|
|
onFullScan?: () => void,
|
|
onWatchError?: (error: Error) => void
|
|
): Promise<WorktreeBaseSubscription> {
|
|
let disposed = false
|
|
let watcher: WatcherProcessSubscription | null = null
|
|
let statusRefPolling: WorktreeBaseSubscription | null = null
|
|
// Why: startup and every rebind can each be mid-build at the same time. The
|
|
// generation names which attempt still owns the slot, so a loser unsubscribes
|
|
// its own poller instead of being silently overwritten — an overwrite would
|
|
// strand that poller's timer and visibility listener for the process lifetime.
|
|
let statusRefGeneration = 0
|
|
let backstopPolling: WorktreeBaseSubscription | null = null
|
|
let fallback: WorktreeBaseSubscription | null = null
|
|
const fallbackFlight = createSingleFlight()
|
|
|
|
const startFallback = (): Promise<void> =>
|
|
fallbackFlight.run(() =>
|
|
startGitCommonPrimaryPolling(
|
|
commonDirPath,
|
|
getStatusRefPaths,
|
|
onEvents,
|
|
pollIntervalMs,
|
|
visibility,
|
|
onFullScan
|
|
).then(async (nextFallback) => {
|
|
// `statusRefPolling` deliberately excluded: it covers only status refs,
|
|
// so its presence is not evidence that primary metadata is covered.
|
|
if (disposed || watcher) {
|
|
await nextFallback.unsubscribe()
|
|
return
|
|
}
|
|
void stopWatcherSidePolling()
|
|
fallback = nextFallback
|
|
})
|
|
)
|
|
|
|
const startStatusRefPollingIfSelected = async (): Promise<WorktreeBaseSubscription | null> =>
|
|
getStatusRefPaths().length === 0
|
|
? null
|
|
: startGitCommonPrimaryPolling(
|
|
commonDirPath,
|
|
getStatusRefPaths,
|
|
onEvents,
|
|
pollIntervalMs,
|
|
visibility,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
// Why: rebinding is synchronous and in-process, so reacting to it costs the
|
|
// same detection latency as polling would have, without the idle wake-ups.
|
|
// Adopts a freshly built poller only if this attempt still owns the slot and a
|
|
// ref is still selected; anything else unsubscribes what it built.
|
|
const adoptStatusRefPolling = async (
|
|
generation: number,
|
|
next: WorktreeBaseSubscription | null
|
|
): Promise<void> => {
|
|
if (!next) {
|
|
return
|
|
}
|
|
if (
|
|
generation !== statusRefGeneration ||
|
|
disposed ||
|
|
!watcher ||
|
|
statusRefPolling ||
|
|
getStatusRefPaths().length === 0
|
|
) {
|
|
await next.unsubscribe().catch(() => {})
|
|
return
|
|
}
|
|
statusRefPolling = next
|
|
}
|
|
|
|
const syncStatusRefPolling = async (): Promise<void> => {
|
|
if (disposed || !watcher) {
|
|
return
|
|
}
|
|
const generation = ++statusRefGeneration
|
|
const selected = getStatusRefPaths().length > 0
|
|
if (selected === (statusRefPolling !== null)) {
|
|
return
|
|
}
|
|
if (!selected) {
|
|
const current = statusRefPolling
|
|
statusRefPolling = null
|
|
await current?.unsubscribe().catch(() => {})
|
|
return
|
|
}
|
|
await adoptStatusRefPolling(generation, await startStatusRefPollingIfSelected())
|
|
}
|
|
|
|
const unsubscribeBindingChanges = onActiveGitStatusRefBindingChanged(() => {
|
|
void syncStatusRefPolling().catch(() => {})
|
|
})
|
|
|
|
const stopWatcherSidePolling = async (): Promise<void> => {
|
|
statusRefGeneration++
|
|
const current = statusRefPolling
|
|
const currentBackstop = backstopPolling
|
|
statusRefPolling = null
|
|
backstopPolling = null
|
|
await Promise.all([
|
|
current?.unsubscribe().catch(() => {}),
|
|
currentBackstop?.unsubscribe().catch(() => {})
|
|
])
|
|
}
|
|
|
|
const handleWatcherError = (error: Error): void => {
|
|
if (disposed) {
|
|
return
|
|
}
|
|
onWatchError?.(error)
|
|
if (!onWatchError) {
|
|
onEvents(primaryMetadataEvents(commonDirPath))
|
|
}
|
|
const current = watcher
|
|
watcher = null
|
|
if (current) {
|
|
void current.unsubscribe().catch(() => {})
|
|
}
|
|
void stopWatcherSidePolling()
|
|
.then(() => startFallback())
|
|
.catch(() => {})
|
|
}
|
|
|
|
try {
|
|
watcher = await subscribeViaWatcherProcess(
|
|
commonDirPath,
|
|
(error, events) => {
|
|
if (error) {
|
|
handleWatcherError(error)
|
|
return
|
|
}
|
|
if (events.length > 0) {
|
|
onEvents(events.map((event) => ({ type: event.type, path: event.path })))
|
|
}
|
|
},
|
|
PRIMARY_WATCH_OPTIONS,
|
|
{
|
|
onInterruption: () => {
|
|
if (!disposed) {
|
|
const error = new Error('Git primary metadata watcher interrupted')
|
|
if (onWatchError) {
|
|
onWatchError(error)
|
|
} else {
|
|
onEvents(primaryMetadataEvents(commonDirPath))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
if (watcher && !disposed && !fallbackFlight.pending()) {
|
|
const generation = ++statusRefGeneration
|
|
const [nextStatusRefPolling, nextBackstop] = await Promise.all([
|
|
startStatusRefPollingIfSelected(),
|
|
startGitCommonPrimaryPolling(
|
|
commonDirPath,
|
|
() => [],
|
|
onEvents,
|
|
pollIntervalMs * PRIMARY_BACKSTOP_TICKS,
|
|
visibility,
|
|
undefined,
|
|
true
|
|
)
|
|
])
|
|
// Why: a terminal watch error can land while the two polls above are still
|
|
// starting. handleWatcherError already ran its teardown against nulls, so
|
|
// adopting these now would strand the repo with status-ref coverage only.
|
|
if (disposed || !watcher) {
|
|
await Promise.all([
|
|
nextStatusRefPolling?.unsubscribe().catch(() => {}),
|
|
nextBackstop.unsubscribe().catch(() => {})
|
|
])
|
|
if (!disposed) {
|
|
await startFallback()
|
|
}
|
|
} else {
|
|
await adoptStatusRefPolling(generation, nextStatusRefPolling)
|
|
backstopPolling = nextBackstop
|
|
}
|
|
}
|
|
} catch (error) {
|
|
handleWatcherError(error instanceof Error ? error : new Error(String(error)))
|
|
}
|
|
|
|
return {
|
|
unsubscribe: async () => {
|
|
disposed = true
|
|
unsubscribeBindingChanges()
|
|
const current = watcher
|
|
watcher = null
|
|
if (current) {
|
|
await current.unsubscribe().catch(() => {})
|
|
}
|
|
await stopWatcherSidePolling()
|
|
await fallbackFlight.pending()?.catch(() => {})
|
|
if (fallback) {
|
|
await fallback.unsubscribe().catch(() => {})
|
|
fallback = null
|
|
}
|
|
}
|
|
}
|
|
}
|