feat(perf): audit comparator setup and schedule performance contracts

This commit is contained in:
Neil
2026-09-05 02:56:09 -07:00
parent 1a76a11e39
commit 538ca76515
8 changed files with 265 additions and 1 deletions
@@ -0,0 +1,48 @@
name: Performance contracts
on:
schedule:
- cron: '15 9 * * *'
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/performance-contracts.yml'
- 'config/vitest.performance.config.ts'
- 'config/oxlint-performance-audit.json'
- 'config/oxlint-plugins/*performance.mjs'
- 'config/scripts/*performance-plugin.test.mjs'
permissions:
contents: read
concurrency:
group: performance-contracts-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
contracts:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- name: Run operation-count and retention contracts
run: pnpm test:perf:contracts --reporter=default --reporter=json --outputFile=performance-contracts.json
- name: Audit production performance patterns
if: always()
shell: bash
run: pnpm --silent audit:perf > performance-audit.json
- uses: actions/upload-artifact@v7
if: always()
with:
name: performance-contracts-${{ matrix.os }}
path: |
performance-contracts.json
performance-audit.json
if-no-files-found: error
+5
View File
@@ -2,6 +2,10 @@
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "react", "react-hooks", "react-perf", "unicorn"],
"jsPlugins": [
{
"name": "sort-comparator-performance",
"specifier": "./config/oxlint-plugins/sort-comparator-performance.mjs"
},
{
"name": "mobile-pairing",
"specifier": "./config/oxlint-plugins/mobile-pairing-qrcode-import.mjs"
@@ -23,6 +27,7 @@
"correctness": "error"
},
"rules": {
"sort-comparator-performance/no-repeated-collator": "warn",
"app-store-performance/require-selector": "error",
"app-store-performance/no-identity-selector": "error",
"app-store-performance/no-fresh-selector-result": "error",
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "../node_modules/oxlint/configuration_schema.json",
"plugins": [],
"categories": {
"correctness": "off",
"suspicious": "off",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"jsPlugins": [
{
"name": "app-store-performance",
"specifier": "../config/oxlint-plugins/app-store-performance.mjs"
},
{
"name": "quadratic-buffer-concat",
"specifier": "../config/oxlint-plugins/quadratic-buffer-concat.mjs"
},
{
"name": "sort-comparator-performance",
"specifier": "../config/oxlint-plugins/sort-comparator-performance.mjs"
}
],
"rules": {
"app-store-performance/require-selector": "warn",
"app-store-performance/no-identity-selector": "warn",
"app-store-performance/no-fresh-selector-result": "warn",
"quadratic-buffer-concat/no-loop-carried-concat": "warn",
"sort-comparator-performance/no-repeated-collator": "warn"
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/out", "**/*.test.*", "**/*.spec.*"]
}
@@ -0,0 +1,60 @@
const FUNCTION_TYPES = new Set([
'ArrowFunctionExpression',
'FunctionExpression',
'FunctionDeclaration'
])
function propertyName(node) {
if (node?.type !== 'MemberExpression') {
return null
}
if (!node.computed && node.property.type === 'Identifier') {
return node.property.name
}
return node.property.type === 'Literal' ? node.property.value : null
}
function isInlineSortComparator(node) {
for (let parent = node.parent; parent; parent = parent.parent) {
if (!FUNCTION_TYPES.has(parent.type)) {
continue
}
const call = parent.parent
return (
call?.type === 'CallExpression' &&
call.arguments[0] === parent &&
['sort', 'toSorted'].includes(propertyName(call.callee))
)
}
return false
}
function isCollatorConstruction(node) {
return (
node.callee?.object?.type === 'Identifier' &&
node.callee.object.name === 'Intl' &&
propertyName(node.callee) === 'Collator'
)
}
function createRule(context) {
function inspect(node) {
const optionedComparison =
node.type === 'CallExpression' &&
propertyName(node.callee) === 'localeCompare' &&
node.arguments.length >= 3
if ((optionedComparison || isCollatorConstruction(node)) && isInlineSortComparator(node)) {
context.report({
node,
message:
'Create one Intl.Collator before sorting and reuse its compare method; resolving collation options inside the comparator repeats setup for every comparison. Preserve the locale, options, and tie-breaker.'
})
}
}
return { CallExpression: inspect, NewExpression: inspect }
}
export default {
meta: { name: 'sort-comparator-performance' },
rules: { 'no-repeated-collator': { create: createRule } }
}
+37
View File
@@ -0,0 +1,37 @@
# Performance regression checks
`pnpm --silent audit:perf > performance-audit.json` scans production `src/` with
the existing app-store and buffer-concatenation rules plus the sort-comparator
rule. Warnings are advisory in this full inventory; tool/parser failures fail.
New warning findings on changed lines fail `pnpm check:code-quality:changed`.
Tests, generated files, `mobile/` and `cloud/` are outside this source audit.
The sort rule detects optioned `localeCompare` and `Intl.Collator` construction
inside inline `sort`/`toSorted` callbacks. Construct one collator outside the
callback, preserving locale, options and tie-breakers. If the locale changes at
runtime, reconstruct at the next sort or key the cache by locale. Bare comparisons
and standalone equality checks are allowed. There is no autofix or interprocedural
analysis: named comparators, aliases, custom methods and deferred callbacks need
manual review. A warning identifies repeated setup, not proof of visible lag.
`pnpm test:perf:contracts` runs the explicit selection in
`vitest.performance.config.ts`: SQLite statement reuse and schema parity, relay
filesystem concurrency, tokenizer rejection, highlighting cache, queued
cancellation, terminal backing-memory retention and detector fixtures. Missing
listed files fail configuration loading. Tests run serially, without retries,
and inherit the full suite's setup and forced-GC support. This makes existing
regression coverage easy to run and attribute; it does not create new workload
coverage by itself.
`.github/workflows/performance-contracts.yml` runs daily and manually on Linux,
macOS and Windows, and on PRs changing this tooling. It uploads JSON test results
and the source inventory. Its schedule starts after merge. Run the existing
`test:e2e:terminal-perf:scale:report` for rendered typing/frame budgets and
`test:e2e:ssh-docker-perf` for real transport behavior. Relay unit tests do not
measure SSH RTT, WSL scheduling or a packaged Electron renderer.
To extend coverage, select a production-path regression with an operation-count,
identity, queue-admission or retained-memory oracle. Confirm it fails with the
old behavior. Use controlled, counterbalanced benchmark samples for timings;
avoid new machine-dependent millisecond gates in the normal unit suite. A green
source scan and these contracts cannot establish that the whole app is fast.
@@ -0,0 +1,45 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
function lint(source) {
return runOxlintPluginOnSource({
pluginName: 'sort-comparator-performance',
pluginPath: path.resolve('config/oxlint-plugins/sort-comparator-performance.mjs'),
rules: { 'sort-comparator-performance/no-repeated-collator': 'warn' },
source
})
}
describe('sort comparator performance', () => {
it('reports repeated collation setup in inline sort and toSorted callbacks', () => {
const findings = lint(`
rows.sort((a, b) => a.name.localeCompare(b.name, locale, { sensitivity: 'base' }))
rows.toSorted(function (a, b) { return new Intl.Collator('sv').compare(a, b) })
rows['sort']((a, b) => Intl.Collator('en', { numeric: true }).compare(a, b))
rows.sort((a, b) => a['localeCompare'](b, undefined, options))
`)
expect(findings).toHaveLength(4)
expect(
findings.every(
(finding) => finding.code === 'sort-comparator-performance(no-repeated-collator)'
)
).toBe(true)
})
it('allows one collator per sort, bare comparisons, and unrelated callbacks', () => {
expect(
lint(`
const collator = new Intl.Collator(locale, options)
rows.sort((a, b) => collator.compare(a.name, b.name) || a.id.localeCompare(b.id))
rows.toSorted(collator.compare)
const equal = a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0
rows.map(a => new Intl.Collator(a.locale))
rows.sort((a, b) => {
function deferred() { return new Intl.Collator(locale) }
return a - b
})
`)
).toEqual([])
})
})
+33
View File
@@ -0,0 +1,33 @@
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { defineConfig } from 'vitest/config'
import baseConfig from './vitest.config'
const contracts = [
'src/main/sqlite/sync-database.test.ts',
'src/main/runtime/orchestration/db/row-column-lists.test.ts',
'src/relay/fs-path-metadata-symlink-concurrency.test.ts',
'src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts',
'src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts',
'src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts',
'src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-queue-retention.test.ts',
'config/scripts/app-store-performance-plugin.test.mjs',
'config/scripts/quadratic-buffer-concat-plugin.test.mjs',
'config/scripts/sort-comparator-performance-plugin.test.mjs'
]
for (const contract of contracts) {
if (!existsSync(resolve(contract))) {
throw new Error(`Missing performance contract: ${contract}`)
}
}
export default defineConfig({
...baseConfig,
test: {
...baseConfig.test,
include: contracts,
fileParallelism: false,
retry: 0
}
})
+2 -1
View File
@@ -10,6 +10,8 @@
},
"main": "./out/main/index.js",
"scripts": {
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
"format": "oxfmt --write .",
"lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-runtime-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage",
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
@@ -144,7 +146,6 @@
"bench:agent-inspection-cadence": "node config/scripts/agent-inspection-cadence-batching-benchmark.mjs",
"bench:renderer-quadratic-scans": "node config/scripts/renderer-quadratic-scan-benchmark.mjs",
"bench:session-write-hot-path": "node config/scripts/session-write-hot-path-benchmark.mjs",
"bench:terminal-partial-escape-tail": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/terminal-partial-escape-tail-benchmark.mjs",
"bench:terminal-partial-escape-tail": "node config/scripts/terminal-partial-escape-tail-benchmark.mjs",
"bench:worktree-refresh-churn": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/worktree-refresh-churn-benchmark.mjs",
"bench:multi-workspace-typing": "pnpm run ensure:electron-runtime && node config/scripts/run-multi-workspace-typing-bench.mjs",