mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
perf(cmd-j): keep cold palette within frame budget
This commit is contained in:
@@ -13,6 +13,8 @@ import { StarNagToastHost } from '../components/star-nag/StarNagToastHost'
|
||||
import { TelemetryFirstLaunchSurface } from '../components/TelemetryFirstLaunchSurface'
|
||||
import { ZoomOverlay } from '../components/ZoomOverlay'
|
||||
import { shouldRenderPetOverlay } from '../components/pet/pet-overlay-visibility'
|
||||
// Why eager: lazy evaluation alone delayed the first global Cmd+J results by ~300 ms.
|
||||
import WorktreeJumpPalette from '../components/WorktreeJumpPalette'
|
||||
import { useAppStore } from '../store'
|
||||
import type { UpdateStatus } from '../../../shared/update-status-types'
|
||||
import { useLazyModalMounts } from './use-lazy-modal-mounts'
|
||||
@@ -20,7 +22,6 @@ import type { FloatingWorkspacePanelState } from './use-floating-workspace-panel
|
||||
import type { OnboardingGate } from './use-onboarding-and-feature-tips'
|
||||
|
||||
const QuickOpen = lazy(() => import('../components/QuickOpen'))
|
||||
const WorktreeJumpPalette = lazy(() => import('../components/WorktreeJumpPalette'))
|
||||
const WorkspaceCleanupDialog = lazy(
|
||||
() => import('../components/workspace-cleanup/WorkspaceCleanupDialog')
|
||||
)
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/* oxlint-disable max-lines */
|
||||
import React, {
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
@@ -75,11 +67,12 @@ import { activateAndRevealWorktree } from '@/lib/worktree-activation'
|
||||
import { queueWorkspaceActivationTerminalFocus } from '@/lib/workspace-activation-terminal-focus'
|
||||
import {
|
||||
getWorktreePaletteSearchScope,
|
||||
searchWorktreeDocuments,
|
||||
type MatchRange,
|
||||
type PaletteSearchResult
|
||||
} from '@/lib/worktree-palette-search'
|
||||
import { buildWorktreePaletteDocuments } from '@/lib/worktree-palette-document'
|
||||
import { useCooperativeWorktreePaletteSearch } from './cmd-j/use-cooperative-worktree-palette-search'
|
||||
import { usePaintDeferredValue } from './cmd-j/use-paint-deferred-value'
|
||||
import {
|
||||
resolveWorktreeBranchLabel,
|
||||
resolveWorktreeDisplayName
|
||||
@@ -826,7 +819,7 @@ function WorktreeJumpPaletteContent({
|
||||
const settingsSections = useSettingsNavigationMetadata()
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
const deferredQuery = usePaintDeferredValue(query)
|
||||
const liveQueryRef = useRef(query)
|
||||
liveQueryRef.current = query
|
||||
// Why a ref: creation must follow an explicit ArrowDown/click, and typing re-arms
|
||||
@@ -1211,25 +1204,14 @@ function WorktreeJumpPaletteContent({
|
||||
]
|
||||
)
|
||||
|
||||
const worktreeMatches = useMemo(
|
||||
() =>
|
||||
searchWorktreeDocuments({
|
||||
worktrees: sortedWorktrees,
|
||||
query: paletteSearchQuery,
|
||||
documents: worktreeDocuments,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
checksReviewByWorktree
|
||||
}),
|
||||
[
|
||||
sortedWorktrees,
|
||||
paletteSearchQuery,
|
||||
worktreeDocuments,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
checksReviewByWorktree
|
||||
]
|
||||
)
|
||||
const worktreeMatches = useCooperativeWorktreePaletteSearch({
|
||||
worktrees: sortedWorktrees,
|
||||
query: paletteSearchQuery,
|
||||
documents: worktreeDocuments,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
checksReviewByWorktree
|
||||
})
|
||||
|
||||
const browserPageEntries = useMemo<SearchableBrowserPage[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
searchWorktreeDocuments,
|
||||
searchWorktreeDocumentsCooperatively,
|
||||
type PaletteSearchResult,
|
||||
type WorktreePaletteSearchArgs
|
||||
} from '@/lib/worktree-palette-search'
|
||||
import { parseCmdJTaskSourceUrl } from '@/lib/worktree-palette-task-url-match'
|
||||
|
||||
const COOPERATIVE_SEARCH_MIN_WORKTREES = 200
|
||||
|
||||
type CompletedSearch = {
|
||||
request: WorktreePaletteSearchArgs
|
||||
results: PaletteSearchResult[]
|
||||
}
|
||||
|
||||
export function useCooperativeWorktreePaletteSearch(
|
||||
args: WorktreePaletteSearchArgs
|
||||
): PaletteSearchResult[] {
|
||||
const { worktrees, query, documents, repoMap, repoMapByHostIdentity, checksReviewByWorktree } =
|
||||
args
|
||||
const request = useMemo<WorktreePaletteSearchArgs>(
|
||||
() => ({
|
||||
worktrees,
|
||||
query,
|
||||
documents,
|
||||
repoMap,
|
||||
repoMapByHostIdentity,
|
||||
checksReviewByWorktree
|
||||
}),
|
||||
[worktrees, query, documents, repoMap, repoMapByHostIdentity, checksReviewByWorktree]
|
||||
)
|
||||
const cooperative =
|
||||
request.worktrees.length >= COOPERATIVE_SEARCH_MIN_WORKTREES &&
|
||||
request.query.trim().length > 0 &&
|
||||
parseCmdJTaskSourceUrl(request.query.trim()) === null
|
||||
const immediateResults = useMemo(
|
||||
() => (cooperative ? null : searchWorktreeDocuments(request)),
|
||||
[cooperative, request]
|
||||
)
|
||||
const [completed, setCompleted] = useState<CompletedSearch | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!cooperative) {
|
||||
return
|
||||
}
|
||||
let current = true
|
||||
void searchWorktreeDocumentsCooperatively(request, {
|
||||
shouldContinue: () => current
|
||||
}).then((results) => {
|
||||
if (current && results) {
|
||||
setCompleted({ request, results })
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
current = false
|
||||
}
|
||||
}, [cooperative, request])
|
||||
|
||||
return immediateResults ?? (completed?.request === request ? completed.results : [])
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
|
||||
|
||||
export function usePaintDeferredValue<T>(value: T): T {
|
||||
const [deferred, setDeferred] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.is(value, deferred)) {
|
||||
return
|
||||
}
|
||||
let current = true
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
void yieldToEventLoop().then(() => {
|
||||
if (current) {
|
||||
setDeferred(value)
|
||||
}
|
||||
})
|
||||
})
|
||||
return () => {
|
||||
current = false
|
||||
cancelAnimationFrame(frameId)
|
||||
}
|
||||
}, [deferred, value])
|
||||
|
||||
return deferred
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { normalizePaletteText, type NormalizedText } from './normalized-text'
|
||||
import { isLetterOnlyWord } from './palette-query'
|
||||
import { segmentPaletteText, type PaletteAtom, type PaletteWord } from './text-segments'
|
||||
import type { PaletteMatchQuality } from './match-quality'
|
||||
|
||||
@@ -40,11 +41,46 @@ export type PaletteIndexedField = {
|
||||
text: NormalizedText
|
||||
atoms: readonly PaletteAtom[]
|
||||
words: readonly PaletteWord[]
|
||||
firstAtomByText: ReadonlyMap<string, PaletteAtom>
|
||||
firstWordByText: ReadonlyMap<string, PaletteWord>
|
||||
uniqueAtoms: readonly PaletteAtom[]
|
||||
uniqueWords: readonly PaletteWord[]
|
||||
wordStarts: ReadonlySet<number>
|
||||
typoWordsByLength: ReadonlyMap<number, readonly PaletteWord[]>
|
||||
evidenceId: string | null
|
||||
identifier: PaletteIdentifierOptions | null
|
||||
isContainer: boolean
|
||||
}
|
||||
|
||||
function indexFirstByText<T>(entries: readonly T[], textOf: (entry: T) => string): Map<string, T> {
|
||||
const firstByText = new Map<string, T>()
|
||||
for (const entry of entries) {
|
||||
const text = textOf(entry)
|
||||
if (!firstByText.has(text)) {
|
||||
firstByText.set(text, entry)
|
||||
}
|
||||
}
|
||||
return firstByText
|
||||
}
|
||||
|
||||
function indexTypoWordsByLength(words: readonly PaletteWord[]): Map<number, PaletteWord[]> {
|
||||
const byLength = new Map<number, PaletteWord[]>()
|
||||
const seen = new Set<string>()
|
||||
for (const word of words) {
|
||||
if (word.text.length < 4 || !isLetterOnlyWord(word.text) || seen.has(word.text)) {
|
||||
continue
|
||||
}
|
||||
seen.add(word.text)
|
||||
const bucket = byLength.get(word.text.length)
|
||||
if (bucket) {
|
||||
bucket.push(word)
|
||||
} else {
|
||||
byLength.set(word.text.length, [word])
|
||||
}
|
||||
}
|
||||
return byLength
|
||||
}
|
||||
|
||||
const IDENTIFIER_PREFIX_KINDS: ReadonlySet<PaletteIdentifierKind> = new Set<PaletteIdentifierKind>([
|
||||
'port',
|
||||
'sha',
|
||||
@@ -121,12 +157,22 @@ export function indexPaletteField(source: PaletteFieldSource): PaletteIndexedFie
|
||||
}
|
||||
const text = normalizePaletteText(trimmed)
|
||||
const segments = segmentPaletteText(text)
|
||||
const firstAtomByText = indexFirstByText(segments.atoms, (atom) =>
|
||||
text.normalized.slice(atom.start, atom.end)
|
||||
)
|
||||
const firstWordByText = indexFirstByText(segments.words, (word) => word.text)
|
||||
return {
|
||||
id: source.id,
|
||||
profile: source.profile,
|
||||
text,
|
||||
atoms: segments.atoms,
|
||||
words: segments.words,
|
||||
firstAtomByText,
|
||||
firstWordByText,
|
||||
uniqueAtoms: [...firstAtomByText.values()],
|
||||
uniqueWords: [...firstWordByText.values()],
|
||||
wordStarts: new Set(segments.words.map((word) => word.start)),
|
||||
typoWordsByLength: indexTypoWordsByLength(segments.words),
|
||||
evidenceId: source.evidenceId ?? null,
|
||||
identifier: source.identifier ?? null,
|
||||
isContainer: Boolean(source.isContainer)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
paletteProfileAllowedQualities,
|
||||
type PaletteIndexedField
|
||||
} from './indexed-field'
|
||||
import { isLetterOnlyWord, type PaletteQueryToken } from './palette-query'
|
||||
import type { PaletteQueryToken } from './palette-query'
|
||||
import type { PaletteMatchQuality } from './match-quality'
|
||||
import type { PaletteAtom } from './text-segments'
|
||||
import { isPaletteTypoCandidate, isWithinOnePaletteEdit } from './typo-distance'
|
||||
@@ -56,7 +56,7 @@ function passesSigilGate(field: PaletteIndexedField, token: PaletteQueryToken):
|
||||
}
|
||||
|
||||
function isWordStart(field: PaletteIndexedField, index: number): boolean {
|
||||
return field.words.some((word) => word.start === index) || index === 0
|
||||
return field.wordStarts.has(index) || index === 0
|
||||
}
|
||||
|
||||
function matchAtomComponentRun(atom: PaletteAtom, compact: string): [number, number] | null {
|
||||
@@ -92,11 +92,11 @@ function matchLiteral(
|
||||
return { quality: 'field-exact', ranges: toRanges(field, 0, normalized.length) }
|
||||
}
|
||||
if (qualities.has('word-exact')) {
|
||||
const word = field.words.find((entry) => entry.text === text)
|
||||
const word = field.firstWordByText.get(text)
|
||||
if (word) {
|
||||
return { quality: 'word-exact', ranges: toRanges(field, word.start, word.end) }
|
||||
}
|
||||
const atom = field.atoms.find((entry) => normalized.slice(entry.start, entry.end) === text)
|
||||
const atom = field.firstAtomByText.get(text)
|
||||
if (atom) {
|
||||
return { quality: 'word-exact', ranges: toRanges(field, atom.start, atom.end) }
|
||||
}
|
||||
@@ -105,8 +105,8 @@ function matchLiteral(
|
||||
return { quality: 'field-prefix', ranges: toRanges(field, 0, text.length) }
|
||||
}
|
||||
if (qualities.has('word-prefix')) {
|
||||
const word = field.words.find((entry) => entry.text.startsWith(text))
|
||||
const atom = field.atoms.find((entry) => normalized.startsWith(text, entry.start))
|
||||
const word = field.uniqueWords.find((entry) => entry.text.startsWith(text))
|
||||
const atom = field.uniqueAtoms.find((entry) => normalized.startsWith(text, entry.start))
|
||||
const start = word && atom ? Math.min(word.start, atom.start) : (word?.start ?? atom?.start)
|
||||
if (start !== undefined) {
|
||||
return { quality: 'word-prefix', ranges: toRanges(field, start, start + text.length) }
|
||||
@@ -157,12 +157,11 @@ function matchTypo(
|
||||
if (!qualities.has('typo') || !token.isLetterOnly || !isPaletteTypoCandidate(token.text)) {
|
||||
return null
|
||||
}
|
||||
for (const word of field.words) {
|
||||
if (!isPaletteTypoCandidate(word.text) || !isLetterOnlyWord(word.text)) {
|
||||
continue
|
||||
}
|
||||
if (isWithinOnePaletteEdit(token.text, word.text)) {
|
||||
return { quality: 'typo', ranges: toRanges(field, word.start, word.end) }
|
||||
for (let length = token.text.length - 1; length <= token.text.length + 1; length += 1) {
|
||||
for (const word of field.typoWordsByLength.get(length) ?? []) {
|
||||
if (isWithinOnePaletteEdit(token.text, word.text)) {
|
||||
return { quality: 'typo', ranges: toRanges(field, word.start, word.end) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -23,3 +23,10 @@ export const PALETTE_MATCH_BUDGET = {
|
||||
*/
|
||||
documentPayloadMb: 24
|
||||
} as const
|
||||
|
||||
/** User-visible budgets measured by the built Electron Cmd+J flow. */
|
||||
export const PALETTE_INTERACTION_BUDGET = {
|
||||
openHandlerMs: 50,
|
||||
firstVisibleResultsMs: 100,
|
||||
maxFrameGapMs: 50
|
||||
} as const
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
getWorktreePaletteSearchScope,
|
||||
makeEmptyPaletteSearchResult,
|
||||
searchWorktreeDocumentsCooperatively,
|
||||
searchWorktrees
|
||||
} from './worktree-palette-search'
|
||||
import { buildWorktreePaletteDocuments } from './worktree-palette-document'
|
||||
import {
|
||||
WORKTREE_PALETTE_QUERY_MAX_BYTES,
|
||||
isWorktreePaletteQueryTooLarge
|
||||
@@ -545,4 +547,49 @@ describe('worktree-palette-search', () => {
|
||||
searchWorktrees([worktree], '300', repoMap, { workspacePortsByWorktreeId })
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('cooperatively preserves complete source ordering', async () => {
|
||||
const worktrees = Array.from({ length: 20 }, (_, index) =>
|
||||
makeWorktree({ id: `wt-${index}`, displayName: `Needle ${index}` })
|
||||
)
|
||||
const yieldBetweenSlices = vi.fn(async () => {})
|
||||
const results = await searchWorktreeDocumentsCooperatively(
|
||||
{
|
||||
worktrees,
|
||||
query: 'needle',
|
||||
documents: buildWorktreePaletteDocuments(worktrees, { repoMap }),
|
||||
repoMap
|
||||
},
|
||||
{ timeSliceMs: 0, yieldBetweenSlices }
|
||||
)
|
||||
|
||||
expect(results?.map((result) => result.worktreeId)).toEqual(
|
||||
worktrees.map((worktree) => worktree.id)
|
||||
)
|
||||
expect(yieldBetweenSlices).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('drops a cooperative result when its generation is stale', async () => {
|
||||
const worktrees = Array.from({ length: 20 }, (_, index) =>
|
||||
makeWorktree({ id: `wt-${index}`, displayName: `Needle ${index}` })
|
||||
)
|
||||
let current = true
|
||||
const results = await searchWorktreeDocumentsCooperatively(
|
||||
{
|
||||
worktrees,
|
||||
query: 'needle',
|
||||
documents: buildWorktreePaletteDocuments(worktrees, { repoMap }),
|
||||
repoMap
|
||||
},
|
||||
{
|
||||
timeSliceMs: 0,
|
||||
shouldContinue: () => current,
|
||||
yieldBetweenSlices: async () => {
|
||||
current = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
matchWorktreePaletteTaskUrl,
|
||||
parseCmdJTaskSourceUrl
|
||||
} from './worktree-palette-task-url-match'
|
||||
import { yieldToEventLoop } from '../../../shared/event-loop-yield'
|
||||
|
||||
export type { MatchRange }
|
||||
|
||||
@@ -162,6 +163,45 @@ export type WorktreePaletteSearchArgs = {
|
||||
checksReviewByWorktree?: ReadonlyMap<Worktree, HostedReviewInfo | null>
|
||||
}
|
||||
|
||||
async function yieldToPalettePaint(): Promise<void> {
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
await yieldToEventLoop()
|
||||
return
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
void yieldToEventLoop().then(resolve)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function matchPreparedWorktree(
|
||||
args: WorktreePaletteSearchArgs,
|
||||
worktree: Worktree,
|
||||
prepared: Extract<ReturnType<typeof preparePaletteQuery>, { state: 'ready' }>,
|
||||
taskSourceUrl: ReturnType<typeof parseCmdJTaskSourceUrl>
|
||||
): PaletteSearchResult | null {
|
||||
if (taskSourceUrl) {
|
||||
return matchWorktreePaletteTaskUrl({
|
||||
worktree,
|
||||
intent: taskSourceUrl,
|
||||
repo: resolvePaletteRepoForWorktree(worktree, args.repoMap, args.repoMapByHostIdentity),
|
||||
review: args.checksReviewByWorktree?.get(worktree)
|
||||
})
|
||||
}
|
||||
|
||||
const document = args.documents.get(getWorktreeHostIdentity(worktree))
|
||||
if (!document) {
|
||||
return null
|
||||
}
|
||||
const match = matchPaletteDocument({
|
||||
document,
|
||||
tokens: prepared.tokens,
|
||||
normalizedQuery: prepared.normalized
|
||||
})
|
||||
return match ? toWorktreePaletteSearchResult(worktree.id, match, worktree.hostId) : null
|
||||
}
|
||||
|
||||
/** Matches prepared documents; callers memoize `documents` across keystrokes. */
|
||||
export function searchWorktreeDocuments(args: WorktreePaletteSearchArgs): PaletteSearchResult[] {
|
||||
const prepared = preparePaletteQuery(args.query)
|
||||
@@ -177,35 +217,50 @@ export function searchWorktreeDocuments(args: WorktreePaletteSearchArgs): Palett
|
||||
const taskSourceUrl = parseCmdJTaskSourceUrl(args.query.trim())
|
||||
const results: PaletteSearchResult[] = []
|
||||
for (const worktree of args.worktrees) {
|
||||
if (taskSourceUrl) {
|
||||
const match = matchWorktreePaletteTaskUrl({
|
||||
worktree,
|
||||
intent: taskSourceUrl,
|
||||
repo: resolvePaletteRepoForWorktree(worktree, args.repoMap, args.repoMapByHostIdentity),
|
||||
review: args.checksReviewByWorktree?.get(worktree)
|
||||
})
|
||||
if (match) {
|
||||
results.push(match)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const document = args.documents.get(getWorktreeHostIdentity(worktree))
|
||||
if (!document) {
|
||||
continue
|
||||
}
|
||||
const match = matchPaletteDocument({
|
||||
document,
|
||||
tokens: prepared.tokens,
|
||||
normalizedQuery: prepared.normalized
|
||||
})
|
||||
const match = matchPreparedWorktree(args, worktree, prepared, taskSourceUrl)
|
||||
if (match) {
|
||||
results.push(toWorktreePaletteSearchResult(worktree.id, match, worktree.hostId))
|
||||
results.push(match)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function searchWorktreeDocumentsCooperatively(
|
||||
args: WorktreePaletteSearchArgs,
|
||||
options: {
|
||||
shouldContinue?: () => boolean
|
||||
timeSliceMs?: number
|
||||
yieldBetweenSlices?: () => Promise<void>
|
||||
} = {}
|
||||
): Promise<PaletteSearchResult[] | null> {
|
||||
const prepared = preparePaletteQuery(args.query)
|
||||
if (prepared.state !== 'ready' || parseCmdJTaskSourceUrl(args.query.trim())) {
|
||||
return searchWorktreeDocuments(args)
|
||||
}
|
||||
|
||||
const shouldContinue = options.shouldContinue ?? (() => true)
|
||||
const timeSliceMs = options.timeSliceMs ?? 8
|
||||
const yieldBetweenSlices = options.yieldBetweenSlices ?? yieldToPalettePaint
|
||||
const results: PaletteSearchResult[] = []
|
||||
let sliceStartedAt = performance.now()
|
||||
|
||||
for (const worktree of args.worktrees) {
|
||||
if (!shouldContinue()) {
|
||||
return null
|
||||
}
|
||||
const match = matchPreparedWorktree(args, worktree, prepared, null)
|
||||
if (match) {
|
||||
results.push(match)
|
||||
}
|
||||
if (performance.now() - sliceStartedAt < timeSliceMs) {
|
||||
continue
|
||||
}
|
||||
await yieldBetweenSlices()
|
||||
sliceStartedAt = performance.now()
|
||||
}
|
||||
return shouldContinue() ? results : null
|
||||
}
|
||||
|
||||
/** Convenience entry point that prepares documents inline. */
|
||||
export function searchWorktrees(
|
||||
worktrees: readonly Worktree[],
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import { PALETTE_INTERACTION_BUDGET } from '../../src/renderer/src/lib/palette-match/palette-match-budget'
|
||||
|
||||
const WORKSPACE_COUNT = 800
|
||||
const {
|
||||
openHandlerMs: MAX_OPEN_HANDLER_MS,
|
||||
firstVisibleResultsMs: MAX_FIRST_VISIBLE_MS,
|
||||
maxFrameGapMs: MAX_FRAME_GAP_MS
|
||||
} = PALETTE_INTERACTION_BUDGET
|
||||
const TARGET_QUERY = 'needle 0399'
|
||||
const TARGET_LOCAL_NAME = 'Needle local 0399'
|
||||
const TARGET_REMOTE_NAME = 'Needle remote 0399'
|
||||
|
||||
type InteractionMetrics = {
|
||||
firstVisibleMs: number
|
||||
handlerMs: number | null
|
||||
maxFrameGapMs: number
|
||||
visibleTexts: string[]
|
||||
}
|
||||
|
||||
type CmdJPerformanceProbe = {
|
||||
begin: (expectedTexts: string[]) => void
|
||||
finish: () => InteractionMetrics | null
|
||||
stop: () => void
|
||||
}
|
||||
|
||||
type PerformanceProbeWindow = Window & { __cmdJPerformanceProbe?: CmdJPerformanceProbe }
|
||||
|
||||
async function seedAccumulatedWorkspaceCatalog(page: Page): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ workspaceCount, targetLocalName, targetRemoteName }) => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('window.__store is not available')
|
||||
}
|
||||
const now = Date.now()
|
||||
const sharedRepoId = 'perf-repo'
|
||||
const localRepo = {
|
||||
id: sharedRepoId,
|
||||
path: '/perf/orca',
|
||||
displayName: 'acme/orca-local',
|
||||
badgeColor: '#64748b',
|
||||
addedAt: now,
|
||||
kind: 'git' as const,
|
||||
executionHostId: 'local' as const
|
||||
}
|
||||
const remoteRepo = {
|
||||
...localRepo,
|
||||
path: '/srv/perf/orca',
|
||||
displayName: 'acme/orca-remote',
|
||||
connectionId: 'perf-box',
|
||||
executionHostId: 'ssh:perf-box' as const
|
||||
}
|
||||
const longComment =
|
||||
'Blocked on the staging relay while the execution host reconnects; review the rollback runbook and deployment evidence before retrying. '.repeat(
|
||||
6
|
||||
)
|
||||
const worktrees = Array.from({ length: workspaceCount }, (_, index) => {
|
||||
const pairIndex = Math.floor(index / 2)
|
||||
const suffix = String(pairIndex).padStart(4, '0')
|
||||
const remote = index % 2 === 1
|
||||
const hostId = remote ? ('ssh:perf-box' as const) : ('local' as const)
|
||||
const displayName =
|
||||
pairIndex === 399
|
||||
? remote
|
||||
? targetRemoteName
|
||||
: targetLocalName
|
||||
: `Accumulated ${remote ? 'remote' : 'local'} workspace ${suffix}`
|
||||
return {
|
||||
id: `${sharedRepoId}::/perf/workspace-${suffix}`,
|
||||
instanceId: `perf-instance-${index}`,
|
||||
repoId: sharedRepoId,
|
||||
projectId: 'perf-project',
|
||||
hostId,
|
||||
projectHostSetupId: remote ? 'perf-ssh-setup' : 'perf-local-setup',
|
||||
path: remote ? `/srv/perf/workspace-${suffix}` : `/perf/workspace-${suffix}`,
|
||||
head: index.toString(16).padStart(40, 'a'),
|
||||
branch: `refs/heads/perf/accumulated-${suffix}-${remote ? 'remote' : 'local'}`,
|
||||
isBare: false,
|
||||
isMainWorktree: pairIndex === 0,
|
||||
displayName,
|
||||
comment: `${longComment} Dataset row ${index}.`,
|
||||
linkedIssue: 10_000 + index,
|
||||
linkedPR: 20_000 + index,
|
||||
linkedLinearIssue: `STA-${30_000 + index}`,
|
||||
linkedWorkItem: {
|
||||
provider: index % 4 === 0 ? ('gitlab' as const) : ('linear' as const),
|
||||
type: index % 4 === 0 ? ('mr' as const) : ('issue' as const),
|
||||
number: 30_000 + index,
|
||||
title: `Rework the palette ranking pipeline for accumulated workspace ${index}`,
|
||||
url:
|
||||
index % 4 === 0
|
||||
? `https://gitlab.example/acme/orca/-/merge_requests/${30_000 + index}`
|
||||
: `https://linear.app/acme/issue/STA-${30_000 + index}`,
|
||||
linearIdentifier: index % 4 === 0 ? undefined : `STA-${30_000 + index}`
|
||||
},
|
||||
automationProvenance: {
|
||||
kind: 'created-by-automation' as const,
|
||||
automationId: `perf-auto-${index % 8}`,
|
||||
automationNameSnapshot: 'Nightly accumulated workspace review',
|
||||
automationRunId: `perf-run-${index}`,
|
||||
automationRunTitleSnapshot: `Scan daily sweep ${index}`,
|
||||
createdAt: now - index * 60_000,
|
||||
executionTargetType: remote ? ('ssh' as const) : ('local' as const),
|
||||
executionTargetId: remote ? 'perf-box' : sharedRepoId,
|
||||
projectId: 'perf-project',
|
||||
repoId: sharedRepoId,
|
||||
hostId
|
||||
},
|
||||
isArchived: false,
|
||||
isUnread: index % 7 === 0,
|
||||
isPinned: index % 19 === 0,
|
||||
sortOrder: index,
|
||||
lastActivityAt: now - (workspaceCount - index) * 1_000
|
||||
}
|
||||
})
|
||||
const ports = worktrees.map((worktree, index) => ({
|
||||
id: `perf-port-${index}`,
|
||||
bindHost: '127.0.0.1',
|
||||
connectHost: '127.0.0.1',
|
||||
port: 3_000 + index,
|
||||
pid: 40_000 + index,
|
||||
processName: index % 2 === 0 ? 'node' : 'vite',
|
||||
protocol: 'http' as const,
|
||||
kind: 'workspace' as const,
|
||||
owner: {
|
||||
worktreeId: worktree.id,
|
||||
repoId: worktree.repoId,
|
||||
displayName: worktree.displayName,
|
||||
path: worktree.path,
|
||||
confidence: 'cwd' as const
|
||||
}
|
||||
}))
|
||||
|
||||
store.setState({
|
||||
repos: [localRepo, remoteRepo],
|
||||
activeRepoId: sharedRepoId,
|
||||
worktreesByRepo: { [sharedRepoId]: worktrees },
|
||||
activeWorktreeId: worktrees[0]!.id,
|
||||
activeWorkspaceExecutionHostId: 'local',
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
browserTabsByWorktree: {},
|
||||
browserPagesByWorkspace: {},
|
||||
workspacePortScan: {
|
||||
key: 'perf-800-workspaces',
|
||||
result: { platform: 'darwin', scannedAt: now, ports }
|
||||
},
|
||||
showSleepingWorkspaces: true,
|
||||
hideDefaultBranchWorkspace: false,
|
||||
hideAutomationGeneratedWorkspaces: false,
|
||||
hideCliCreatedWorkspaces: false,
|
||||
hideDetachedHeadWorkspaces: false,
|
||||
hideWorkspacesFromOtherDevices: false,
|
||||
activeView: 'tasks',
|
||||
sidebarOpen: false,
|
||||
rightSidebarOpen: false,
|
||||
activeModal: undefined
|
||||
})
|
||||
},
|
||||
{
|
||||
workspaceCount: WORKSPACE_COUNT,
|
||||
targetLocalName: TARGET_LOCAL_NAME,
|
||||
targetRemoteName: TARGET_REMOTE_NAME
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function installPerformanceProbe(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('window.__store is not available')
|
||||
}
|
||||
let actionStartedAt = 0
|
||||
let expectedTexts: string[] = []
|
||||
let firstVisibleAt: number | null = null
|
||||
let completedMetrics: InteractionMetrics | null = null
|
||||
let handlerMs: number | null = null
|
||||
let maxFrameGapMs = 0
|
||||
let previousFrameAt = performance.now()
|
||||
let visibleTexts: string[] = []
|
||||
let frameId = 0
|
||||
const originalOpenModal = store.getState().openModal
|
||||
|
||||
store.setState({
|
||||
openModal: (...args: Parameters<typeof originalOpenModal>) => {
|
||||
const startedAt = performance.now()
|
||||
originalOpenModal(...args)
|
||||
if (args[0] === 'worktree-palette') {
|
||||
handlerMs = performance.now() - startedAt
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const sampleFrame = (now: number): void => {
|
||||
if (actionStartedAt > 0 && completedMetrics === null) {
|
||||
maxFrameGapMs = Math.max(maxFrameGapMs, now - previousFrameAt)
|
||||
const items = [...document.querySelectorAll<HTMLElement>('[cmdk-item]')].filter(
|
||||
(item) => item.getClientRects().length > 0
|
||||
)
|
||||
visibleTexts = items.map((item) => item.textContent?.replace(/\s+/g, ' ').trim() ?? '')
|
||||
const hasExpectedRows =
|
||||
items.length > 0 &&
|
||||
expectedTexts.every((expected) => visibleTexts.some((text) => text.includes(expected)))
|
||||
if (firstVisibleAt === null && hasExpectedRows) {
|
||||
firstVisibleAt = now
|
||||
completedMetrics = {
|
||||
firstVisibleMs: firstVisibleAt - actionStartedAt,
|
||||
handlerMs,
|
||||
maxFrameGapMs,
|
||||
visibleTexts
|
||||
}
|
||||
}
|
||||
}
|
||||
previousFrameAt = now
|
||||
frameId = requestAnimationFrame(sampleFrame)
|
||||
}
|
||||
frameId = requestAnimationFrame(sampleFrame)
|
||||
|
||||
;(window as PerformanceProbeWindow).__cmdJPerformanceProbe = {
|
||||
begin: (nextExpectedTexts) => {
|
||||
actionStartedAt = performance.now()
|
||||
previousFrameAt = actionStartedAt
|
||||
expectedTexts = nextExpectedTexts
|
||||
firstVisibleAt = null
|
||||
completedMetrics = null
|
||||
maxFrameGapMs = 0
|
||||
visibleTexts = []
|
||||
},
|
||||
finish: () => completedMetrics,
|
||||
stop: () => {
|
||||
cancelAnimationFrame(frameId)
|
||||
store.setState({ openModal: originalOpenModal })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function beginProbe(page: Page, expectedTexts: string[]): Promise<void> {
|
||||
await page.evaluate(
|
||||
(texts) => (window as PerformanceProbeWindow).__cmdJPerformanceProbe?.begin(texts),
|
||||
expectedTexts
|
||||
)
|
||||
}
|
||||
|
||||
async function readMetrics(page: Page): Promise<InteractionMetrics | null> {
|
||||
return page.evaluate(
|
||||
() => (window as PerformanceProbeWindow).__cmdJPerformanceProbe?.finish() ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async function togglePaletteFromMain(electronApp: ElectronApplication): Promise<void> {
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
|
||||
if (!mainWindow) {
|
||||
throw new Error('Orca main window is not available')
|
||||
}
|
||||
mainWindow.webContents.send('ui:toggleWorktreePalette')
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Cmd-J cold accumulated-workspace performance', () => {
|
||||
test.beforeEach(async ({ orcaPage }) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
})
|
||||
|
||||
test('paints complete host-qualified results without dropping a frame', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
const startupTiming = await orcaPage.evaluate(() => ({
|
||||
rendererReadyMs: performance.now(),
|
||||
firstContentfulPaintMs:
|
||||
performance.getEntriesByName('first-contentful-paint')[0]?.startTime ?? null
|
||||
}))
|
||||
await seedAccumulatedWorkspaceCatalog(orcaPage)
|
||||
await installPerformanceProbe(orcaPage)
|
||||
await orcaPage.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
||||
)
|
||||
)
|
||||
|
||||
await beginProbe(orcaPage, [])
|
||||
await togglePaletteFromMain(electronApp)
|
||||
const dialog = orcaPage.getByRole('dialog', { name: 'Jump to...' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect.poll(() => readMetrics(orcaPage), { timeout: 10_000 }).not.toBeNull()
|
||||
const coldOpen = await readMetrics(orcaPage)
|
||||
expect(coldOpen).not.toBeNull()
|
||||
|
||||
await beginProbe(orcaPage, [
|
||||
TARGET_REMOTE_NAME,
|
||||
TARGET_LOCAL_NAME,
|
||||
`Create worktree "${TARGET_QUERY}"`
|
||||
])
|
||||
await dialog.getByPlaceholder(/Search chats, terminals, worktrees/).fill(TARGET_QUERY)
|
||||
await expect.poll(() => readMetrics(orcaPage), { timeout: 10_000 }).not.toBeNull()
|
||||
const immediateQuery = await readMetrics(orcaPage)
|
||||
expect(immediateQuery).not.toBeNull()
|
||||
|
||||
const matchingRows = dialog
|
||||
.locator('[cmdk-item]:has([data-slot="palette-worktree-name"])')
|
||||
.filter({ hasText: 'Needle' })
|
||||
await expect(matchingRows).toHaveCount(2)
|
||||
const matchingTexts = (await matchingRows.allTextContents()).map((text) =>
|
||||
text.replace(/\s+/g, ' ').trim()
|
||||
)
|
||||
expect(matchingTexts[0]).toContain(TARGET_REMOTE_NAME)
|
||||
expect(matchingTexts[1]).toContain(TARGET_LOCAL_NAME)
|
||||
|
||||
await togglePaletteFromMain(electronApp)
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await orcaPage.waitForTimeout(400)
|
||||
await beginProbe(orcaPage, ['Accumulated remote workspace 0398'])
|
||||
await togglePaletteFromMain(electronApp)
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect.poll(() => readMetrics(orcaPage), { timeout: 10_000 }).not.toBeNull()
|
||||
const warmReopen = await readMetrics(orcaPage)
|
||||
expect(warmReopen).not.toBeNull()
|
||||
|
||||
const report = {
|
||||
startupTiming,
|
||||
coldOpen,
|
||||
immediateQuery,
|
||||
warmReopen,
|
||||
workspaceCount: WORKSPACE_COUNT
|
||||
}
|
||||
await testInfo.attach('cmd-j-cold-open-metrics.json', {
|
||||
body: Buffer.from(`${JSON.stringify(report, null, 2)}\n`),
|
||||
contentType: 'application/json'
|
||||
})
|
||||
await testInfo.attach('cmd-j-cold-open-visible.png', {
|
||||
body: await orcaPage.screenshot(),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
console.log(`[cmd-j-cold-open] ${JSON.stringify(report)}`)
|
||||
|
||||
expect(coldOpen!.handlerMs).not.toBeNull()
|
||||
expect(coldOpen!.handlerMs!).toBeLessThanOrEqual(MAX_OPEN_HANDLER_MS)
|
||||
expect(coldOpen!.firstVisibleMs).toBeLessThanOrEqual(MAX_FIRST_VISIBLE_MS)
|
||||
expect(coldOpen!.maxFrameGapMs).toBeLessThanOrEqual(MAX_FRAME_GAP_MS)
|
||||
expect(immediateQuery!.firstVisibleMs).toBeLessThanOrEqual(MAX_FIRST_VISIBLE_MS)
|
||||
expect(immediateQuery!.maxFrameGapMs).toBeLessThanOrEqual(MAX_FRAME_GAP_MS)
|
||||
expect(warmReopen!.firstVisibleMs).toBeLessThanOrEqual(MAX_FIRST_VISIBLE_MS)
|
||||
expect(warmReopen!.maxFrameGapMs).toBeLessThanOrEqual(MAX_FRAME_GAP_MS)
|
||||
|
||||
await orcaPage.evaluate(() => (window as PerformanceProbeWindow).__cmdJPerformanceProbe?.stop())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user