diff --git a/config/scripts/mobile-file-ranking-benchmark.mjs b/config/scripts/mobile-file-ranking-benchmark.mjs new file mode 100644 index 00000000000..68ac5b9d977 --- /dev/null +++ b/config/scripts/mobile-file-ranking-benchmark.mjs @@ -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 ') +} +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)) diff --git a/mobile/src/session/mobile-native-chat-autocomplete.ts b/mobile/src/session/mobile-native-chat-autocomplete.ts index 548d0614837..8de107c9fb0 100644 --- a/mobile/src/session/mobile-native-chat-autocomplete.ts +++ b/mobile/src/session/mobile-native-chat-autocomplete.ts @@ -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)) { diff --git a/src/main/runtime/runtime-mobile-file-path-search.test.ts b/src/main/runtime/runtime-mobile-file-path-search.test.ts index 495a7933842..0be5ecad50e 100644 --- a/src/main/runtime/runtime-mobile-file-path-search.test.ts +++ b/src/main/runtime/runtime-mobile-file-path-search.test.ts @@ -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( diff --git a/src/main/runtime/runtime-mobile-file-path-search.ts b/src/main/runtime/runtime-mobile-file-path-search.ts index 13213d6dfcf..1455028c798 100644 --- a/src/main/runtime/runtime-mobile-file-path-search.ts +++ b/src/main/runtime/runtime-mobile-file-path-search.ts @@ -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) {