mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
perf(browser): use reverse guest tab lookup (#17462)
* perf(browser): use reverse guest tab lookup * test(browser): keep mutable tab harness lookup
This commit is contained in:
@@ -234,6 +234,44 @@ describe('AgentBrowserBridge', () => {
|
||||
expect(bridge.getActiveWebContentsId()).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves repeated guest lookups without enumerating registered tabs', () => {
|
||||
const tabCount = 1_000
|
||||
const tabs = new Map<string, number>()
|
||||
const tabIdByWebContentsId = new Map<number, string>()
|
||||
for (let index = 0; index < tabCount; index += 1) {
|
||||
const tabId = `tab-${index}`
|
||||
const webContentsId = 20_000 + index
|
||||
tabs.set(tabId, webContentsId)
|
||||
tabIdByWebContentsId.set(webContentsId, tabId)
|
||||
}
|
||||
|
||||
const getRegisteredTabs = vi.fn(() => {
|
||||
throw new Error('unexpected registered-tab enumeration')
|
||||
})
|
||||
const getTabIdForWebContentsId = vi.fn(
|
||||
(webContentsId: number) => tabIdByWebContentsId.get(webContentsId) ?? null
|
||||
)
|
||||
const b = new AgentBrowserBridge(
|
||||
mockBrowserManager(tabs, new Map(), {
|
||||
getWebContentsIdByTabId: getRegisteredTabs,
|
||||
getTabIdForWebContentsId
|
||||
})
|
||||
)
|
||||
const resolveTabIdSafe = (
|
||||
b as unknown as { resolveTabIdSafe: (webContentsId: number) => string | null }
|
||||
).resolveTabIdSafe.bind(b)
|
||||
const targetWebContentsId = 20_000 + tabCount - 1
|
||||
|
||||
// The old forward scan inspected 1,000,000 entries for this repeated last-tab lookup.
|
||||
for (let lookup = 0; lookup < tabCount; lookup += 1) {
|
||||
expect(resolveTabIdSafe(targetWebContentsId)).toBe(`tab-${tabCount - 1}`)
|
||||
}
|
||||
|
||||
expect(getTabIdForWebContentsId).toHaveBeenCalledTimes(tabCount)
|
||||
expect(getRegisteredTabs).not.toHaveBeenCalled()
|
||||
expect(resolveTabIdSafe(99_999)).toBeNull()
|
||||
})
|
||||
|
||||
it('closes the named agent-browser session when a tab closes', async () => {
|
||||
succeedWith({ snapshot: 'tree' })
|
||||
await bridge.snapshot()
|
||||
|
||||
@@ -18,6 +18,14 @@ export function mockBrowserManager(
|
||||
): BrowserManager {
|
||||
return {
|
||||
getWebContentsIdByTabId: () => tabs,
|
||||
getTabIdForWebContentsId: (webContentsId: number) => {
|
||||
for (const [tabId, tabWebContentsId] of tabs) {
|
||||
if (tabWebContentsId === webContentsId) {
|
||||
return tabId
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
getWorktreeIdForTab: (tabId: string) => worktrees.get(tabId),
|
||||
getGuestWebContentsId: vi.fn(() => null),
|
||||
getBrowserPageLoadError: vi.fn(() => null),
|
||||
|
||||
@@ -2861,13 +2861,7 @@ export class AgentBrowserBridge {
|
||||
}
|
||||
|
||||
private resolveTabIdSafe(webContentsId: number): string | null {
|
||||
const tabs = this.browserManager.getWebContentsIdByTabId()
|
||||
for (const [tabId, wcId] of tabs) {
|
||||
if (wcId === webContentsId) {
|
||||
return tabId
|
||||
}
|
||||
}
|
||||
return null
|
||||
return this.browserManager.getTabIdForWebContentsId(webContentsId)
|
||||
}
|
||||
|
||||
private requireTargetWebContents(target: ResolvedBrowserCommandTarget): WebContents {
|
||||
|
||||
@@ -541,6 +541,11 @@ describe('browserManager', () => {
|
||||
})
|
||||
expect(oldGuestOffMock).toHaveBeenCalled()
|
||||
expect(browserManager.getGuestWebContentsId('browser-1')).toBe(newGuest.id)
|
||||
expect(browserManager.getTabIdForWebContentsId(oldGuest.id)).toBeNull()
|
||||
expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBe('browser-1')
|
||||
|
||||
browserManager.unregisterGuest('browser-1')
|
||||
expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBeNull()
|
||||
})
|
||||
|
||||
it('cleans up prior guest listeners before re-registering the same tab', () => {
|
||||
|
||||
@@ -1575,6 +1575,10 @@ export class BrowserManager {
|
||||
return this.webContentsIdByTabId
|
||||
}
|
||||
|
||||
getTabIdForWebContentsId(webContentsId: number): string | null {
|
||||
return this.tabIdByWebContentsId.get(webContentsId) ?? null
|
||||
}
|
||||
|
||||
getWorktreeIdForTab(browserTabId: string): string | undefined {
|
||||
return this.worktreeIdByTabId.get(browserTabId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { webContentsFromIdMock } = vi.hoisted(() => ({
|
||||
webContentsFromIdMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
webContents: { fromId: webContentsFromIdMock }
|
||||
}))
|
||||
|
||||
import type { CdpTabState } from './cdp-auxiliary-commands'
|
||||
import { CdpBridgeState } from './cdp-bridge-state'
|
||||
import { CdpTabCommands } from './cdp-tab-commands'
|
||||
|
||||
function createBridgeState(
|
||||
getRegisteredTabs: () => Map<string, number>,
|
||||
getTabIdForWebContentsId: (webContentsId: number) => string | null
|
||||
) {
|
||||
let activeWebContentsId: number | null = null
|
||||
const bindings = {
|
||||
getActiveWebContentsId: () => activeWebContentsId,
|
||||
setActiveWebContentsId: (webContentsId: number | null) => {
|
||||
activeWebContentsId = webContentsId
|
||||
},
|
||||
getRegisteredTabs,
|
||||
getTabIdForWebContentsId,
|
||||
tabState: new Map<string, CdpTabState>(),
|
||||
commandQueues: new Map(),
|
||||
processingQueues: new Set<string>()
|
||||
}
|
||||
|
||||
return new CdpBridgeState(bindings)
|
||||
}
|
||||
|
||||
describe('CdpBridgeState reverse tab lookup', () => {
|
||||
it('resolves repeated lookups from the reverse map without enumerating tabs', () => {
|
||||
const tabCount = 1_000
|
||||
const lookupCount = 1_000
|
||||
const registeredTabs = new Map<string, number>()
|
||||
const tabIdByWebContentsId = new Map<number, string>()
|
||||
for (let index = 0; index < tabCount; index += 1) {
|
||||
const tabId = `tab-${index}`
|
||||
const webContentsId = 10_000 + index
|
||||
registeredTabs.set(tabId, webContentsId)
|
||||
tabIdByWebContentsId.set(webContentsId, tabId)
|
||||
}
|
||||
|
||||
const getRegisteredTabs = vi.fn(() => {
|
||||
throw new Error('unexpected registered-tab enumeration')
|
||||
})
|
||||
const getTabIdForWebContentsId = vi.fn(
|
||||
(webContentsId: number) => tabIdByWebContentsId.get(webContentsId) ?? null
|
||||
)
|
||||
const state = createBridgeState(getRegisteredTabs, getTabIdForWebContentsId)
|
||||
const targetWebContentsId = 10_000 + tabCount - 1
|
||||
|
||||
// The old forward scan inspected 1,000,000 entries for this repeated last-tab lookup.
|
||||
for (let lookup = 0; lookup < lookupCount; lookup += 1) {
|
||||
expect(state.resolveTabIdSafe(targetWebContentsId)).toBe(`tab-${tabCount - 1}`)
|
||||
}
|
||||
|
||||
expect(getTabIdForWebContentsId).toHaveBeenCalledTimes(lookupCount)
|
||||
expect(getRegisteredTabs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps strict and safe resolution semantics when a registration changes', () => {
|
||||
const registeredTabs = new Map([['tab-a', 101]])
|
||||
const tabIdByWebContentsId = new Map([[101, 'tab-a']])
|
||||
const state = createBridgeState(
|
||||
() => registeredTabs,
|
||||
(webContentsId) => tabIdByWebContentsId.get(webContentsId) ?? null
|
||||
)
|
||||
|
||||
expect(state.resolveTabId(101)).toBe('tab-a')
|
||||
expect(state.resolveTabIdSafe(999)).toBeNull()
|
||||
expect(() => state.resolveTabId(999)).toThrow('Tab is no longer registered.')
|
||||
|
||||
tabIdByWebContentsId.delete(101)
|
||||
tabIdByWebContentsId.set(202, 'tab-a')
|
||||
expect(state.resolveTabIdSafe(101)).toBeNull()
|
||||
expect(state.resolveTabId(202)).toBe('tab-a')
|
||||
})
|
||||
|
||||
it('lets CdpTabCommands read the active page id through the reverse map', () => {
|
||||
const tabIdByWebContentsId = new Map([[101, 'tab-a']])
|
||||
const getRegisteredTabs = vi.fn(() => {
|
||||
throw new Error('unexpected registered-tab enumeration')
|
||||
})
|
||||
const state = createBridgeState(
|
||||
getRegisteredTabs,
|
||||
(webContentsId) => tabIdByWebContentsId.get(webContentsId) ?? null
|
||||
)
|
||||
const commands = new CdpTabCommands(state, {} as never, {} as never, {} as never)
|
||||
|
||||
state.activeWebContentsId = 101
|
||||
expect(commands.getActivePageId()).toBe('tab-a')
|
||||
state.activeWebContentsId = 999
|
||||
expect(commands.getActivePageId()).toBeNull()
|
||||
expect(getRegisteredTabs).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ export type CdpBridgeStateBindings = {
|
||||
getActiveWebContentsId: () => number | null
|
||||
setActiveWebContentsId: (webContentsId: number | null) => void
|
||||
getRegisteredTabs: () => Map<string, number>
|
||||
getTabIdForWebContentsId: (webContentsId: number) => string | null
|
||||
tabState: Map<string, CdpTabState>
|
||||
commandQueues: Map<string, CdpQueuedCommand[]>
|
||||
processingQueues: Set<string>
|
||||
@@ -84,21 +85,15 @@ export class CdpBridgeState {
|
||||
}
|
||||
|
||||
resolveTabId(webContentsId: number): string {
|
||||
for (const [tabId, wcId] of this.getRegisteredTabs()) {
|
||||
if (wcId === webContentsId) {
|
||||
return tabId
|
||||
}
|
||||
const tabId = this.bindings.getTabIdForWebContentsId(webContentsId)
|
||||
if (tabId !== null) {
|
||||
return tabId
|
||||
}
|
||||
throw new BrowserError('browser_debugger_detached', 'Tab is no longer registered.')
|
||||
}
|
||||
|
||||
resolveTabIdSafe(webContentsId: number): string | null {
|
||||
for (const [tabId, wcId] of this.getRegisteredTabs()) {
|
||||
if (wcId === webContentsId) {
|
||||
return tabId
|
||||
}
|
||||
}
|
||||
return null
|
||||
return this.bindings.getTabIdForWebContentsId(webContentsId)
|
||||
}
|
||||
|
||||
getOrCreateTabState(tabId: string): CdpTabState {
|
||||
|
||||
@@ -60,6 +60,8 @@ export class CdpBridge {
|
||||
this.activeWebContentsId = webContentsId
|
||||
},
|
||||
getRegisteredTabs: () => this.getRegisteredTabs(),
|
||||
getTabIdForWebContentsId: (webContentsId) =>
|
||||
this.browserManager.getTabIdForWebContentsId(webContentsId),
|
||||
tabState: this.tabState,
|
||||
commandQueues: this.commandQueues,
|
||||
processingQueues: this.processingQueues
|
||||
|
||||
@@ -20,12 +20,7 @@ export class CdpTabCommands extends CdpBridgeCommandModule {
|
||||
if (!this.activeWebContentsId) {
|
||||
return null
|
||||
}
|
||||
for (const [tabId, wcId] of this.getRegisteredTabs()) {
|
||||
if (wcId === this.activeWebContentsId) {
|
||||
return tabId
|
||||
}
|
||||
}
|
||||
return null
|
||||
return this.resolveTabIdSafe(this.activeWebContentsId)
|
||||
}
|
||||
|
||||
getPageInfo(
|
||||
|
||||
Reference in New Issue
Block a user