mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
perf(renderer): reuse locale collators (#13444)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
// Benchmarks optioned localeCompare calls used by renderer list comparators.
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createJiti } from 'jiti'
|
||||
|
||||
const ROUND_COUNT = 5
|
||||
const MIN_ROUND_MS = 120
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
alias: { '@': fileURLToPath(new URL('../../src/renderer/src', import.meta.url)) }
|
||||
})
|
||||
const { compareBaseSensitivityLocaleText } = await jiti.import(
|
||||
'../../src/renderer/src/lib/locale-text-collators.ts'
|
||||
)
|
||||
const { sortJiraIssues } = await jiti.import(
|
||||
'../../src/renderer/src/components/jira-issue-sorter.ts'
|
||||
)
|
||||
|
||||
let randomState = 0x9e3779b9
|
||||
function random() {
|
||||
randomState = (Math.imul(randomState, 1664525) + 1013904223) >>> 0
|
||||
return randomState / 0x100000000
|
||||
}
|
||||
|
||||
function shuffle(values) {
|
||||
for (let index = values.length - 1; index > 0; index -= 1) {
|
||||
const swapIndex = Math.floor(random() * (index + 1))
|
||||
;[values[index], values[swapIndex]] = [values[swapIndex], values[index]]
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
const labels = ['alpha', 'Álpha', 'beta', 'BÉTA', 'café', 'Cafe', 'zeta', 'Ångström']
|
||||
|
||||
function makeJiraIssues(count) {
|
||||
return shuffle(
|
||||
Array.from({ length: count }, (_, index) => ({
|
||||
key: `TASK-${(index * 37) % (count + 17)}`,
|
||||
title: labels[index % labels.length],
|
||||
updatedAt: '2026-01-01T00:00:00.000Z'
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
function makeBaseSensitivityValues(count) {
|
||||
return shuffle(
|
||||
Array.from(
|
||||
{ length: count },
|
||||
(_, index) =>
|
||||
`${labels[(index * 5) % labels.length]}-${String((index * 41) % (count + 23)).padStart(4, '0')}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function calibrate(run) {
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
run()
|
||||
}
|
||||
let iterations = 1
|
||||
while (true) {
|
||||
const startedAt = performance.now()
|
||||
for (let index = 0; index < iterations; index += 1) {
|
||||
run()
|
||||
}
|
||||
if (performance.now() - startedAt >= MIN_ROUND_MS) {
|
||||
break
|
||||
}
|
||||
iterations *= 2
|
||||
}
|
||||
return iterations
|
||||
}
|
||||
|
||||
function measureRound(run, iterations) {
|
||||
const startedAt = performance.now()
|
||||
for (let index = 0; index < iterations; index += 1) {
|
||||
run()
|
||||
}
|
||||
return (performance.now() - startedAt) / iterations
|
||||
}
|
||||
|
||||
function measurePair(before, after) {
|
||||
const beforeIterations = calibrate(before)
|
||||
const afterIterations = calibrate(after)
|
||||
const beforeSamples = []
|
||||
const afterSamples = []
|
||||
for (let round = 0; round < ROUND_COUNT; round += 1) {
|
||||
if (round % 2 === 0) {
|
||||
beforeSamples.push(measureRound(before, beforeIterations))
|
||||
afterSamples.push(measureRound(after, afterIterations))
|
||||
} else {
|
||||
afterSamples.push(measureRound(after, afterIterations))
|
||||
beforeSamples.push(measureRound(before, beforeIterations))
|
||||
}
|
||||
}
|
||||
const middle = Math.floor(ROUND_COUNT / 2)
|
||||
return {
|
||||
beforeMs: beforeSamples.sort((a, b) => a - b)[middle],
|
||||
afterMs: afterSamples.sort((a, b) => a - b)[middle]
|
||||
}
|
||||
}
|
||||
|
||||
function assertSameOrder(before, after, label) {
|
||||
const expected = before()
|
||||
const actual = after()
|
||||
if (
|
||||
expected.length !== actual.length ||
|
||||
expected.some((value, index) => value !== actual[index])
|
||||
) {
|
||||
throw new Error(`${label} sort order changed`)
|
||||
}
|
||||
}
|
||||
|
||||
const pad = (value, width) => String(value).padStart(width)
|
||||
console.log('Renderer locale sort, ms per sort (median of 5 rounds). Lower is better.')
|
||||
console.log(
|
||||
`${pad('mode', 9)} ${pad('items', 7)} ${pad('per-call', 11)} ${pad('reused', 11)} ${pad('speedup', 9)}`
|
||||
)
|
||||
|
||||
for (const count of [36, 50, 250]) {
|
||||
const issues = makeJiraIssues(count)
|
||||
const before = () =>
|
||||
[...issues]
|
||||
.sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }))
|
||||
.map((issue) => issue.key)
|
||||
const after = () => sortJiraIssues(issues, 'key', 'asc').map((issue) => issue.key)
|
||||
assertSameOrder(before, after, `numeric ${count}`)
|
||||
const { beforeMs, afterMs } = measurePair(before, after)
|
||||
console.log(
|
||||
`${pad('numeric', 9)} ${pad(count, 7)} ${pad(`${beforeMs.toFixed(3)} ms`, 11)} ${pad(`${afterMs.toFixed(3)} ms`, 11)} ${pad(`${(beforeMs / afterMs).toFixed(1)}x`, 9)}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const count of [10, 50, 250]) {
|
||||
const values = makeBaseSensitivityValues(count)
|
||||
const before = () =>
|
||||
[...values].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||
const after = () => [...values].sort(compareBaseSensitivityLocaleText)
|
||||
assertSameOrder(before, after, `base ${count}`)
|
||||
const { beforeMs, afterMs } = measurePair(before, after)
|
||||
console.log(
|
||||
`${pad('base', 9)} ${pad(count, 7)} ${pad(`${beforeMs.toFixed(3)} ms`, 11)} ${pad(`${afterMs.toFixed(3)} ms`, 11)} ${pad(`${(beforeMs / afterMs).toFixed(1)}x`, 9)}`
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'\n36 rows matches the Linear page size, 50 matches the picker/Jira scale, and\n250 is a stress case. Both arms assert identical output before timing.'
|
||||
)
|
||||
@@ -43,6 +43,7 @@ import { useAllWorktrees, useRepoMap } from '@/store/selectors'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
|
||||
import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
|
||||
import { compareNumericLocaleText } from '@/lib/locale-text-collators'
|
||||
import {
|
||||
getSettingsFocusedExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
@@ -846,7 +847,7 @@ function compareLinearIssues(a: LinearIssue, b: LinearIssue, orderBy: LinearOrde
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
}
|
||||
if (orderBy === 'identifier') {
|
||||
return a.identifier.localeCompare(b.identifier, undefined, { numeric: true })
|
||||
return compareNumericLocaleText(a.identifier, b.identifier)
|
||||
}
|
||||
|
||||
const priorityDelta = getLinearPriorityRank(a.priority) - getLinearPriorityRank(b.priority)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JiraIssue, JiraPriority } from '../../../shared/types'
|
||||
import { compareNumericLocaleText } from '@/lib/locale-text-collators'
|
||||
|
||||
export type JiraIssueSortColumn = 'key' | 'title' | 'status' | 'priority' | 'assignee' | 'updated'
|
||||
|
||||
@@ -57,7 +58,7 @@ export function sortJiraIssues(
|
||||
return [...issues].sort((a, b) => {
|
||||
let comparison = 0
|
||||
if (orderBy === 'key') {
|
||||
comparison = a.key.localeCompare(b.key, undefined, { numeric: true })
|
||||
comparison = compareNumericLocaleText(a.key, b.key)
|
||||
} else if (orderBy === 'title') {
|
||||
comparison = a.title.localeCompare(b.title)
|
||||
} else if (orderBy === 'status') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isSafeDisplayCharacter,
|
||||
stripUnsafeDisplayCharacters
|
||||
} from '../../../../shared/skill-display-text'
|
||||
import { compareBaseSensitivityLocaleText } from '@/lib/locale-text-collators'
|
||||
|
||||
// Send classification lives in shared so mobile gates optimistic echoes with
|
||||
// the same rules; re-exported here to keep renderer import paths stable.
|
||||
@@ -191,7 +192,7 @@ function sanitizePickerText(value: string, maxLength: number): string {
|
||||
function compareDiscoveredSkills(a: DiscoveredSkill, b: DiscoveredSkill): number {
|
||||
return (
|
||||
SCOPE_PRIORITY[a.sourceKind] - SCOPE_PRIORITY[b.sourceKind] ||
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) ||
|
||||
compareBaseSensitivityLocaleText(a.name, b.name) ||
|
||||
a.skillFilePath.localeCompare(b.skillFilePath)
|
||||
)
|
||||
}
|
||||
@@ -202,7 +203,7 @@ function comparePickerSkills(
|
||||
): number {
|
||||
return (
|
||||
SCOPE_PRIORITY[a.sources[0].sourceKind] - SCOPE_PRIORITY[b.sources[0].sourceKind] ||
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
compareBaseSensitivityLocaleText(a.name, b.name)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ORCA_BROWSER_BLANK_URL } from '../../../shared/constants'
|
||||
import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text'
|
||||
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
|
||||
import { resolveWorktreeDisplayName } from './worktree-default-display-name'
|
||||
import type { MatchRange } from './worktree-palette-search'
|
||||
|
||||
@@ -43,7 +44,7 @@ export function isBrowserPaletteQueryTooLarge(
|
||||
}
|
||||
|
||||
function compareText(a: string, b: string): number {
|
||||
return a.localeCompare(b, undefined, { sensitivity: 'base' })
|
||||
return compareBaseSensitivityLocaleText(a, b)
|
||||
}
|
||||
|
||||
export function isBlankBrowserUrl(url: string): boolean {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { compareBaseSensitivityLocaleText, compareNumericLocaleText } from './locale-text-collators'
|
||||
|
||||
function expectComparatorParity(
|
||||
values: readonly string[],
|
||||
legacy: (a: string, b: string) => number,
|
||||
current: (a: string, b: string) => number
|
||||
): void {
|
||||
for (const a of values) {
|
||||
for (const b of values) {
|
||||
expect(Math.sign(current(a, b))).toBe(Math.sign(legacy(a, b)))
|
||||
}
|
||||
}
|
||||
expect([...values].sort(current)).toEqual([...values].sort(legacy))
|
||||
}
|
||||
|
||||
describe('locale text collators', () => {
|
||||
it('preserves base-sensitivity localeCompare ordering', () => {
|
||||
const values = [
|
||||
'zeta',
|
||||
'Zeta',
|
||||
'Álpha',
|
||||
'alpha',
|
||||
'BÉTA',
|
||||
'beta',
|
||||
'café',
|
||||
'Cafe',
|
||||
'a-b',
|
||||
'a b',
|
||||
'a_b',
|
||||
'Ångström',
|
||||
'alpha'
|
||||
]
|
||||
const legacy = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: 'base' })
|
||||
|
||||
expectComparatorParity(values, legacy, compareBaseSensitivityLocaleText)
|
||||
})
|
||||
|
||||
it('preserves numeric localeCompare ordering and ties', () => {
|
||||
const values = [
|
||||
'TASK-10',
|
||||
'TASK-2',
|
||||
'TASK-02',
|
||||
'TASK-1',
|
||||
'TASK-20',
|
||||
'TASK-11',
|
||||
'TASK_2',
|
||||
'task-2',
|
||||
'TASK-2'
|
||||
]
|
||||
const legacy = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true })
|
||||
|
||||
expectComparatorParity(values, legacy, compareNumericLocaleText)
|
||||
})
|
||||
|
||||
it('constructs each collator only when its comparison mode is first used', async () => {
|
||||
vi.resetModules()
|
||||
const NativeCollator = Intl.Collator
|
||||
const collatorSpy = vi
|
||||
.spyOn(Intl, 'Collator')
|
||||
.mockImplementation(function Collator(locales, options) {
|
||||
return new NativeCollator(locales, options)
|
||||
})
|
||||
try {
|
||||
const comparison = await import('./locale-text-collators')
|
||||
expect(collatorSpy).not.toHaveBeenCalled()
|
||||
comparison.compareNumericLocaleText('TASK-2', 'TASK-10')
|
||||
comparison.compareNumericLocaleText('TASK-3', 'TASK-11')
|
||||
expect(collatorSpy).toHaveBeenCalledTimes(1)
|
||||
comparison.compareBaseSensitivityLocaleText('café', 'Cafe')
|
||||
comparison.compareBaseSensitivityLocaleText('Álpha', 'alpha')
|
||||
expect(collatorSpy).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
collatorSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
let baseSensitivityCollator: Intl.Collator | undefined
|
||||
let numericCollator: Intl.Collator | undefined
|
||||
|
||||
export function compareBaseSensitivityLocaleText(a: string, b: string): number {
|
||||
// Why: stay lazy like localeCompare while resolving ICU options only once.
|
||||
baseSensitivityCollator ??= new Intl.Collator(undefined, { sensitivity: 'base' })
|
||||
return baseSensitivityCollator.compare(a, b)
|
||||
}
|
||||
|
||||
export function compareNumericLocaleText(a: string, b: string): number {
|
||||
numericCollator ??= new Intl.Collator(undefined, { numeric: true })
|
||||
return numericCollator.compare(a, b)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { Tab, TabGroup, Worktree } from '../../../shared/types'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text'
|
||||
import { selectPaletteTypeAliasMatch } from './palette-type-alias-match'
|
||||
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
|
||||
import { resolveWorktreeDisplayName } from './worktree-default-display-name'
|
||||
import type { MatchRange } from './worktree-palette-search'
|
||||
|
||||
@@ -67,7 +68,7 @@ export type BuildSearchableSimulatorTabsOptions = {
|
||||
}
|
||||
|
||||
function compareText(a: string, b: string): number {
|
||||
return a.localeCompare(b, undefined, { sensitivity: 'base' })
|
||||
return compareBaseSensitivityLocaleText(a, b)
|
||||
}
|
||||
|
||||
function findRange(text: string, query: string): MatchRange | null {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { selectPaletteTypeAliasMatch } from './palette-type-alias-match'
|
||||
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
|
||||
import { resolveWorktreeDisplayName } from './worktree-default-display-name'
|
||||
import type { MatchRange } from './worktree-palette-search'
|
||||
import type {
|
||||
@@ -27,7 +28,7 @@ export type WorkspaceTabPaletteSearchResult = {
|
||||
}
|
||||
|
||||
function compareText(a: string, b: string): number {
|
||||
return a.localeCompare(b, undefined, { sensitivity: 'base' })
|
||||
return compareBaseSensitivityLocaleText(a, b)
|
||||
}
|
||||
|
||||
function findRange(text: string, query: string): MatchRange | null {
|
||||
|
||||
Reference in New Issue
Block a user