mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(mobile): list Source Control changes with desktop's sections, sort and row rules Conflicts get their own section in the user's group order, submodule rows lose stage and discard like desktop, paths sort with the shared collator, and review notes go out as desktop formats them. * fix(mobile): read the group order the host now publishes and keep submodule rows read-only The host's paired settings projection now carries sourceControlGroupOrder, the normalized status projection keeps submodule metadata, and conflict rows reuse the shared conflict-kind labels. * refactor(mobile): share Source Control grouping and row rules with desktop Desktop and mobile group rows through one shared helper, mobile's unstage button follows desktop's rule, and the group order resets when the host changes.
111 lines
4.1 KiB
JavaScript
111 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// Compare per-comparison localeCompare options with hoisted Intl.Collator on Source Control refresh; assert identical sorted output before timing.
|
|
import { execFileSync } from 'node:child_process'
|
|
import { build } from 'esbuild'
|
|
import { performance } from 'node:perf_hooks'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ITERATIONS = Number(process.env.ORCA_SC_SORT_BENCH_ITERATIONS ?? '25')
|
|
const WARMUP = Number(process.env.ORCA_SC_SORT_BENCH_WARMUP ?? '5')
|
|
|
|
for (const [name, value] of [
|
|
['ORCA_SC_SORT_BENCH_ITERATIONS', ITERATIONS],
|
|
['ORCA_SC_SORT_BENCH_WARMUP', WARMUP]
|
|
]) {
|
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
throw new Error(`${name} must be a positive integer, received ${value}`)
|
|
}
|
|
}
|
|
|
|
function conflictRank(entry) {
|
|
if (entry.conflictStatus === 'unresolved') {
|
|
return 0
|
|
}
|
|
if (entry.conflictStatus === 'resolved_locally') {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
|
|
// Per-call baseline preserves production's locale and deterministic numeric ties.
|
|
function compareBefore(a, b) {
|
|
return (
|
|
conflictRank(a) - conflictRank(b) ||
|
|
a.path.localeCompare(b.path, 'en', { numeric: true }) ||
|
|
(a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
)
|
|
}
|
|
|
|
const { outputFiles } = await build({
|
|
entryPoints: [fileURLToPath(new URL('../../src/shared/git-status-sort.ts', import.meta.url))],
|
|
bundle: true,
|
|
platform: 'node',
|
|
format: 'esm',
|
|
write: false
|
|
})
|
|
const { compareGitStatusEntries: compareAfter } = await import(
|
|
`data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}`
|
|
)
|
|
|
|
// Use real byte-sorted Git paths: shuffled input inflates win over nearly sorted production arrays.
|
|
const REPO_PATHS = execFileSync('git', ['ls-files', '-z'], {
|
|
cwd: fileURLToPath(new URL('../..', import.meta.url)),
|
|
maxBuffer: 256 * 1024 * 1024
|
|
})
|
|
.toString()
|
|
.split('\0')
|
|
.filter(Boolean)
|
|
|
|
function makeEntries(count) {
|
|
const step = Math.max(1, Math.floor(REPO_PATHS.length / count))
|
|
const paths = []
|
|
for (let index = 0; index < REPO_PATHS.length && paths.length < count; index += step) {
|
|
paths.push(REPO_PATHS[index])
|
|
}
|
|
return paths.map((path, index) => ({
|
|
path,
|
|
area: 'unstaged',
|
|
status: 'modified',
|
|
...(index % 37 === 0 ? { conflictStatus: 'unresolved' } : {})
|
|
}))
|
|
}
|
|
|
|
function measure(compare, entries) {
|
|
for (let index = 0; index < WARMUP; index += 1) {
|
|
;[...entries].sort(compare)
|
|
}
|
|
const samples = []
|
|
for (let round = 0; round < 5; round += 1) {
|
|
const start = performance.now()
|
|
for (let index = 0; index < ITERATIONS; index += 1) {
|
|
;[...entries].sort(compare)
|
|
}
|
|
samples.push((performance.now() - start) / ITERATIONS)
|
|
}
|
|
samples.sort((a, b) => a - b)
|
|
return samples[2]
|
|
}
|
|
|
|
const pad = (value, width) => String(value).padStart(width)
|
|
console.log('Source Control changed-file sort, per git refresh. Lower is better.')
|
|
console.log(`iterations=${ITERATIONS} warmup=${WARMUP} (median of 5 rounds)`)
|
|
console.log(`${pad('files', 7)} ${pad('per-call', 11)} ${pad('hoisted', 11)} ${pad('speedup', 9)}`)
|
|
|
|
// Fixture sizes follow 7,324 non-merge commits: p50 3, p75 7, p90 17, p95 26, p99 63, max 1626.
|
|
for (const count of [3, 17, 26, 63, 308, 1000]) {
|
|
const entries = makeEntries(count)
|
|
const before = [...entries].sort(compareBefore).map((entry) => entry.path)
|
|
const after = [...entries].sort(compareAfter).map((entry) => entry.path)
|
|
if (before.length !== after.length || before.some((path, index) => path !== after[index])) {
|
|
throw new Error(`sort order differs at ${count} files`)
|
|
}
|
|
const beforeMs = measure(compareBefore, entries)
|
|
const afterMs = measure(compareAfter, entries)
|
|
console.log(
|
|
`${pad(count, 7)} ${pad(`${beforeMs.toFixed(3)} ms`, 11)} ${pad(`${afterMs.toFixed(3)} ms`, 11)} ${pad(`${(beforeMs / afterMs).toFixed(1)}x`, 9)}`
|
|
)
|
|
}
|
|
console.log(
|
|
'\nSizes are the real changed-file distribution over 7,324 non-merge commits on\nthis repo (p50 3, p90 17, p95 26, p99 63), so the top rows are the common case.\nThis times the sort alone; the sort is roughly 85% of the Source Control\nprojection chain, so the end-to-end memo win is smaller than these ratios.'
|
|
)
|