Cache browser history by identity and snapshot omnibox entries

- Use WeakMap to cache prepared browser history entries by identity, so
  re-parsing is skipped for the same snapshot
- Change omnibox to read a history snapshot at menu open (via getState)
  instead of subscribing to live updates, preventing background navigations
  from reshuffling results mid-keystroke
- Support fully-qualified URL prefix matching (e.g., `https://github.com`)
  to preserve address-bar behavior
- Fix percentile calculation in performance tests (nearest-rank method)
This commit is contained in:
Jinjing
2026-08-31 23:52:58 -07:00
parent fd6339bf49
commit 01237b1328
5 changed files with 41 additions and 8 deletions
@@ -41,7 +41,7 @@ vi.mock('@/store', () => ({
browserDefaultSearchEngine: 'google',
getKnownWorktreeById: () => ({ path: '/tmp/wt' })
}),
{ getState: () => ({}) }
{ getState: () => ({ browserUrlHistory: historyStoreMock.entries }) }
)
}))
vi.mock('react', async () => {
@@ -85,9 +85,14 @@ export function useOmniboxBrowserHistory({
query: string
tabResults: readonly OpenTabSearchResult[]
}): readonly BrowserHistoryOmniboxRow[] {
// Why null while disabled: the tab bar is mounted for every workspace, and
// this array is replaced on every committed navigation in any browser tab.
const history = useAppStore((store) => (enabled ? store.browserUrlHistory : null))
// Why read once: the tab bar is mounted for every workspace, and this array
// is replaced on every committed navigation in any browser tab. History is
// intentionally a per-open snapshot so background navigations cannot churn
// the omnibox or reshuffle a selection under the user's fingers.
const history = useMemo(
() => (enabled ? useAppStore.getState().browserUrlHistory : null),
[enabled]
)
const prepared = useMemo(
() => (history ? prepareBrowserHistoryEntries(history) : null),
// oxlint-disable-next-line react-hooks/exhaustive-deps -- Snapshot per menu open; a background navigation must not reshuffle rows mid-keystroke.
@@ -26,7 +26,8 @@ const WORST_QUERY = 'observability rollout'
function percentile95(samples: number[]): number {
const sorted = [...samples].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))]
// Nearest-rank p95: ceil(n*0.95)-1, so 20 samples pick index 18 rather than the max.
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)]
}
describe('browser history match performance budget', () => {
@@ -41,7 +42,9 @@ describe('browser history match performance budget', () => {
const samples: number[] = []
for (let run = 0; run < 20; run += 1) {
const start = performance.now()
prepareBrowserHistoryEntries(entries)
// Use a fresh array identity so this measures preparation rather than
// the identity cache used by live address bars and omniboxes.
prepareBrowserHistoryEntries(entries.slice())
samples.push(performance.now() - start)
}
expect(percentile95(samples)).toBeLessThan(prepareP95Ms)
@@ -56,6 +56,12 @@ describe('browser history match', () => {
])
})
it('keeps fully-qualified URL prefixes in the top tier', () => {
expect(match([entry({ url: 'https://github.com/acme' })], 'https://github.com')).toEqual([
{ tier: 'host-prefix', url: 'https://github.com/acme' }
])
})
it('leaves a subdomain match in host-substring rather than host-prefix', () => {
expect(match([entry({ url: 'https://docs.github.com/actions' })], 'git')).toEqual([
{ tier: 'host-substring', url: 'https://docs.github.com/actions' }
@@ -131,6 +137,13 @@ describe('browser history match', () => {
expect(matchBrowserHistory({ prepared, query: 'git', limit: 0, now: NOW })).toBe(missA)
})
it('reuses preparation for the same immutable history snapshot', () => {
const first = prepareBrowserHistoryEntries(GIT_CORPUS)
expect(prepareBrowserHistoryEntries(GIT_CORPUS)).toBe(first)
expect(prepareBrowserHistoryEntries([...GIT_CORPUS])).not.toBe(first)
})
it('survives an unparseable url by falling back to url and title matching', () => {
const broken = entry({ url: 'not a url at all', title: 'Broken' })
+14 -2
View File
@@ -34,6 +34,10 @@ export type BrowserHistoryMatch = {
}
const NO_MATCHES: readonly BrowserHistoryMatch[] = []
const preparedHistoryCache = new WeakMap<
readonly BrowserHistoryEntry[],
readonly PreparedBrowserHistoryEntry[]
>()
function historyHost(url: string): string {
try {
@@ -53,13 +57,19 @@ export function formatBrowserHistoryUrl(url: string): string {
export function prepareBrowserHistoryEntries(
entries: readonly BrowserHistoryEntry[]
): readonly PreparedBrowserHistoryEntry[] {
return entries.map((entry) => ({
const cached = preparedHistoryCache.get(entries)
if (cached) {
return cached
}
const prepared = entries.map((entry) => ({
entry,
lowerUrl: entry.url.toLowerCase(),
lowerTitle: entry.title.toLowerCase(),
lowerHost: historyHost(entry.url),
frecencyBase: Math.min(entry.visitCount, MAX_VISIT_COUNT_BONUS)
}))
preparedHistoryCache.set(entries, prepared)
return prepared
}
function matchTier(
@@ -69,7 +79,9 @@ function matchTier(
// A workspace-doc entry's url is a filesystem path, so it has no host: a path
// prefix is as deliberate as a host prefix and earns the same top tier.
const prefixTarget = prepared.lowerHost === '' ? prepared.lowerUrl : prepared.lowerHost
if (prefixTarget.startsWith(lowerQuery)) {
// Preserve the address-bar's long-standing behavior for fully-qualified
// input (e.g. `https://github.com`), while still ranking bare hosts by host.
if (prefixTarget.startsWith(lowerQuery) || prepared.lowerUrl.startsWith(lowerQuery)) {
return 'host-prefix'
}
if (prepared.lowerHost.includes(lowerQuery)) {