mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
feat(perf): audit comparator setup and schedule performance contracts
This commit is contained in:
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } }
|
||||
}
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user