diff --git a/config/scripts/browser-session-startup-metadata-benchmark.mjs b/config/scripts/browser-session-startup-metadata-benchmark.mjs new file mode 100644 index 00000000000..e1b5ca1cdca --- /dev/null +++ b/config/scripts/browser-session-startup-metadata-benchmark.mjs @@ -0,0 +1,167 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' + +const root = resolve(import.meta.dirname, '../..') +const baseline = process.argv[2] ?? 'HEAD^' +const require = createRequire(join(root, 'package.json')) +const { build } = require('esbuild') +const relative = 'src/main/browser/browser-session-registry.ts' +const target = join(root, relative) +const scratch = mkdtempSync(join(tmpdir(), 'orca-browser-meta-bench-')) +const previousSource = execFileSync('git', ['show', `${baseline}:${relative}`], { + cwd: root, + encoding: 'utf8' +}) +const sources = { before: previousSource, after: readFileSync(target, 'utf8') } +const stubs = new Map([ + [ + 'electron', + 'export const app={getPath:()=>globalThis.__metaBench.directory}; export const session={fromPartition:()=>({})};' + ], + [ + './browser-session-cookie-staging', + 'export function applyPendingBrowserCookieImports(){} export function clearPendingBrowserCookieImport(){}' + ], + [ + './browser-session-partition-policies', + 'export async function installBrowserSessionPartitionPolicies(){}' + ], + [ + './browser-session-route-policies', + 'export function clearBrowserRoutePartitionPolicies(){} export async function installBrowserRoutePartitionPolicies(){}' + ], + [ + './browser-session-profile-retirement', + 'export async function retireDeletedBrowserSessionProfile(){} export async function retireFailedBrowserSessionProfile(){}' + ], + [ + '../persistence/loading-store/user-data-path', + 'export function getCanonicalUserDataPath(){return globalThis.__metaBench.directory}' + ], + [ + './browser-identity-mode-store', + 'export async function markBrowserIdentityMigrationNoticePending(){}' + ], + ['./browser-site-permission-store', 'export function forgetBrowserSitePermissions(){}'], + ['./browser-site-display-store', 'export function forgetBrowserSiteDisplay(){}'], + [ + './extensions/browser-extension-session-loader', + 'export function attachBrowserExtensionsToProfileSession(profile,ids){globalThis.__metaBench.assignments.push([profile.id,ids])}' + ] +]) +const registryByVersion = {} +try { + for (const [version, source] of Object.entries(sources)) { + const output = join(scratch, `${version}.cjs`) + await build({ + stdin: { contents: source, sourcefile: target, resolveDir: dirname(target), loader: 'ts' }, + bundle: true, + platform: 'node', + format: 'cjs', + outfile: output, + plugins: [ + { + name: 'isolate-startup-metadata', + setup(builder) { + builder.onResolve({ filter: /.*/ }, (args) => { + if (args.importer === target && stubs.has(args.path)) { + return { path: args.path, namespace: 'startup-stub' } + } + if ( + args.path === 'node:fs' && + args.importer.endsWith('/browser-session-meta-store.ts') + ) { + return { path: 'fs-probe', namespace: 'startup-stub' } + } + }) + builder.onLoad({ filter: /.*/, namespace: 'startup-stub' }, (args) => ({ + contents: + args.path === 'fs-probe' + ? `import * as fs from 'node:fs'; export const {mkdirSync,renameSync,writeFileSync}=fs; export function readFileSync(...args){const result=fs.readFileSync(...args);globalThis.__metaBench.reads++;globalThis.__metaBench.bytes+=Buffer.byteLength(result);return result}` + : stubs.get(args.path), + loader: 'js' + })) + } + } + ] + }) + globalThis.__metaBench = { directory: scratch, reads: 0, bytes: 0, assignments: [] } + registryByVersion[version] = require(output).browserSessionRegistry + } + const rows = [] + for (const count of [10, 100, 1000]) { + const profiles = Array.from({ length: count }, (_, index) => { + const id = `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}` + return { + id, + scope: 'isolated', + partition: `persist:orca-browser-session-${id}`, + label: `Browser ${index}`, + source: null + } + }) + const enabledExtensionIdsByProfileId = Object.fromEntries([ + ['default', ['default-extension']], + ...profiles.map((profile, index) => [profile.id, [`extension-${index}`]]) + ]) + const json = JSON.stringify({ profiles, enabledExtensionIdsByProfileId }) + writeFileSync(join(scratch, 'browser-session-meta.json'), json) + const row = { profiles: count, metadataBytes: Buffer.byteLength(json) } + let beforeAssignments + for (const [version, registry] of Object.entries(registryByVersion)) { + const timings = [] + for (let iteration = 0; iteration < 4; iteration++) { + globalThis.__metaBench = { directory: scratch, reads: 0, bytes: 0, assignments: [] } + registry.configureForOrcaProfile({ + orcaProfileId: 'local-default', + profileDirectory: scratch + }) + globalThis.__metaBench.reads = 0 + globalThis.__metaBench.bytes = 0 + const start = performance.now() + registry.initializeBrowserSessionsFromPersistedState() + const durationMs = performance.now() - start + const probe = globalThis.__metaBench + assert.equal(probe.reads, version === 'before' ? count + 2 : 1) + assert.equal(registry.listProfiles().length, count + 1) + assert.equal(probe.assignments.length, count + 1) + if (version === 'before') { + beforeAssignments = probe.assignments + } else { + assert.deepEqual(probe.assignments, beforeAssignments) + } + if (iteration > 0) { + timings.push(durationMs) + } + row[version] = { reads: probe.reads, bytes: probe.bytes } + await Promise.resolve() + } + timings.sort((left, right) => left - right) + row[version].medianSynchronousMs = Number(timings[1].toFixed(3)) + } + rows.push(row) + } + console.log( + JSON.stringify( + { + node: process.version, + platform: process.platform, + baseline, + productionSeam: 'initializeBrowserSessionsFromPersistedState', + limitations: + 'Warm local filesystem; actual registry, JSON parser, profile validation and disk reads. Electron policy installation and extension loading stubbed. Synchronous hydration only; excludes process startup and guest creation.', + rows + }, + null, + 2 + ) + ) +} finally { + rmSync(scratch, { recursive: true, force: true }) + delete globalThis.__metaBench +} diff --git a/src/main/browser/browser-session-registry-startup-metadata.test.ts b/src/main/browser/browser-session-registry-startup-metadata.test.ts new file mode 100644 index 00000000000..bc986772893 --- /dev/null +++ b/src/main/browser/browser-session-registry-startup-metadata.test.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + DEFAULT_LOCAL_ORCA_PROFILE_ID, + getOrcaProfileBrowserSessionPartition +} from '../../shared/orca-profiles' +import { + createFsState, + installModuleMocks, + META_PATH, + seedMeta +} from './__mocks__/browser-session-registry-persistence-fixture' + +describe('browser profile startup metadata reads', () => { + beforeEach(() => { + vi.resetModules() + vi.restoreAllMocks() + }) + + it.each([10, 100])( + 'reads one snapshot when restoring %i profiles and keeps extension assignments current', + async (count) => { + const fsState = createFsState() + const profiles = Array.from({ length: count }, (_, index) => { + const id = `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}` + return { + id, + scope: 'isolated', + partition: getOrcaProfileBrowserSessionPartition(DEFAULT_LOCAL_ORCA_PROFILE_ID, id), + label: `Browser ${index}`, + source: null + } + }) + const enabledExtensionIdsByProfileId = Object.fromEntries([ + ['default', ['default-extension']], + ...profiles.map((profile, index) => [profile.id, [`extension-${index}`]]) + ]) + seedMeta(fsState, { profiles, enabledExtensionIdsByProfileId }) + installModuleMocks(fsState) + const attachExtensions = vi.fn() + vi.doMock('./extensions/browser-extension-session-loader', () => ({ + attachBrowserExtensionsToProfileSession: attachExtensions + })) + const { browserSessionRegistry } = await import('./browser-session-registry') + const { readFileSync } = await import('node:fs') + vi.mocked(readFileSync).mockClear() + + browserSessionRegistry.initializeBrowserSessionsFromPersistedState() + + expect( + vi.mocked(readFileSync).mock.calls.filter(([path]) => path === META_PATH) + ).toHaveLength(1) + expect(browserSessionRegistry.listProfiles()).toHaveLength(count + 1) + expect(attachExtensions).toHaveBeenCalledTimes(count + 1) + for (const profile of browserSessionRegistry.listProfiles()) { + expect(attachExtensions).toHaveBeenCalledWith( + profile, + enabledExtensionIdsByProfileId[profile.id] + ) + } + + seedMeta(fsState, { + profiles, + enabledExtensionIdsByProfileId: { default: ['new-extension'] } + }) + attachExtensions.mockClear() + vi.mocked(readFileSync).mockClear() + + browserSessionRegistry.initializeBrowserSessionsFromPersistedState() + + expect( + vi.mocked(readFileSync).mock.calls.filter(([path]) => path === META_PATH) + ).toHaveLength(1) + expect(attachExtensions).toHaveBeenCalledWith(browserSessionRegistry.getDefaultProfile(), [ + 'new-extension' + ]) + for (const profile of profiles) { + expect(attachExtensions).toHaveBeenCalledWith(profile, []) + } + } + ) +}) diff --git a/src/main/browser/browser-session-registry.ts b/src/main/browser/browser-session-registry.ts index 4fbe188721c..f1918196660 100644 --- a/src/main/browser/browser-session-registry.ts +++ b/src/main/browser/browser-session-registry.ts @@ -130,7 +130,7 @@ class BrowserSessionRegistry { } } if (meta.profiles.length > 0) { - this.hydrateFromPersisted(meta.profiles) + this.hydrateFromPersisted(meta.profiles, meta.enabledExtensionIdsByProfileId) } // Hydration skips the default profile, so install its policies separately. @@ -138,13 +138,16 @@ class BrowserSessionRegistry { void installBrowserSessionPartitionPolicies(defaultProfile).catch(() => { console.warn('[proxy] Failed to apply proxy to browser partition', defaultProfile.partition) }) - this.attachExtensions(defaultProfile) + this.attachExtensions(defaultProfile, meta.enabledExtensionIdsByProfileId.default ?? []) } // Why here and not in the partition installer: that installer also serves route, SSH and // doc-preview partitions, and release 1 runs extensions only in local browser profiles. - private attachExtensions(profile: BrowserSessionProfile): void { - attachBrowserExtensionsToProfileSession(profile, this.getEnabledExtensionIds(profile.id)) + private attachExtensions( + profile: BrowserSessionProfile, + enabledExtensionIds = this.getEnabledExtensionIds(profile.id) + ): void { + attachBrowserExtensionsToProfileSession(profile, enabledExtensionIds) } getEnabledExtensionIds(profileId: string): string[] { @@ -320,7 +323,10 @@ class BrowserSessionRegistry { } } - hydrateFromPersisted(profiles: BrowserSessionProfile[]): void { + hydrateFromPersisted( + profiles: BrowserSessionProfile[], + enabledExtensionIdsByProfileId = this.loadPersistedMeta().enabledExtensionIdsByProfileId + ): void { for (const profile of profiles) { if (!isValidPersistedBrowserSessionProfile(profile, this.activeOrcaProfileId)) { continue @@ -330,7 +336,7 @@ class BrowserSessionRegistry { void installBrowserSessionPartitionPolicies(profile).catch(() => { console.warn('[proxy] Failed to apply proxy to browser partition', profile.partition) }) - this.attachExtensions(profile) + this.attachExtensions(profile, enabledExtensionIdsByProfileId[profile.id] ?? []) } } }