perf: avoid splitting every path during file autocomplete (#18919)

This commit is contained in:
Neil
2026-09-05 20:03:08 -07:00
committed by GitHub
parent 295684dc6d
commit 4e8e14424d
4 changed files with 86 additions and 2 deletions
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
const baseline = process.argv[2]
if (!baseline) {
throw new Error('Usage: node config/scripts/mobile-file-ranking-benchmark.mjs <baseline-ref>')
}
async function load(source) {
const js = stripTypeScriptTypes(source, { mode: 'transform' })
return await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
}
function measure(fn, paths, query) {
for (let warmup = 0; warmup < 10; warmup++) {
fn(paths, query, 16)
}
const samples = []
for (let i = 0; i < 9; i++) {
const start = performance.now()
fn(paths, query, 16)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[4]
}
const results = []
for (const [file, name] of [
['src/main/runtime/runtime-mobile-file-path-search.ts', 'rankRuntimeMobileFilePaths'],
['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSuggestions']
]) {
const before = (
await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }))
)[name]
const after = (await load(readFileSync(file, 'utf8')))[name]
for (const count of [100, 100000]) {
const paths = Array.from(
{ length: count },
(_, i) => `src/components/workspace/group-${i % 100}/file-${i}.tsx`
)
for (const query of ['file-9', 'missing', 'workspace']) {
assert.deepEqual(after(paths, query, 16), before(paths, query, 16))
results.push({
function: name,
paths: count,
query,
beforeMs: measure(before, paths, query),
afterMs: measure(after, paths, query)
})
}
}
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -81,7 +81,7 @@ export function rankSuggestions(candidates: readonly string[], query: string, li
const substring: string[] = []
for (const candidate of candidates) {
const lower = candidate.toLowerCase()
const base = lower.split('/').pop() ?? lower
const base = lower.slice(lower.lastIndexOf('/') + 1)
if (lower.startsWith(q) || base.startsWith(q)) {
prefix.push(candidate)
} else if (lower.includes(q)) {
@@ -6,6 +6,37 @@ import {
} from './runtime-mobile-file-path-search'
describe('rankRuntimeMobileFilePaths', () => {
it('preserves basename matching, ordering and total counts for unusual paths', () => {
const paths = [
'',
'/',
'a/',
'a//b.ts',
'b.ts',
'B.TS',
'a\\b.ts',
'界/😀.ts',
'.hidden',
'a/./b.ts'
]
for (const query of ['', ' ', 'b', '.ts', '😀', '/', 'a\\', 'missing']) {
for (const limit of [0, 1, 3, 100]) {
const q = query.trim().toLowerCase()
const prefix = paths.filter((path) => {
const lower = path.toLowerCase()
return lower.startsWith(q) || (lower.split('/').pop() ?? lower).startsWith(q)
})
const other = paths.filter(
(path) => !prefix.includes(path) && path.toLowerCase().includes(q)
)
expect(rankRuntimeMobileFilePaths(paths, query, limit)).toEqual({
paths: [...prefix, ...other].slice(0, limit),
totalCount: prefix.length + other.length
})
}
}
})
it('ranks path and basename prefixes before substrings and caps output', () => {
expect(
rankRuntimeMobileFilePaths(
@@ -76,7 +76,7 @@ export function rankRuntimeMobileFilePaths(
let totalCount = 0
for (const path of paths) {
const lower = path.toLowerCase()
const basename = lower.split('/').pop() ?? lower
const basename = lower.slice(lower.lastIndexOf('/') + 1)
if (lower.startsWith(normalizedQuery) || basename.startsWith(normalizedQuery)) {
totalCount++
if (prefix.length < limit) {