mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
perf(terminal): prioritize active hidden restores
This commit is contained in:
@@ -57,6 +57,38 @@ describe('hidden output restore scheduler', () => {
|
||||
expect(activeRestore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('waits for every active restore before draining inactive panes', async () => {
|
||||
let settleFirst: (() => void) | undefined
|
||||
let settleSecond: (() => void) | undefined
|
||||
const inactiveRestore = vi.fn()
|
||||
const firstActiveRestore = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
settleFirst = resolve
|
||||
})
|
||||
)
|
||||
const secondActiveRestore = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
settleSecond = resolve
|
||||
})
|
||||
)
|
||||
|
||||
scheduleHiddenOutputRestore({}, inactiveRestore, 'inactive')
|
||||
scheduleHiddenOutputRestore({}, firstActiveRestore, 'active')
|
||||
scheduleHiddenOutputRestore({}, secondActiveRestore, 'active')
|
||||
await vi.advanceTimersByTimeAsync(160)
|
||||
expect(inactiveRestore).not.toHaveBeenCalled()
|
||||
|
||||
settleFirst?.()
|
||||
await vi.advanceTimersByTimeAsync(160)
|
||||
expect(inactiveRestore).not.toHaveBeenCalled()
|
||||
|
||||
settleSecond?.()
|
||||
await vi.advanceTimersByTimeAsync(16)
|
||||
expect(inactiveRestore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('can cancel pending inactive restores', () => {
|
||||
const target = {}
|
||||
const requestRestore = vi.fn()
|
||||
@@ -67,4 +99,50 @@ describe('hidden output restore scheduler', () => {
|
||||
|
||||
expect(requestRestore).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('releases active priority when its target is canceled', async () => {
|
||||
const activeTarget = {}
|
||||
const activeRestore = vi.fn(() => new Promise<void>(() => undefined))
|
||||
const inactiveRestore = vi.fn()
|
||||
|
||||
scheduleHiddenOutputRestore(activeTarget, activeRestore, 'active')
|
||||
scheduleHiddenOutputRestore({}, inactiveRestore, 'inactive')
|
||||
cancelScheduledHiddenOutputRestore(activeTarget)
|
||||
await vi.advanceTimersByTimeAsync(16)
|
||||
|
||||
expect(inactiveRestore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('ignores completion from a superseded active restore', async () => {
|
||||
const activeTarget = {}
|
||||
let settleFirst: (() => void) | undefined
|
||||
let settleSecond: (() => void) | undefined
|
||||
const inactiveRestore = vi.fn()
|
||||
|
||||
scheduleHiddenOutputRestore(
|
||||
activeTarget,
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
settleFirst = resolve
|
||||
}),
|
||||
'active'
|
||||
)
|
||||
scheduleHiddenOutputRestore(
|
||||
activeTarget,
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
settleSecond = resolve
|
||||
}),
|
||||
'active'
|
||||
)
|
||||
scheduleHiddenOutputRestore({}, inactiveRestore, 'inactive')
|
||||
|
||||
settleFirst?.()
|
||||
await vi.advanceTimersByTimeAsync(160)
|
||||
expect(inactiveRestore).not.toHaveBeenCalled()
|
||||
|
||||
settleSecond?.()
|
||||
await vi.advanceTimersByTimeAsync(16)
|
||||
expect(inactiveRestore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
type HiddenOutputRestorePriority = 'active' | 'inactive'
|
||||
|
||||
type HiddenOutputRestoreRequest = () => void
|
||||
type HiddenOutputRestoreRequest = () => void | Promise<void>
|
||||
|
||||
type HiddenOutputRestoreEntry = {
|
||||
requestRestore: HiddenOutputRestoreRequest
|
||||
@@ -11,6 +11,8 @@ type HiddenOutputRestoreEntry = {
|
||||
const INACTIVE_RESTORE_INTERVAL_MS = 16
|
||||
|
||||
const inactiveRestoreQueue = new Map<object, HiddenOutputRestoreEntry>()
|
||||
// Why: overlapping deep replays can starve the active pane the user is returning to.
|
||||
const activeRestoreTokensByTarget = new Map<object, object>()
|
||||
let inactiveRestoreTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function clearInactiveRestoreTimer(): void {
|
||||
@@ -22,14 +24,48 @@ function clearInactiveRestoreTimer(): void {
|
||||
}
|
||||
|
||||
function scheduleInactiveRestoreDrain(): void {
|
||||
if (inactiveRestoreTimer !== null || inactiveRestoreQueue.size === 0) {
|
||||
if (
|
||||
inactiveRestoreTimer !== null ||
|
||||
activeRestoreTokensByTarget.size > 0 ||
|
||||
inactiveRestoreQueue.size === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
inactiveRestoreTimer = setTimeout(drainInactiveRestoreQueue, INACTIVE_RESTORE_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function finishActiveRestore(target: object, token: object): void {
|
||||
if (activeRestoreTokensByTarget.get(target) !== token) {
|
||||
return
|
||||
}
|
||||
activeRestoreTokensByTarget.delete(target)
|
||||
scheduleInactiveRestoreDrain()
|
||||
}
|
||||
|
||||
function runActiveRestore(target: object, requestRestore: HiddenOutputRestoreRequest): void {
|
||||
const token = {}
|
||||
activeRestoreTokensByTarget.set(target, token)
|
||||
try {
|
||||
const completion = requestRestore()
|
||||
if (completion) {
|
||||
void completion.then(
|
||||
() => finishActiveRestore(target, token),
|
||||
() => finishActiveRestore(target, token)
|
||||
)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
finishActiveRestore(target, token)
|
||||
throw error
|
||||
}
|
||||
finishActiveRestore(target, token)
|
||||
}
|
||||
|
||||
function drainInactiveRestoreQueue(): void {
|
||||
inactiveRestoreTimer = null
|
||||
if (activeRestoreTokensByTarget.size > 0) {
|
||||
return
|
||||
}
|
||||
const next = inactiveRestoreQueue.entries().next()
|
||||
if (next.done) {
|
||||
return
|
||||
@@ -47,7 +83,7 @@ export function scheduleHiddenOutputRestore(
|
||||
): void {
|
||||
if (priority === 'active') {
|
||||
cancelScheduledHiddenOutputRestore(target)
|
||||
requestRestore()
|
||||
runActiveRestore(target, requestRestore)
|
||||
return
|
||||
}
|
||||
inactiveRestoreQueue.set(target, { requestRestore })
|
||||
@@ -56,12 +92,17 @@ export function scheduleHiddenOutputRestore(
|
||||
|
||||
export function cancelScheduledHiddenOutputRestore(target: object): void {
|
||||
inactiveRestoreQueue.delete(target)
|
||||
const activeToken = activeRestoreTokensByTarget.get(target)
|
||||
if (activeToken) {
|
||||
finishActiveRestore(target, activeToken)
|
||||
}
|
||||
if (inactiveRestoreQueue.size === 0) {
|
||||
clearInactiveRestoreTimer()
|
||||
}
|
||||
}
|
||||
|
||||
export function resetHiddenOutputRestoreSchedulerForTests(): void {
|
||||
activeRestoreTokensByTarget.clear()
|
||||
inactiveRestoreQueue.clear()
|
||||
clearInactiveRestoreTimer()
|
||||
}
|
||||
|
||||
@@ -7048,6 +7048,16 @@ export function connectPanePty(
|
||||
}
|
||||
cancelScheduledHiddenOutputRestore(pane.terminal)
|
||||
hiddenOutputRestoreScheduled = false
|
||||
let restoreRequested = false
|
||||
scheduleHiddenOutputRestore(
|
||||
pane.terminal,
|
||||
() => {
|
||||
restoreRequested = requestHiddenOutputRestoreIfNeeded({ bypassScheduler: true })
|
||||
return hiddenOutputRestoreInFlight ?? undefined
|
||||
},
|
||||
priority
|
||||
)
|
||||
return restoreRequested
|
||||
}
|
||||
clearHiddenOutputRestoreDeferredRetryTimer()
|
||||
hiddenOutputRestoreRetryDeferred = false
|
||||
|
||||
Reference in New Issue
Block a user