mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
Merge remote-tracking branch 'origin/main' into salvage-20351
This commit is contained in:
@@ -10,6 +10,10 @@ inputs:
|
||||
description: Node.js version override; defaults to the version declared in package.json.
|
||||
required: false
|
||||
default: ''
|
||||
cache-dependency-path:
|
||||
description: Lockfiles for the pnpm download store; include mobile/pnpm-lock.yaml only when the job installs mobile dependencies.
|
||||
required: false
|
||||
default: pnpm-lock.yaml
|
||||
persist-native-cache:
|
||||
description: Save restored native modules at job end. Set false when a later step overwrites the same path with a different ABI.
|
||||
required: false
|
||||
@@ -39,9 +43,7 @@ runs:
|
||||
with:
|
||||
install: false
|
||||
|
||||
# Why both lockfiles: setup-node keys the pnpm store on the root lockfile alone, so
|
||||
# jobs that also install mobile restored a store with none of the React Native tree
|
||||
# in it and re-downloaded the lot on every run.
|
||||
# Desktop-only jobs should not miss their download cache when mobile dependencies change.
|
||||
- name: Setup Node.js
|
||||
id: default-node
|
||||
if: inputs.node-version == ''
|
||||
@@ -49,9 +51,7 @@ runs:
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||
|
||||
- name: Setup requested Node.js
|
||||
id: requested-node
|
||||
@@ -60,9 +60,7 @@ runs:
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||
|
||||
- name: Validate native runtime
|
||||
shell: bash
|
||||
@@ -152,9 +150,9 @@ runs:
|
||||
with:
|
||||
path: |
|
||||
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
||||
node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
|
||||
node_modules/.pnpm/@orca+windows-registry@*/node_modules/@orca/windows-registry/build
|
||||
node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
|
||||
key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
|
||||
key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
||||
|
||||
- name: Restore compiled native modules without saving
|
||||
id: native-cache-restore-only
|
||||
@@ -163,9 +161,9 @@ runs:
|
||||
with:
|
||||
path: |
|
||||
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
||||
node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
|
||||
node_modules/.pnpm/@orca+windows-registry@*/node_modules/@orca/windows-registry/build
|
||||
node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
|
||||
key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
|
||||
key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
||||
|
||||
# pnpm's bundled gyp_main.py is not executable on fresh Linux runners.
|
||||
- name: Use external node-gyp
|
||||
|
||||
@@ -160,16 +160,14 @@ jobs:
|
||||
|
||||
- name: Checkout the requested ref
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
# Full-history checkout must also preserve case-twin branch and tag names.
|
||||
GIT_DEFAULT_REF_FORMAT: reftable
|
||||
with:
|
||||
# Why an input at all rather than just github.ref: the whole point is to
|
||||
# build code that has not landed, and the workflow definition itself
|
||||
# always comes from the dispatch ref — naming the branch here instead
|
||||
# applies main's current copy of this file to an arbitrary branch.
|
||||
ref: ${{ steps.vetted.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
fetch-depth: 1
|
||||
# This job only reads stablyai/orca and never pushes; every write goes
|
||||
# to the adhoc repo through a minted App token passed by env. Not
|
||||
# persisting the checkout credential shrinks the blast radius if a build
|
||||
|
||||
@@ -62,7 +62,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
canary-run-id:
|
||||
description: Successful same-commit canary run required for batch-apply
|
||||
description: Successful same-code canary in this rehome control generation; reusable across batches
|
||||
required: false
|
||||
type: string
|
||||
confirmation:
|
||||
|
||||
@@ -90,7 +90,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the daily repo through a minted App token passed by env.
|
||||
# Not persisting the checkout credential shrinks the blast radius if a
|
||||
|
||||
@@ -170,8 +170,34 @@ jobs:
|
||||
# artifact instead of starting five concurrent electron-vite builds.
|
||||
# ORCA_E2E_FORWARD_APP_LOGS keeps startup failures visible when Electron
|
||||
# launches but never creates a BrowserWindow.
|
||||
- name: Balance E2E shard from timing evidence
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
SKIP_BUILD: '1'
|
||||
ORCA_E2E_FORWARD_APP_LOGS: '1'
|
||||
ORCA_E2E_WEB_CLIENT: '1'
|
||||
ORCA_RELAY_PATH: ${{ github.workspace }}/out/relay
|
||||
run: |
|
||||
mkdir -p ci-shards
|
||||
pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --list --reporter=json > ci-shards/discovery.json
|
||||
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
|
||||
node config/scripts/ci-e2e-shard-plan.mjs ci-shards/discovery.json '${{ matrix.shard }}' ci-shards
|
||||
pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --test-list=ci-shards/selected.txt --list --reporter=json > ci-shards/selected-discovery.json
|
||||
node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards/assignment.json ci-shards/selected-discovery.json
|
||||
|
||||
- name: Run E2E tests (${{ matrix.shard_name }})
|
||||
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --shard=${{ matrix.shard }}
|
||||
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt
|
||||
|
||||
- name: Upload E2E shard assignment
|
||||
if: always()
|
||||
# Diagnostic upload outages must not change the test verdict.
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e-shard-${{ matrix.shard_name }}-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
# The frame benchmark needs a mapped window, which the headless shards exclude.
|
||||
- name: Run worktree first-paint benchmark
|
||||
|
||||
@@ -137,7 +137,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preflight.outputs.head_sha }}
|
||||
fetch-depth: 0
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the hourly repo through a minted App token passed by env.
|
||||
# Not persisting the checkout credential shrinks the blast radius if a
|
||||
|
||||
@@ -94,6 +94,8 @@ jobs:
|
||||
run: node -e 'const fs = require("node:fs"); const { expo } = require("./app.json"); fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${expo.version}\nbuild_number=${expo.ios.buildNumber}\n`)'
|
||||
|
||||
- name: Expo prebuild
|
||||
env:
|
||||
ORCA_IOS_APS_ENVIRONMENT: production
|
||||
run: npx expo prebuild --platform ios --no-install
|
||||
|
||||
- name: Install CocoaPods
|
||||
|
||||
@@ -41,6 +41,10 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# bundler-cache installs mobile/Gemfile.lock, so this job is also what
|
||||
# proves the pinned fastlane the release workflow depends on still
|
||||
|
||||
+15
-14
@@ -126,6 +126,9 @@ jobs:
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
@@ -273,6 +276,12 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: node .github/scripts/check-root-directory-entries.mjs "$BASE_SHA" "$HEAD_SHA"
|
||||
|
||||
# Why here: the READMEs embed media owned by docs/site and resources/onboarding,
|
||||
# and the classifier skips static_analysis for docs-only diffs. This job runs
|
||||
# on every PR and needs no install.
|
||||
- name: Check README local links
|
||||
run: node config/scripts/check-readme-local-links.mjs
|
||||
|
||||
typecheck:
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.typecheck == 'true'
|
||||
@@ -775,18 +784,9 @@ jobs:
|
||||
[[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; }
|
||||
|
||||
- name: Verify headless serve signal shutdown
|
||||
run: node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage
|
||||
|
||||
- name: Verify extracted launcher serve signal shutdown
|
||||
run: >-
|
||||
node config/scripts/run-headless-serve-shutdown-docker.mjs
|
||||
--appimage dist/orca-linux.AppImage --entrypoint launcher
|
||||
|
||||
- name: Verify AppImage CLI registration and serve signal shutdown
|
||||
run: >-
|
||||
node config/scripts/run-headless-serve-shutdown-docker.mjs
|
||||
--appimage dist/orca-linux.AppImage --entrypoint appimage
|
||||
--signal-target serving-electron --int-delivery pid
|
||||
--appimage dist/orca-linux.AppImage --all-entrypoints
|
||||
|
||||
# A default container reproduces the hostile AppImage launch environment.
|
||||
- name: Verify Linux CLI launch contract
|
||||
@@ -835,9 +835,9 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
||||
node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
|
||||
node_modules/.pnpm/@orca+windows-registry@*/node_modules/@orca/windows-registry/build
|
||||
node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
|
||||
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
|
||||
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
||||
|
||||
# vitest runs here directly rather than through `pnpm test`, so the addon
|
||||
# assertions only hold once install-node-dependencies has rebuilt natives.
|
||||
@@ -846,6 +846,7 @@ jobs:
|
||||
pnpm exec vitest run --config config/vitest.config.ts
|
||||
config/scripts/rebuild-native-deps.test.mjs
|
||||
config/scripts/rebuild-native-deps-windows-process-tree.test.mjs
|
||||
src/main/windows-registry-addon.test.ts
|
||||
src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts
|
||||
src/main/browser/browser-route-tcp-egress.electron.test.ts
|
||||
src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
||||
@@ -903,9 +904,9 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
||||
node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
|
||||
node_modules/.pnpm/@orca+windows-registry@*/node_modules/@orca/windows-registry/build
|
||||
node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
|
||||
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
|
||||
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
||||
|
||||
- name: Prepare Electron native runtime
|
||||
run: node config/scripts/ensure-native-runtime.mjs --runtime=electron
|
||||
|
||||
@@ -45,7 +45,11 @@ jobs:
|
||||
npx --yes pnpm@10.24.0 --filter '@orca-cloud/relay^...' build
|
||||
|
||||
- name: Test shard
|
||||
env:
|
||||
ORCA_BALANCE_UNIT_SHARDS: '1'
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
run: |
|
||||
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
|
||||
--exclude=src/main/daemon/shell-ready.test.ts \
|
||||
@@ -65,3 +69,14 @@ jobs:
|
||||
--exclude=src/shared/posix-command-path-lookup.test.ts \
|
||||
--exclude=tests/e2e/cross-version-wire/** \
|
||||
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
|
||||
|
||||
- name: Upload unit shard assignment
|
||||
if: always()
|
||||
# Diagnostic upload outages must not change the test verdict.
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -28,6 +28,9 @@ out/
|
||||
/build/
|
||||
release/
|
||||
native/**/.build/
|
||||
# node-gyp output for the vendored Windows registry addon; generated per host and ABI.
|
||||
native/windows-registry/build/
|
||||
native/windows-registry/bin/
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
@@ -104,6 +107,7 @@ docs/**
|
||||
!docs/mobile-terminal-shortcut-bar.md
|
||||
!docs/reference/
|
||||
!docs/reference/agent-pty-transcript-capture.md
|
||||
!docs/reference/agent-session-search-query-tuning.md
|
||||
!docs/reference/agent-status-store.md
|
||||
!docs/reference/antigravity-readiness-evidence.md
|
||||
!docs/reference/git-compatibility.md
|
||||
|
||||
@@ -54,7 +54,7 @@ Fan one prompt across five agents, each in its own isolated git worktree — com
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/model/worktrees"><picture><source srcset="docs/assets/feature-wall/parallel-worktrees.gif" type="image/gif"><img src="docs/assets/feature-wall/parallel-worktrees.jpg" alt="Parallel worktree orchestration" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/model/worktrees"><picture><source srcset="docs/site/public/docs/tab-split.gif" type="image/gif"><img src="docs/site/public/docs/posters/tab-split.jpg" alt="Parallel worktree orchestration" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -68,7 +68,7 @@ Ghostty-class terminals with WebGL rendering, infinite splits, and scrollback th
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/terminal"><picture><source srcset="docs/assets/feature-wall/terminal-splits.gif" type="image/gif"><img src="docs/assets/feature-wall/terminal-splits.jpg" alt="Terminal splits" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/terminal"><picture><source srcset="resources/onboarding/feature-wall/tile-02.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-02.poster.jpg" alt="Terminal splits" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -82,7 +82,7 @@ Click any UI element in a real Chromium window to send its HTML, CSS, and a crop
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/browser/design-mode"><picture><source srcset="docs/assets/feature-wall/design-mode.gif" type="image/gif"><img src="docs/assets/feature-wall/design-mode.jpg" alt="Embedded browser and Design Mode" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/browser/design-mode"><picture><source srcset="docs/site/public/docs/orca-design-mode.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-05.poster.jpg" alt="Embedded browser and Design Mode" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -96,7 +96,7 @@ Browse PRs, issues, and project boards in-app — open a worktree from any task
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/review/linear"><picture><source srcset="docs/assets/feature-wall/github-linear.gif" type="image/gif"><img src="docs/assets/feature-wall/github-linear.jpg" alt="GitHub and Linear task workflows in Orca" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/review/linear"><picture><source srcset="resources/onboarding/feature-wall/tile-03.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-03.poster.jpg" alt="GitHub and Linear task workflows in Orca" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -110,7 +110,7 @@ Run agents on a beefy remote box with full file editing, git, and terminals —
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/ssh"><picture><source srcset="docs/assets/feature-wall/ssh-worktrees.gif" type="image/gif"><img src="docs/assets/feature-wall/ssh-worktrees.jpg" alt="Remote worktrees over SSH" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/ssh"><picture><source srcset="resources/onboarding/feature-wall/tile-06.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-06.poster.jpg" alt="Remote worktrees over SSH" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -124,7 +124,7 @@ Drop comments on any diff line and ship them back to the agent — review, edit,
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/review/annotate-ai-diff"><picture><source srcset="docs/assets/feature-wall/annotate-diff.gif" type="image/gif"><img src="docs/assets/feature-wall/annotate-diff.jpg" alt="Annotate AI-generated diffs" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/review/annotate-ai-diff"><picture><source srcset="docs/site/public/docs/annotate-ai-diff.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-08.poster.jpg" alt="Annotate AI-generated diffs" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -138,7 +138,7 @@ VS Code's editor with autosave everywhere — drag files or images straight into
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/editing/file-explorer"><picture><source srcset="docs/assets/feature-wall/file-drag.gif" type="image/gif"><img src="docs/assets/feature-wall/file-drag.jpg" alt="Drag files and images into an agent prompt" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/editing/file-explorer"><picture><source srcset="resources/onboarding/feature-wall/tile-07.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-07.poster.jpg" alt="Drag files and images into an agent prompt" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -152,7 +152,7 @@ Agents drive Orca too — script every workflow with `orca worktree create`, `sn
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://www.onorca.dev/docs/cli/overview"><picture><source srcset="docs/assets/feature-wall/orca-cli.gif" type="image/gif"><img src="docs/assets/feature-wall/orca-cli.jpg" alt="Script Orca from the CLI" width="100%" /></picture></a>
|
||||
<a href="https://www.onorca.dev/docs/cli/overview"><picture><source srcset="resources/onboarding/feature-wall/tile-09.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-09.poster.jpg" alt="Script Orca from the CLI" width="100%" /></picture></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -273,6 +273,26 @@ describe('incident monitor evaluator', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('allows at most three unexpected director errors per five minutes without relaxing other gates', () => {
|
||||
for (const errors of [1, 2, 3]) {
|
||||
const sample = healthySample()
|
||||
sample.sources['cloud-monitoring']!.signals['director.errors'] = signal(errors)
|
||||
expect(evaluateIncidentSample(sample, startedAt).status).toBe('green')
|
||||
}
|
||||
const excess = healthySample()
|
||||
excess.sources['cloud-monitoring']!.signals['director.errors'] = signal(4)
|
||||
expect(evaluateIncidentSample(excess, startedAt).failures).toContainEqual(
|
||||
expect.objectContaining({ signal: 'director.errors', observed: 4, threshold: 3 })
|
||||
)
|
||||
const auth = healthySample()
|
||||
auth.sources['cloud-monitoring']!.signals['auth.errors'] = signal(1)
|
||||
expect(evaluateIncidentSample(auth, startedAt).status).toBe('freeze')
|
||||
const pressure = healthySample()
|
||||
pressure.sources['cloud-monitoring']!.signals['director.errors'] = signal(1)
|
||||
pressure.sources['cloud-monitoring']!.signals['cloud_sql.cpu'] = signal(0.81)
|
||||
expect(evaluateIncidentSample(pressure, startedAt).status).toBe('freeze')
|
||||
})
|
||||
|
||||
it('freezes on SQL, director, relay pool, heartbeat, and migration breaches', () => {
|
||||
const sample = healthySample()
|
||||
sample.sources['cloud-monitoring']!.signals['cloud_sql.cpu'] = signal(0.81)
|
||||
|
||||
@@ -101,7 +101,8 @@ export const INCIDENT_MONITOR_THRESHOLDS = {
|
||||
directorCpuUtilization: 0.8,
|
||||
directorMemoryUtilization: 0.8,
|
||||
directorConcurrency: 64,
|
||||
directorErrors: 0,
|
||||
// Sparse connection timeouts must not block a healthy rollout; four/5min still freezes.
|
||||
directorErrors: 3,
|
||||
authErrors: 0,
|
||||
// Why: 800 exceeded the 600 hard cap, so this could never trigger on a capped cell. 500 is
|
||||
// the ordinary admission limit a cell actually stops at (600 cap - 100 control-rebind reserve).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createDrainMigrationRowLookup } from './drain-migration-row-lookup.js'
|
||||
import { IDLE_REHOME_PAGE_SIZE, selectIdleRegionalRehomes } from './idle-regional-rehome-selection.js'
|
||||
import { readRegionCorrectionOutcomes } from './region-correction-outcomes.js'
|
||||
import {
|
||||
@@ -2187,22 +2188,16 @@ export class RelayAssignmentStore {
|
||||
[input.cellId]
|
||||
)
|
||||
const cells = await this.lockCellInventory(transaction, 'request')
|
||||
const assignmentRows = createDrainMigrationRowLookup(assignments, text)
|
||||
const leaseRows = createDrainMigrationRowLookup(activityLeases, text)
|
||||
for (const migrationRow of migrations) {
|
||||
const identity = {
|
||||
userId: text(migrationRow, 'user_id'),
|
||||
relayHostId: text(migrationRow, 'relay_host_id')
|
||||
}
|
||||
const assignment = assignments.find(
|
||||
(candidate) =>
|
||||
text(candidate, 'user_id') === identity.userId &&
|
||||
text(candidate, 'relay_host_id') === identity.relayHostId
|
||||
)
|
||||
const assignment = assignmentRows.first(identity)
|
||||
assertCurrentMigrationAssignment(assignment, migrationRow)
|
||||
const leases = activityLeases.filter(
|
||||
(lease) =>
|
||||
text(lease, 'user_id') === identity.userId &&
|
||||
text(lease, 'relay_host_id') === identity.relayHostId
|
||||
)
|
||||
const leases = leaseRows.all(identity)
|
||||
const migrationLeases = leases.filter(
|
||||
(lease) => text(lease, 'activity_kind') === 'migration'
|
||||
)
|
||||
@@ -2377,22 +2372,16 @@ export class RelayAssignmentStore {
|
||||
) {
|
||||
throw new Error('drain_migration_source_incarnation_mismatch')
|
||||
}
|
||||
const assignmentRows = createDrainMigrationRowLookup(assignments, text)
|
||||
const leaseRows = createDrainMigrationRowLookup(activityLeases, text)
|
||||
for (const migrationRow of migrationIncarnations) {
|
||||
const assignment = assignments.find(
|
||||
(candidate) =>
|
||||
text(candidate, 'user_id') === text(migrationRow, 'user_id') &&
|
||||
text(candidate, 'relay_host_id') === text(migrationRow, 'relay_host_id')
|
||||
)
|
||||
const identity = {
|
||||
userId: text(migrationRow, 'user_id'),
|
||||
relayHostId: text(migrationRow, 'relay_host_id')
|
||||
}
|
||||
const assignment = assignmentRows.first(identity)
|
||||
assertCurrentMigrationAssignment(assignment, migrationRow)
|
||||
assertAssignmentActivityAccounting(
|
||||
assignment,
|
||||
activityLeases.filter(
|
||||
(lease) =>
|
||||
text(lease, 'user_id') === text(migrationRow, 'user_id') &&
|
||||
text(lease, 'relay_host_id') === text(migrationRow, 'relay_host_id')
|
||||
),
|
||||
migrationRow
|
||||
)
|
||||
assertAssignmentActivityAccounting(assignment, leaseRows.all(identity), migrationRow)
|
||||
}
|
||||
const sendPermitExpiresAt = now + CELL_DRAIN_SEND_PERMIT_MS
|
||||
await transaction.query(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { createDrainMigrationRowLookup } from './drain-migration-row-lookup.js'
|
||||
import type { SqlRow } from './database.js'
|
||||
|
||||
function text(row: SqlRow, field: string): string {
|
||||
const value = row[field]
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`invalid_${field}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
it('keeps first assignment matches, lease order, row identity, and separate identity components', () => {
|
||||
const rows = [
|
||||
{ user_id: 'a:b', relay_host_id: 'c', value: 1 },
|
||||
{ user_id: 'a', relay_host_id: 'b:c', value: 2 },
|
||||
{ user_id: 'a:b', relay_host_id: 'c', value: 3 },
|
||||
{ user_id: '', relay_host_id: '', value: 4 }
|
||||
]
|
||||
const lookup = createDrainMigrationRowLookup(rows, text)
|
||||
for (const identity of [
|
||||
{ userId: 'a:b', relayHostId: 'c' },
|
||||
{ userId: 'a', relayHostId: 'b:c' },
|
||||
{ userId: '', relayHostId: '' },
|
||||
{ userId: 'missing', relayHostId: 'c' }
|
||||
]) {
|
||||
const expected = rows.filter(
|
||||
(row) =>
|
||||
row.user_id === identity.userId && row.relay_host_id === identity.relayHostId
|
||||
)
|
||||
expect(lookup.first(identity)).toBe(expected[0])
|
||||
expect(lookup.all(identity)).toEqual(expected)
|
||||
lookup.all(identity).forEach((row, index) => expect(row).toBe(expected[index]))
|
||||
}
|
||||
})
|
||||
|
||||
it('retains lazy validation and short circuiting when an inventory is malformed', () => {
|
||||
const first = { user_id: 'user', relay_host_id: 'host' }
|
||||
const identity = { userId: 'user', relayHostId: 'host' }
|
||||
const lookup = createDrainMigrationRowLookup(
|
||||
[first, { user_id: null, relay_host_id: 'bad' }],
|
||||
text
|
||||
)
|
||||
expect(lookup.first(identity)).toBe(first)
|
||||
expect(() => lookup.all(identity)).toThrow('invalid_user_id')
|
||||
const unrelated = createDrainMigrationRowLookup(
|
||||
[first, { user_id: 'other', relay_host_id: null }],
|
||||
text
|
||||
)
|
||||
expect(unrelated.all(identity)).toEqual([first])
|
||||
expect(() => unrelated.first({ userId: 'other', relayHostId: 'host' })).toThrow(
|
||||
'invalid_relay_host_id'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not share an index between refreshed inventories', () => {
|
||||
const identity = { userId: 'user', relayHostId: 'host' }
|
||||
const oldRow = { user_id: 'user', relay_host_id: 'host', version: 1 }
|
||||
const newRow = { ...oldRow, version: 2 }
|
||||
expect(createDrainMigrationRowLookup([oldRow], text).first(identity)).toBe(oldRow)
|
||||
expect(createDrainMigrationRowLookup([newRow], text).first(identity)).toBe(newRow)
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { SqlRow } from './database.js'
|
||||
|
||||
type Identity = { userId: string; relayHostId: string }
|
||||
type RowIndex = Map<string, Map<string, SqlRow[]>>
|
||||
|
||||
/** A single locked inventory, never retained across transactions or refreshed queries. */
|
||||
export function createDrainMigrationRowLookup(
|
||||
rows: SqlRow[],
|
||||
readText: (row: SqlRow, field: string) => string
|
||||
): {
|
||||
first: (identity: Identity) => SqlRow | undefined
|
||||
all: (identity: Identity) => SqlRow[]
|
||||
} {
|
||||
let index: RowIndex | null | undefined
|
||||
const indexed = (identity: Identity): SqlRow[] | undefined => {
|
||||
if (index === undefined) {
|
||||
index = indexRows(rows)
|
||||
}
|
||||
return index?.get(identity.userId)?.get(identity.relayHostId)
|
||||
}
|
||||
const matches = (row: SqlRow, identity: Identity): boolean =>
|
||||
readText(row, 'user_id') === identity.userId &&
|
||||
readText(row, 'relay_host_id') === identity.relayHostId
|
||||
return {
|
||||
first(identity) {
|
||||
const group = indexed(identity)
|
||||
return index === null ? rows.find((row) => matches(row, identity)) : group?.[0]
|
||||
},
|
||||
all(identity) {
|
||||
const group = indexed(identity)
|
||||
return index === null ? rows.filter((row) => matches(row, identity)) : (group ?? [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function indexRows(rows: SqlRow[]): RowIndex | null {
|
||||
const index: RowIndex = new Map()
|
||||
for (const row of rows) {
|
||||
const userId = row.user_id
|
||||
const hostId = row.relay_host_id
|
||||
// Preserve the original lazy validation and refusal order for malformed database rows.
|
||||
if (typeof userId !== 'string' || typeof hostId !== 'string') {
|
||||
return null
|
||||
}
|
||||
let hosts = index.get(userId)
|
||||
if (!hosts) {
|
||||
hosts = new Map()
|
||||
index.set(userId, hosts)
|
||||
}
|
||||
const group = hosts.get(hostId)
|
||||
if (group) {
|
||||
group.push(row)
|
||||
} else {
|
||||
hosts.set(hostId, [row])
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { RelayAssignmentStore } from './assignment-store.js'
|
||||
import { openInMemoryRelayDatabase, type RelayDatabase, type SqlRow } from './database.js'
|
||||
|
||||
let database: RelayDatabase | undefined
|
||||
afterEach(async () => await database?.close())
|
||||
|
||||
it.each([false, true])(
|
||||
'looks up a whole drain inventory with linear identity reads (expired: %s)',
|
||||
async (expired) => {
|
||||
database = await openInMemoryRelayDatabase()
|
||||
let measuring = false
|
||||
let identityReads = 0
|
||||
let indexedRows = 0
|
||||
const instrument = (delegate: RelayDatabase): RelayDatabase => ({
|
||||
query: (sql, params) => delegate.query(sql, params),
|
||||
queryLocked: async (sql, params, options) => {
|
||||
const rows = await delegate.queryLocked(sql, params, options)
|
||||
if (
|
||||
!measuring ||
|
||||
!sql.includes('WHERE EXISTS') ||
|
||||
!(sql.includes('SELECT assignment.*') || sql.includes('SELECT lease.*'))
|
||||
) {
|
||||
return rows
|
||||
}
|
||||
indexedRows += rows.length
|
||||
return rows.map(
|
||||
(row): SqlRow =>
|
||||
new Proxy(row, {
|
||||
get(target, key) {
|
||||
if (key === 'user_id' || key === 'relay_host_id') {
|
||||
identityReads++
|
||||
}
|
||||
return Reflect.get(target, key)
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
transaction: (operation, options) =>
|
||||
delegate.transaction((tx) => operation(instrument(tx)), options),
|
||||
close: () => delegate.close()
|
||||
})
|
||||
let now = 100
|
||||
const store = new RelayAssignmentStore(instrument(database), () => now, {
|
||||
requireLiveCells: true
|
||||
})
|
||||
const cells = ['a', 'b'].map((id) => ({
|
||||
id: `cell-${id}`,
|
||||
url: `https://relay-${id}.example.com`,
|
||||
capacityRequests: 500
|
||||
}))
|
||||
await store.reconcileCells(cells)
|
||||
const incarnation = '11111111-1111-4111-8111-111111111111'
|
||||
for (const cell of cells) {
|
||||
await store.recordCellHeartbeat({
|
||||
cellId: cell.id,
|
||||
cellUrl: cell.url,
|
||||
cellIncarnation: incarnation,
|
||||
startedAt: 50,
|
||||
ready: true,
|
||||
observedRequests: 0
|
||||
})
|
||||
}
|
||||
await store.setCellEnabled('cell-b', false)
|
||||
const identities = Array.from({ length: 50 }, (_, index) => ({
|
||||
userId: `user-${index % 5}`,
|
||||
relayHostId: `host${String(index).padStart(12, '0')}`
|
||||
}))
|
||||
for (const identity of identities) {
|
||||
await store.assign(identity)
|
||||
}
|
||||
await store.setCellEnabled('cell-b', true)
|
||||
await store.setCellEnabled('cell-a', false)
|
||||
for (const identity of identities) {
|
||||
const migration = await store.startEvacuation(identity, 'cell-b')
|
||||
await store.markMigrationTargetRegistered(identity, {
|
||||
cellId: 'cell-b',
|
||||
assignmentEpoch: migration.assignmentEpoch
|
||||
})
|
||||
}
|
||||
const attempt = {
|
||||
attemptId: '55555555-5555-4555-8555-555555555555',
|
||||
cellId: 'cell-a',
|
||||
cellIncarnation: incarnation,
|
||||
traceValue: '66666666-6666-4666-8666-666666666666',
|
||||
plannedGraceMs: 120_000
|
||||
}
|
||||
await store.prepareCellDrainAttempt(attempt)
|
||||
if (expired) {
|
||||
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
||||
await store.releaseExpiredActivityLeases()
|
||||
for (const cell of cells) {
|
||||
await store.recordCellHeartbeat({
|
||||
cellId: cell.id,
|
||||
cellUrl: cell.url,
|
||||
cellIncarnation: incarnation,
|
||||
startedAt: 50,
|
||||
ready: true,
|
||||
observedRequests: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
measuring = true
|
||||
await expect(store.beginCellDrainSend(attempt)).resolves.toMatchObject({
|
||||
state: 'send-may-have-started',
|
||||
shouldSend: true
|
||||
})
|
||||
expect(indexedRows).toBeGreaterThanOrEqual(100)
|
||||
expect(identityReads).toBeLessThanOrEqual(indexedRows * 2)
|
||||
const migrations = await database.query(
|
||||
'SELECT expires_at FROM relay_assignment_migrations'
|
||||
)
|
||||
expect(migrations).toHaveLength(50)
|
||||
expect(
|
||||
migrations.every(
|
||||
(row) => row.expires_at === now + ASSIGNMENT_LIMITS.migrationLeaseMs
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
@@ -424,6 +424,8 @@ describe('successful client accept timing', () => {
|
||||
) as { connId: string; connTicket: string }
|
||||
// The desktop's data leg is the attach window this is meant to expose.
|
||||
now += 23
|
||||
const session = h.registry.get({ userId: identity.sub, relayHostId: identity.relayHostId })!
|
||||
const ownerProbe = vi.spyOn(session.pendingConns, 'has')
|
||||
const accepted = await h.registry.acceptHostData(
|
||||
hostData as unknown as WebSocket,
|
||||
connOpen.connId,
|
||||
@@ -432,6 +434,7 @@ describe('successful client accept timing', () => {
|
||||
)
|
||||
|
||||
expect(accepted).toBe(true)
|
||||
expect(ownerProbe).toHaveBeenCalledOnce()
|
||||
expect(h.observer.recordClientAcceptCompleted).toHaveBeenCalledWith({
|
||||
totalMs: 49,
|
||||
stageMs: { assignment: 5, credential: 7, activity: 11, attach: 23, basis: 3 }
|
||||
@@ -485,6 +488,80 @@ async function advanceToPing(control: FakeSocket, clock: { now: number }): Promi
|
||||
return (JSON.parse(String(ping[0])) as { t: number }).t
|
||||
}
|
||||
|
||||
// The attach resolves its owning session once and hands it to the unfenced leg;
|
||||
// these hold the session it must be and the order the client hears about it.
|
||||
describe('host data attach ownership', () => {
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
const bystander = { ...identity, sub: 'user-2', relayHostId: 'qponmlkjihgfedcb' }
|
||||
|
||||
async function pendingAttach(h: ReturnType<typeof harness>) {
|
||||
const client = new FakeSocket()
|
||||
await h.registry.acceptClient(
|
||||
client as unknown as WebSocket,
|
||||
identity.relayHostId,
|
||||
'credential'
|
||||
)
|
||||
const session = h.registry.get({ userId: identity.sub, relayHostId: identity.relayHostId })!
|
||||
return { client, session, pending: [...session.pendingConns.values()][0]! }
|
||||
}
|
||||
|
||||
it('attaches the session that owns the connection, not the first one registered', async () => {
|
||||
const h = harness()
|
||||
const idle = new FakeSocket()
|
||||
await h.activate(idle as unknown as WebSocket, bystander, null, 1, false, 1, '1.4.197')
|
||||
await activeHost(h)
|
||||
const { client, session, pending } = await pendingAttach(h)
|
||||
const idleSession = h.registry.get({
|
||||
userId: bystander.sub,
|
||||
relayHostId: bystander.relayHostId
|
||||
})!
|
||||
const host = new FakeSocket()
|
||||
expect(
|
||||
await h.registry.acceptHostData(
|
||||
host as unknown as WebSocket,
|
||||
pending.connId,
|
||||
pending.connTicket,
|
||||
1
|
||||
)
|
||||
).toBe(true)
|
||||
expect(client.send).toHaveBeenCalledWith(expect.stringContaining('"type":"relay-hello"'))
|
||||
expect(session.activeSplices.has(pending.connId)).toBe(true)
|
||||
expect(idleSession.activeSplices.size).toBe(0)
|
||||
expect(idleSession.activeConnIds.size).toBe(0)
|
||||
h.registry.drain(0)
|
||||
vi.advanceTimersByTime(0)
|
||||
})
|
||||
|
||||
it('acknowledges the client only after the connection basis is persisted', async () => {
|
||||
const h = harness()
|
||||
await activeHost(h)
|
||||
const { client, session, pending } = await pendingAttach(h)
|
||||
const basis = deferred<void>()
|
||||
h.store.recordConnectionBasis.mockReturnValueOnce(basis.promise)
|
||||
const host = new FakeSocket()
|
||||
const attaching = h.registry.acceptHostData(
|
||||
host as unknown as WebSocket,
|
||||
pending.connId,
|
||||
pending.connTicket,
|
||||
1
|
||||
)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(h.store.recordConnectionBasis).toHaveBeenCalledOnce()
|
||||
expect(client.send).not.toHaveBeenCalledWith(expect.stringContaining('relay-hello'))
|
||||
basis.resolve()
|
||||
expect(await attaching).toBe(true)
|
||||
expect(client.send).toHaveBeenCalledWith(expect.stringContaining('"type":"relay-hello"'))
|
||||
expect(session.activeSplices.has(pending.connId)).toBe(true)
|
||||
h.registry.drain(0)
|
||||
vi.advanceTimersByTime(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('control round-trip sampling', () => {
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => {
|
||||
|
||||
@@ -111,6 +111,7 @@ function createRegistry(
|
||||
renewControlActivity: ReturnType<typeof vi.fn>
|
||||
releaseActivity: ReturnType<typeof vi.fn>
|
||||
observer: {
|
||||
recordAuth: ReturnType<typeof vi.fn>
|
||||
recordControlClose: ReturnType<typeof vi.fn>
|
||||
recordSpliceClose: ReturnType<typeof vi.fn>
|
||||
}
|
||||
@@ -1396,6 +1397,57 @@ describe('source-owned idle cutover', () => {
|
||||
cleanup.resolve()
|
||||
await attached
|
||||
})
|
||||
it('rejects an attach mid-cutover before its ticket is ever examined', async () => {
|
||||
const h = await source({ failReservation: vi.fn().mockResolvedValue(undefined) })
|
||||
const result = deferred<{ outcome: 'deferred' }>()
|
||||
// The cutover must already be in flight: an idle host is what it claims.
|
||||
const moving = h.registry.idleRehome(request, () => result.promise, vi.fn())
|
||||
const client = new FakeSocket()
|
||||
h.session.pendingConns.set('conn', {
|
||||
connId: 'conn',
|
||||
connTicket: 'ticket',
|
||||
client: client as unknown as WebSocket,
|
||||
reservation: {
|
||||
userId: identity.sub,
|
||||
relayHostId: identity.relayHostId,
|
||||
credentialKind: 'invite',
|
||||
leaseExpiresAt: Date.now() + 1000
|
||||
},
|
||||
attachTimer: setTimeout(() => {}, 1000),
|
||||
credentialActivityId: null
|
||||
} as never)
|
||||
const host = new FakeSocket()
|
||||
// The ticket below is the live one: only the cutover fence may reject it.
|
||||
expect(
|
||||
await h.registry.acceptHostData(host as unknown as WebSocket, 'conn', 'ticket', 1)
|
||||
).toBe(false)
|
||||
expect(host.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.WRONG_CELL, expect.any(String))
|
||||
expect(h.observer.recordAuth).not.toHaveBeenCalled()
|
||||
expect(h.session.pendingConns.has('conn')).toBe(true)
|
||||
expect(h.session.activeConnIds.size).toBe(0)
|
||||
result.resolve({ outcome: 'deferred' })
|
||||
await moving
|
||||
})
|
||||
it('holds no attach ownership when no session owns the connection', async () => {
|
||||
const h = await source()
|
||||
const host = new FakeSocket()
|
||||
expect(
|
||||
await h.registry.acceptHostData(host as unknown as WebSocket, 'stranger', 'ticket', 1)
|
||||
).toBe(false)
|
||||
expect(h.observer.recordAuth).toHaveBeenCalledWith(false)
|
||||
expect(host.close).toHaveBeenCalledWith(
|
||||
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
|
||||
expect.any(String)
|
||||
)
|
||||
// A leaked idle-work hold from the unowned attach would report `busy` here.
|
||||
expect(
|
||||
await h.registry.idleRehome(
|
||||
request,
|
||||
vi.fn().mockResolvedValue({ outcome: 'committed' }),
|
||||
vi.fn()
|
||||
)
|
||||
).toEqual({ outcome: 'committed' })
|
||||
})
|
||||
it('returns the durable operation outcome after source retirement', async () => {
|
||||
const h = await source()
|
||||
const commit = vi.fn().mockResolvedValue({ outcome: 'committed' })
|
||||
@@ -1421,3 +1473,226 @@ describe('source-owned idle cutover', () => {
|
||||
expect(h.registry.get(request)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// The host data leg's owner lookup is the registry's only whole-inventory scan on
|
||||
// an attach. These count what that scan touches, not how long it takes.
|
||||
describe('host data attach owner lookup', () => {
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
const SESSION_COUNT = 1000
|
||||
const CONN_ID = 'conn-owned'
|
||||
const OWNER_INDEX = { first: 0, middle: 499, last: 999 } as const
|
||||
type Placement = keyof typeof OWNER_INDEX | 'absent'
|
||||
type LookupCounts = { visits: number; membership: number }
|
||||
|
||||
function bindOwn<K, V>(map: Map<K, V>, property: string | symbol): unknown {
|
||||
const value: unknown = Reflect.get(map, property, map)
|
||||
return typeof value === 'function' ? value.bind(map) : value
|
||||
}
|
||||
|
||||
// One visit per session the scan pulls off the map iterator; answers unchanged.
|
||||
function countingValues<K, V>(map: Map<K, V>, counts: LookupCounts): Map<K, V> {
|
||||
return new Proxy(map, {
|
||||
get(target, property) {
|
||||
if (property !== 'values') return bindOwn(target, property)
|
||||
return function* (): Generator<V> {
|
||||
for (const value of target.values()) {
|
||||
counts.visits += 1
|
||||
yield value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// One membership check per `pendingConns.has`; answers unchanged.
|
||||
function countingHas<K, V>(map: Map<K, V>, counts: LookupCounts): Map<K, V> {
|
||||
return new Proxy(map, {
|
||||
get(target, property) {
|
||||
if (property !== 'has') return bindOwn(target, property)
|
||||
return (key: K) => {
|
||||
counts.membership += 1
|
||||
return target.has(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The pre-change implementation, kept inline as the oracle the new counts are
|
||||
// differenced against: two inventory arrays, two independent finds.
|
||||
function legacyOwnerLookup(
|
||||
sessions: Map<string, HostSession>,
|
||||
connId: string
|
||||
): { owner: HostSession | undefined; session: HostSession | undefined } {
|
||||
const owner = [...sessions.values()].find((candidate) => candidate.pendingConns.has(connId))
|
||||
const session = [...sessions.values()].find((candidate) => candidate.pendingConns.has(connId))
|
||||
return { owner, session }
|
||||
}
|
||||
|
||||
function pendingConn(client: FakeSocket, connTicket: string) {
|
||||
return {
|
||||
connId: CONN_ID,
|
||||
connTicket,
|
||||
client: client as unknown as WebSocket,
|
||||
reservation: {
|
||||
userId: identity.sub,
|
||||
relayHostId: identity.relayHostId,
|
||||
credentialKind: 'invite',
|
||||
leaseExpiresAt: Date.now() + 1000
|
||||
},
|
||||
attachTimer: setTimeout(() => {}, 1000),
|
||||
credentialActivityId: null
|
||||
} as never
|
||||
}
|
||||
|
||||
// Every decoy holds a pending conn of its own, so each membership check the
|
||||
// scan makes is real work rather than a lookup in an empty map.
|
||||
function decoySession(index: number, counts: LookupCounts): HostSession {
|
||||
const pendingConns = new Map<string, unknown>([[`conn-decoy-${index}`, { connId: 'decoy' }]])
|
||||
return {
|
||||
relayHostId: `decoy-host-${index}`,
|
||||
generation: 1,
|
||||
state: 'active',
|
||||
activeConnIds: new Set<string>(),
|
||||
pendingConns: countingHas(pendingConns, counts)
|
||||
} as unknown as HostSession
|
||||
}
|
||||
|
||||
async function attachRegistry(placement: Placement, store: Partial<RelayCredentialStore> = {}) {
|
||||
const h = createRegistry(vi.fn().mockResolvedValue('control:1'), {
|
||||
failReservation: vi.fn().mockResolvedValue(undefined),
|
||||
recordConnectionBasis: vi.fn().mockResolvedValue(undefined),
|
||||
deactivateBasis: vi.fn().mockResolvedValue(undefined),
|
||||
...store
|
||||
})
|
||||
const control = new FakeSocket()
|
||||
await h.activate(control as unknown as WebSocket, identity, null, 1, false, 1)
|
||||
const internals = h.registry as unknown as { sessions: Map<string, HostSession> }
|
||||
const [ownerKey, owner] = [...internals.sessions.entries()][0]!
|
||||
const counts: LookupCounts = { visits: 0, membership: 0 }
|
||||
const client = new FakeSocket()
|
||||
if (placement !== 'absent') owner.pendingConns.set(CONN_ID, pendingConn(client, 'ticket'))
|
||||
owner.pendingConns = countingHas(owner.pendingConns, counts)
|
||||
const ordered: HostSession[] = []
|
||||
const sessions = new Map<string, HostSession>()
|
||||
const ownerIndex = placement === 'absent' ? 0 : OWNER_INDEX[placement]
|
||||
for (let index = 0; index < SESSION_COUNT; index += 1) {
|
||||
const session = index === ownerIndex ? owner : decoySession(index, counts)
|
||||
ordered.push(session)
|
||||
sessions.set(index === ownerIndex ? ownerKey : `decoy-${index}`, session)
|
||||
}
|
||||
internals.sessions = countingValues(sessions, counts)
|
||||
return { ...h, owner, ordered, counts, client, control, sessions: internals.sessions }
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
placement: 'first',
|
||||
before: { visits: 2000, membership: 2 },
|
||||
after: { visits: 1, membership: 1 }
|
||||
},
|
||||
{
|
||||
placement: 'middle',
|
||||
before: { visits: 2000, membership: 1000 },
|
||||
after: { visits: 500, membership: 500 }
|
||||
},
|
||||
{
|
||||
placement: 'last',
|
||||
before: { visits: 2000, membership: 2000 },
|
||||
after: { visits: 1000, membership: 1000 }
|
||||
},
|
||||
{
|
||||
placement: 'absent',
|
||||
before: { visits: 2000, membership: 2000 },
|
||||
after: { visits: 1000, membership: 1000 }
|
||||
}
|
||||
] as const)(
|
||||
'visits the inventory once, not twice, for a $placement owner',
|
||||
async ({ placement, before, after }) => {
|
||||
const h = await attachRegistry(placement)
|
||||
expect(h.sessions.size).toBe(SESSION_COUNT)
|
||||
const oracle = legacyOwnerLookup(h.sessions, CONN_ID)
|
||||
const legacy = { ...h.counts }
|
||||
h.counts.visits = 0
|
||||
h.counts.membership = 0
|
||||
const host = new FakeSocket()
|
||||
// An unusable ticket stops the attach immediately after the lookup, so the
|
||||
// counts below belong to the lookup alone.
|
||||
expect(
|
||||
await h.registry.acceptHostData(host as unknown as WebSocket, CONN_ID, 'wrong', 1)
|
||||
).toBe(false)
|
||||
expect(h.observer.recordAuth).toHaveBeenCalledExactlyOnceWith(false)
|
||||
expect(host.close).toHaveBeenCalledWith(
|
||||
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
|
||||
'invalid host data ticket'
|
||||
)
|
||||
expect(legacy).toEqual(before)
|
||||
expect({ ...h.counts }).toEqual(after)
|
||||
expect(oracle.owner).toBe(placement === 'absent' ? undefined : h.owner)
|
||||
expect(oracle.owner).toBe(oracle.session)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
{ reason: 'ticket', ticket: 'wrong', generation: 1, state: 'active' },
|
||||
{ reason: 'generation', ticket: 'ticket', generation: 2, state: 'active' },
|
||||
{ reason: 'state', ticket: 'ticket', generation: 1, state: 'orphaned' }
|
||||
] as const)('fails an attach whose $reason does not match the owner', async (input) => {
|
||||
const h = await attachRegistry('middle')
|
||||
h.owner.state = input.state
|
||||
const host = new FakeSocket()
|
||||
expect(
|
||||
await h.registry.acceptHostData(
|
||||
host as unknown as WebSocket,
|
||||
CONN_ID,
|
||||
input.ticket,
|
||||
input.generation
|
||||
)
|
||||
).toBe(false)
|
||||
expect(h.observer.recordAuth).toHaveBeenCalledExactlyOnceWith(false)
|
||||
expect(host.close).toHaveBeenCalledWith(
|
||||
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
|
||||
'invalid host data ticket'
|
||||
)
|
||||
expect(h.owner.pendingConns.has(CONN_ID)).toBe(true)
|
||||
expect(h.owner.activeConnIds.size).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects on the earlier duplicate owner rather than the later live one', async () => {
|
||||
const h = await attachRegistry('middle')
|
||||
h.ordered[0]!.pendingConns.set(CONN_ID, pendingConn(new FakeSocket(), 'stale-ticket') as never)
|
||||
expect(legacyOwnerLookup(h.sessions, CONN_ID).owner).toBe(h.ordered[0])
|
||||
h.counts.visits = 0
|
||||
h.counts.membership = 0
|
||||
const host = new FakeSocket()
|
||||
expect(
|
||||
await h.registry.acceptHostData(host as unknown as WebSocket, CONN_ID, 'ticket', 1)
|
||||
).toBe(false)
|
||||
expect({ ...h.counts }).toEqual({ visits: 1, membership: 1 })
|
||||
expect(h.owner.pendingConns.has(CONN_ID)).toBe(true)
|
||||
})
|
||||
|
||||
it('splices the earlier duplicate owner and leaves the later one untouched', async () => {
|
||||
const basis = vi.fn().mockRejectedValue(new Error('basis failed'))
|
||||
const h = await attachRegistry('first', { recordConnectionBasis: basis })
|
||||
const duplicate = h.ordered[3]!
|
||||
duplicate.pendingConns.set(CONN_ID, pendingConn(new FakeSocket(), 'ticket') as never)
|
||||
h.counts.visits = 0
|
||||
h.counts.membership = 0
|
||||
const host = new FakeSocket()
|
||||
expect(
|
||||
await h.registry.acceptHostData(host as unknown as WebSocket, CONN_ID, 'ticket', 1)
|
||||
).toBe(false)
|
||||
expect({ ...h.counts }).toEqual({ visits: 1, membership: 1 })
|
||||
expect(h.observer.recordAuth).toHaveBeenCalledWith(true)
|
||||
expect(basis).toHaveBeenCalledOnce()
|
||||
// The first owner's entry was consumed; the later duplicate never was.
|
||||
expect(h.owner.pendingConns.has(CONN_ID)).toBe(false)
|
||||
expect(duplicate.pendingConns.has(CONN_ID)).toBe(true)
|
||||
expect(h.owner.activeConnIds.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -511,16 +511,22 @@ export class HostSessionRegistry {
|
||||
connTicket: string,
|
||||
generation: number
|
||||
): Promise<boolean> {
|
||||
const owner = [...this.sessions.values()].find((candidate) =>
|
||||
candidate.pendingConns.has(connId)
|
||||
)
|
||||
// First insertion-order owner, and the only scan the attach makes: the
|
||||
// unfenced leg reuses this result instead of repeating the search.
|
||||
let owner: HostSession | undefined
|
||||
for (const candidate of this.sessions.values()) {
|
||||
if (candidate.pendingConns.has(connId)) {
|
||||
owner = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
const release = owner ? this.beginIdleWork(owner.relayHostId) : () => {}
|
||||
if (!release) {
|
||||
socket.close(RELAY_CLOSE_CODE.WRONG_CELL, 'idle cutover in progress')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return await this.acceptHostDataUnfenced(socket, connId, connTicket, generation)
|
||||
return await this.acceptHostDataUnfenced(socket, connId, connTicket, generation, owner)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
@@ -530,11 +536,9 @@ export class HostSessionRegistry {
|
||||
socket: WebSocket,
|
||||
connId: string,
|
||||
connTicket: string,
|
||||
generation: number
|
||||
generation: number,
|
||||
session: HostSession | undefined
|
||||
): Promise<boolean> {
|
||||
const session = [...this.sessions.values()].find((candidate) =>
|
||||
candidate.pendingConns.has(connId)
|
||||
)
|
||||
const pending = session?.pendingConns.get(connId)
|
||||
if (
|
||||
!session ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observeRelayDatabase } from './observed-relay-database.js'
|
||||
import {
|
||||
CONTROL_RTT_RESERVOIR_LIMIT,
|
||||
observedRelayRequests,
|
||||
percentile,
|
||||
RelayObservability,
|
||||
type RelayProcessCounts
|
||||
} from './relay-observability.js'
|
||||
@@ -412,3 +413,181 @@ describe('relay observability', () => {
|
||||
expect(recordSql.mock.calls.map((call) => call[1])).toEqual([true, false, true, false])
|
||||
})
|
||||
})
|
||||
|
||||
// The pre-change implementation, kept verbatim as the differential oracle. Both
|
||||
// ranks sorted their own copy and the maximum was a zero-seeded fold.
|
||||
function legacyPercentile(values: number[], percentileRank: number): number {
|
||||
if (values.length === 0) return 0
|
||||
const sorted = [...values].sort((left, right) => left - right)
|
||||
return sorted[Math.ceil(percentileRank * sorted.length) - 1] ?? 0
|
||||
}
|
||||
|
||||
function legacyLatencySummary(samples: number[]): { p50: number; p95: number; max: number } {
|
||||
const round = (value: number): number => Number(value.toFixed(3))
|
||||
return {
|
||||
p50: round(legacyPercentile(samples, 0.5)),
|
||||
p95: round(legacyPercentile(samples, 0.95)),
|
||||
max: round(samples.reduce((highest, sample) => Math.max(highest, sample), 0))
|
||||
}
|
||||
}
|
||||
|
||||
// `-0` and `NaN` both survive a string round trip, unlike a bare equality check.
|
||||
function describeNumber(value: number): string {
|
||||
return Object.is(value, -0) ? '-0' : String(value)
|
||||
}
|
||||
|
||||
function expectSameNumber(actual: number, expected: number, label: string): void {
|
||||
expect(`${label} = ${describeNumber(actual)}`).toBe(`${label} = ${describeNumber(expected)}`)
|
||||
}
|
||||
|
||||
function sparseWindow(size: number, filled: Record<number, number>): number[] {
|
||||
const values: number[] = new Array<number>(size)
|
||||
for (const [index, value] of Object.entries(filled)) values[Number(index)] = value
|
||||
return values
|
||||
}
|
||||
|
||||
// Lehmer generator: stays inside the safe-integer range so the window is
|
||||
// byte-identical on every engine the relay runs on.
|
||||
function deterministicWindow(size: number): number[] {
|
||||
let seed = 20_260_912
|
||||
return Array.from({ length: size }, () => {
|
||||
seed = (seed * 48_271) % 2_147_483_647
|
||||
return (seed % 4_000_000) / 1_000
|
||||
})
|
||||
}
|
||||
|
||||
const DENSE_WINDOWS: Array<{ name: string; values: number[] }> = [
|
||||
{ name: 'empty', values: [] },
|
||||
{ name: 'single', values: [7.5] },
|
||||
{ name: 'single negative', values: [-7.5] },
|
||||
{ name: 'ascending', values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] },
|
||||
{ name: 'descending', values: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] },
|
||||
{ name: 'duplicates', values: [4, 4, 4, 4, 4] },
|
||||
// The trap: a sorted last element reads -1 here, the zero-seeded fold reads 0.
|
||||
{ name: 'all negative', values: [-5, -1, -9, -3, -2] },
|
||||
{ name: 'mixed signs', values: [-2, 3, -7, 0, 11, -0.5] },
|
||||
{ name: 'signed zero', values: [0, -0, -0, 0] },
|
||||
{ name: 'negative then signed zero', values: [-3, -0, -1] },
|
||||
{ name: 'nan leading', values: [NaN, 5, 1, 9] },
|
||||
{ name: 'nan trailing', values: [5, 1, 9, NaN] },
|
||||
{ name: 'nan interleaved', values: [5, NaN, 1, NaN, 9] },
|
||||
{ name: 'all nan', values: [NaN, NaN, NaN] },
|
||||
{ name: 'positive infinity', values: [Infinity, 3, 1] },
|
||||
{ name: 'negative infinity', values: [-Infinity, -3, -1] },
|
||||
{ name: 'both infinities', values: [Infinity, -Infinity, 3, -Infinity] },
|
||||
{ name: 'infinities and nan', values: [Infinity, NaN, -Infinity, 0] },
|
||||
{ name: 'sub-millisecond rounding', values: [0.00049, 0.0005, 0.00051, 0.9995] },
|
||||
{ name: 'reservoir sized', values: deterministicWindow(CONTROL_RTT_RESERVOIR_LIMIT) }
|
||||
]
|
||||
|
||||
// Holes cannot reach the recorders, so they are exercised through `percentile`
|
||||
// alone — the surface `host-session-registry` also calls.
|
||||
const SPARSE_WINDOWS: Array<{ name: string; values: number[] }> = [
|
||||
{ name: 'all holes', values: sparseWindow(4, {}) },
|
||||
{ name: 'leading hole', values: sparseWindow(5, { 3: 8, 4: 2 }) },
|
||||
{ name: 'trailing hole', values: sparseWindow(5, { 0: 8, 1: 2 }) },
|
||||
{ name: 'interleaved holes', values: sparseWindow(6, { 0: 3, 2: -4, 5: 1 }) },
|
||||
{ name: 'holes with nan', values: sparseWindow(5, { 1: NaN, 3: 6 }) }
|
||||
]
|
||||
|
||||
const PERCENTILE_RANKS = [0, 0.05, 0.5, 0.9, 0.95, 0.99, 1]
|
||||
|
||||
type SortWork = { sorts: number; comparisons: number; copiedElements: number }
|
||||
|
||||
// Every sorted array here is a fresh spread copy, so its length is the number of
|
||||
// elements copied to produce it.
|
||||
function countSortWork(run: () => void): SortWork {
|
||||
const work: SortWork = { sorts: 0, comparisons: 0, copiedElements: 0 }
|
||||
const original = Array.prototype.sort
|
||||
const patched = Array.prototype as { sort: unknown }
|
||||
patched.sort = function <T>(this: T[], compare?: (left: T, right: T) => number): T[] {
|
||||
work.sorts++
|
||||
work.copiedElements += this.length
|
||||
return original.call(this, (left: T, right: T) => {
|
||||
work.comparisons++
|
||||
return compare ? compare(left, right) : String(left) < String(right) ? -1 : 1
|
||||
})
|
||||
}
|
||||
try {
|
||||
run()
|
||||
} finally {
|
||||
patched.sort = original
|
||||
}
|
||||
return work
|
||||
}
|
||||
|
||||
function summaryThroughFlush(samples: number[]): { p50: number; p95: number; max: number } {
|
||||
const entries: Array<Record<string, unknown>> = []
|
||||
const observability = new RelayObservability(
|
||||
{ role: 'cell', cellId: 'staging-c1', region: 'us-central1' },
|
||||
(entry) => entries.push(entry)
|
||||
)
|
||||
for (const sample of samples) observability.recordControlRenewal(sample, 'renewed')
|
||||
observability.flush(counts)
|
||||
const entry = entries[0]!
|
||||
return {
|
||||
p50: entry.controlRenewalLatencyMsP50 as number,
|
||||
p95: entry.controlRenewalLatencyMsP95 as number,
|
||||
max: entry.controlRenewalLatencyMsMax as number
|
||||
}
|
||||
}
|
||||
|
||||
describe('latency window summarisation', () => {
|
||||
it('matches the pre-change percentile on every edge-case window', () => {
|
||||
let compared = 0
|
||||
for (const { name, values } of [...DENSE_WINDOWS, ...SPARSE_WINDOWS]) {
|
||||
for (const rank of PERCENTILE_RANKS) {
|
||||
expectSameNumber(
|
||||
percentile(values, rank),
|
||||
legacyPercentile(values, rank),
|
||||
`${name} @ p${rank}`
|
||||
)
|
||||
compared++
|
||||
}
|
||||
}
|
||||
expect(compared).toBe((DENSE_WINDOWS.length + SPARSE_WINDOWS.length) * PERCENTILE_RANKS.length)
|
||||
})
|
||||
|
||||
it('matches the pre-change p50, p95 and maximum through a flush', () => {
|
||||
let compared = 0
|
||||
for (const { name, values } of DENSE_WINDOWS) {
|
||||
const actual = summaryThroughFlush(values)
|
||||
const expected = legacyLatencySummary(values)
|
||||
expectSameNumber(actual.p50, expected.p50, `${name} p50`)
|
||||
expectSameNumber(actual.p95, expected.p95, `${name} p95`)
|
||||
// The zero-seeded fold, not the sorted last element: all-negative and NaN
|
||||
// windows disagree between the two.
|
||||
expectSameNumber(actual.max, expected.max, `${name} max`)
|
||||
compared += 3
|
||||
}
|
||||
expect(compared).toBe(DENSE_WINDOWS.length * 3)
|
||||
// The trap, spelled out: the sorted window ends at -1 but the fold reports 0.
|
||||
expect(summaryThroughFlush([-5, -1, -9, -3, -2]).max).toBe(0)
|
||||
expect(Number.isNaN(summaryThroughFlush([5, NaN, 1]).max)).toBe(true)
|
||||
})
|
||||
|
||||
it('sorts each latency window once instead of once per rank', () => {
|
||||
const samples = deterministicWindow(CONTROL_RTT_RESERVOIR_LIMIT)
|
||||
const before = countSortWork(() => legacyLatencySummary(samples))
|
||||
const after = countSortWork(() => summaryThroughFlush(samples))
|
||||
|
||||
expect(before.sorts).toBe(2)
|
||||
expect(after.sorts).toBe(1)
|
||||
expect(before.copiedElements).toBe(2 * CONTROL_RTT_RESERVOIR_LIMIT)
|
||||
expect(after.copiedElements).toBe(CONTROL_RTT_RESERVOIR_LIMIT)
|
||||
// Identical input and comparator, so the dropped sort is exactly half the
|
||||
// comparator calls rather than an engine-specific constant.
|
||||
expect(before.comparisons).toBeGreaterThan(CONTROL_RTT_RESERVOIR_LIMIT)
|
||||
expect(after.comparisons).toBe(before.comparisons / 2)
|
||||
})
|
||||
|
||||
it('never sorts an empty window and leaves the caller window untouched', () => {
|
||||
const samples = [5, -1, NaN, 3, -0]
|
||||
const before = samples.map(describeNumber)
|
||||
expect(countSortWork(() => summaryThroughFlush([])).sorts).toBe(0)
|
||||
expect(countSortWork(() => percentile([], 0.95)).sorts).toBe(0)
|
||||
countSortWork(() => summaryThroughFlush(samples))
|
||||
percentile(samples, 0.5)
|
||||
expect(samples.map(describeNumber)).toEqual(before)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,10 +168,18 @@ const emptyDeltas = (): RelayMetricDeltas => ({
|
||||
controlActivityRecoveryFailures: 0
|
||||
})
|
||||
|
||||
function ascending(values: number[]): number[] {
|
||||
return [...values].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
// Holes and NaN land past the requested rank, so the fallback still applies.
|
||||
function nearestRank(sorted: number[], percentileRank: number): number {
|
||||
return sorted[Math.ceil(percentileRank * sorted.length) - 1] ?? 0
|
||||
}
|
||||
|
||||
export function percentile(values: number[], percentileRank: number): number {
|
||||
if (values.length === 0) return 0
|
||||
const sorted = [...values].sort((left, right) => left - right)
|
||||
return sorted[Math.ceil(percentileRank * sorted.length) - 1] ?? 0
|
||||
return nearestRank(ascending(values), percentileRank)
|
||||
}
|
||||
|
||||
function roundMs(value: number): number {
|
||||
@@ -179,11 +187,15 @@ function roundMs(value: number): number {
|
||||
}
|
||||
|
||||
// Spreading a window into Math.max blows the stack once a busy cell samples
|
||||
// enough of it, so the maximum is folded instead.
|
||||
// enough of it, so the maximum is folded instead. The fold is also not
|
||||
// interchangeable with the sorted last element: it is seeded with zero, so an
|
||||
// all-negative or NaN window reads differently.
|
||||
function latencySummary(samples: number[]): { p50: number; p95: number; max: number } {
|
||||
// One sorted copy serves both ranks.
|
||||
const sorted = samples.length === 0 ? samples : ascending(samples)
|
||||
return {
|
||||
p50: roundMs(percentile(samples, 0.5)),
|
||||
p95: roundMs(percentile(samples, 0.95)),
|
||||
p50: roundMs(nearestRank(sorted, 0.5)),
|
||||
p95: roundMs(nearestRank(sorted, 0.95)),
|
||||
max: roundMs(samples.reduce((highest, sample) => Math.max(highest, sample), 0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function parseRehomeTrustProbeArguments(argv, environment = process.env)
|
||||
|
||||
export async function probeRehomeTrust(config, dependencies = {}) {
|
||||
const fetchImpl = dependencies.fetch ?? fetch
|
||||
const response = await fetchAdminOnceMore(
|
||||
const request = () => fetchAdminOnceMore(
|
||||
fetchImpl,
|
||||
`${config.directorOrigin}/v1/admin/regional-rehome-trust-probe`,
|
||||
{
|
||||
@@ -55,9 +55,26 @@ export async function probeRehomeTrust(config, dependencies = {}) {
|
||||
},
|
||||
{ wait: dependencies.wait }
|
||||
)
|
||||
const body = await response.json().catch(() => ({}))
|
||||
let response = await request()
|
||||
let body = await response.json().catch(() => ({}))
|
||||
// The director wraps source HTTP failures in 409; retry only explicit transient statuses.
|
||||
if (response.status === 409 && /^regional_rehome_trust_probe_source_(500|502|503|504)$/.test(body?.error ?? '')) {
|
||||
await (dependencies.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(2_000)
|
||||
response = await request()
|
||||
body = await response.json().catch(() => ({}))
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`application-mediated rehome trust probe returned ${response.status}`)
|
||||
const safeReasons = new Set([
|
||||
'invalid_token', 'director_only', 'invalid_request',
|
||||
'regional_rehome_trust_not_configured',
|
||||
'regional_rehome_trust_probe_source_unavailable',
|
||||
'regional_rehome_trust_probe_source_invalid_response',
|
||||
'regional_rehome_trust_probe_not_proven',
|
||||
...[400, 401, 403, 404, 409, 429, 500, 502, 503, 504]
|
||||
.map((status) => `regional_rehome_trust_probe_source_${status}`)
|
||||
])
|
||||
const reason = safeReasons.has(body?.error) ? body.error : 'unrecognized_error'
|
||||
throw new Error(`application-mediated rehome trust probe returned ${response.status}: ${reason}`)
|
||||
}
|
||||
if (
|
||||
body.v !== 1 ||
|
||||
|
||||
@@ -131,3 +131,40 @@ test('approves the asia-east2 rehome sources and still rejects unlisted cells',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('retries one director-wrapped source 503 without relaxing the proof', async () => {
|
||||
let calls = 0
|
||||
const result = await probeRehomeTrust(parseRehomeTrustProbeArguments(argv, environment), {
|
||||
wait: async () => {},
|
||||
fetch: async () => ++calls === 1
|
||||
? Response.json({ error: 'regional_rehome_trust_probe_source_503' }, { status: 409 })
|
||||
: Response.json(provenProbe)
|
||||
})
|
||||
assert.equal(calls, 2)
|
||||
assert.equal(result.proven, true)
|
||||
})
|
||||
|
||||
test('reports safe trust reasons, keeps rejection final, and redacts arbitrary error text', async () => {
|
||||
for (const reason of ['regional_rehome_trust_probe_source_403', 'secret-token-example']) {
|
||||
let calls = 0
|
||||
await assert.rejects(probeRehomeTrust(parseRehomeTrustProbeArguments(argv, environment), {
|
||||
wait: async () => { throw new Error('must not retry') },
|
||||
fetch: async () => { calls++; return Response.json({ error: reason }, { status: 409 }) }
|
||||
}), error => {
|
||||
assert.match(error.message, /returned 409/)
|
||||
assert.ok(!error.message.includes('secret-token-example'))
|
||||
if (reason.endsWith('_403')) assert.match(error.message, /source_403/)
|
||||
return true
|
||||
})
|
||||
assert.equal(calls, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('stops after the second wrapped transient failure', async () => {
|
||||
let calls = 0
|
||||
await assert.rejects(probeRehomeTrust(parseRehomeTrustProbeArguments(argv, environment), {
|
||||
wait: async () => {},
|
||||
fetch: async () => { calls++; return Response.json({ error: 'regional_rehome_trust_probe_source_503' }, { status: 409 }) }
|
||||
}), /returned 409.*source_503/)
|
||||
assert.equal(calls, 2)
|
||||
})
|
||||
|
||||
@@ -87,18 +87,21 @@ export function canaryAuthority(input) {
|
||||
}
|
||||
|
||||
export function verifyCanaryAuthority(authority, expected, repositoryRoot) {
|
||||
const selectorGeneration = Number(expected.selectorGeneration)
|
||||
if (
|
||||
authority?.v !== 1 ||
|
||||
!/^[0-9a-f]{40}$/.test(authority.commitSha ?? '') ||
|
||||
authority.runId !== expected.runId ||
|
||||
authority.targetDigest !== expected.targetDigest ||
|
||||
authority.rollbackDigest !== expected.rollbackDigest ||
|
||||
authority.selectorGeneration !== Number(expected.selectorGeneration) ||
|
||||
!Number.isSafeInteger(authority.selectorGeneration) ||
|
||||
authority.selectorGeneration < 0 ||
|
||||
!Number.isSafeInteger(selectorGeneration) ||
|
||||
selectorGeneration < authority.selectorGeneration ||
|
||||
authority.rehomeGeneration !== Number(expected.rehomeGeneration) ||
|
||||
!SAME_CAP_CELLS.includes(authority.cellId)
|
||||
) throw new Error('canary authority does not match this batch')
|
||||
// The batch dispatch resolves main after the canary sealed, so bind to the same code, not the
|
||||
// same SHA; every field above still pins this batch to that exact canary.
|
||||
// Each cell checks exact live selector state; later batches may reuse this control epoch's canary.
|
||||
requireSameEvidenceCode({
|
||||
sealedSha: authority.commitSha,
|
||||
currentSha: expected.commitSha,
|
||||
|
||||
@@ -109,6 +109,41 @@ test('seals and verifies canary authority for later batches', () => {
|
||||
}), /does not match/)
|
||||
})
|
||||
|
||||
test('reuses a canary across selector advances only within the same control epoch', () => {
|
||||
const authority = canaryAuthority({
|
||||
cellIds: 'production-gce-c7', targetDigest, rollbackDigest,
|
||||
confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} production-gce-c7`,
|
||||
commitSha: 'c'.repeat(40), runId: '42', selectorGeneration: '11', rehomeGeneration: '4'
|
||||
})
|
||||
const expected = {
|
||||
commitSha: 'c'.repeat(40), runId: '42', targetDigest, rollbackDigest,
|
||||
selectorGeneration: '21', rehomeGeneration: '4'
|
||||
}
|
||||
for (const generation of ['13', '14', '21', '29']) {
|
||||
assert.equal(verifyCanaryAuthority(authority, {
|
||||
...expected, selectorGeneration: generation
|
||||
}), authority)
|
||||
}
|
||||
for (const generation of ['12', '-1', 'NaN', 'Infinity', '13.5', '9007199254740992']) {
|
||||
assert.throws(() => verifyCanaryAuthority(authority, {
|
||||
...expected, selectorGeneration: generation
|
||||
}), /does not match/)
|
||||
}
|
||||
for (const generation of [-1, NaN, Infinity, 13.5, '13', Number.MAX_SAFE_INTEGER + 1]) {
|
||||
assert.throws(() => verifyCanaryAuthority({
|
||||
...authority, selectorGeneration: generation
|
||||
}, expected), /does not match/)
|
||||
}
|
||||
for (const mismatch of [
|
||||
{ rehomeGeneration: '3' }, { rehomeGeneration: '5' },
|
||||
{ targetDigest: rollbackDigest }, { rollbackDigest: targetDigest }, { runId: '43' }
|
||||
]) {
|
||||
assert.throws(() => verifyCanaryAuthority(authority, {
|
||||
...expected, ...mismatch
|
||||
}), /does not match/)
|
||||
}
|
||||
})
|
||||
|
||||
function gitIn(root, ...args) {
|
||||
return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim()
|
||||
}
|
||||
@@ -158,7 +193,7 @@ test('a batch trusts a canary sealed by identical code at an ancestor commit', a
|
||||
runId: '42',
|
||||
targetDigest,
|
||||
rollbackDigest,
|
||||
selectorGeneration: '13',
|
||||
selectorGeneration: '21',
|
||||
rehomeGeneration: '4'
|
||||
}, repositoryRoot)
|
||||
assert.equal(verifyAt(repository.sameCode, repository.root).cellId, 'production-gce-c7')
|
||||
|
||||
@@ -112,7 +112,8 @@ durably marked consumed before mutation and cannot authorize another run.
|
||||
| Director instances | outside 5–6 |
|
||||
| Director CPU or memory | over 80% |
|
||||
| Director concurrency | over 64 |
|
||||
| Unexpected director 5xx or auth 5xx in five minutes | over 0 |
|
||||
| Unexpected director 5xx in five minutes (excludes 503) | over 3 |
|
||||
| Auth 5xx in five minutes | over 0 |
|
||||
| Connections per cell process | over 500 |
|
||||
| Queued bytes per cell process | over 48 MiB |
|
||||
| Blocked or expired/unregistered migration | over 0 |
|
||||
@@ -276,3 +277,7 @@ without its segment is a compile error in relay-contract, not a silent gap.
|
||||
load the director's three-connection database pool.
|
||||
- Added private atomic state, idempotent JSONL checkpoints, and secret-safe Markdown evidence.
|
||||
- Added the manual production workflow. It has not been dispatched.
|
||||
|
||||
### Director error allowance (2026-09-12)
|
||||
|
||||
The serving-cell rollout observed three unexpected director 500 responses among approximately 33,600 responses in an hour, all two-second PostgreSQL connection timeouts. CPU remained near 30–37% and the zero-error bar repeatedly prevented any cell mutation. The five-minute allowance is now three non-503 director 5xx; four freezes. Auth errors, data freshness, active probes, SQL/pool pressure and other limits are unchanged. This is a bounded operational allowance, not a calibrated SLO or proof that intermittent failures are resolved; persistent low-frequency errors below this limit still require diagnosis.
|
||||
|
||||
@@ -35,7 +35,7 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
|
||||
]
|
||||
const WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS = [
|
||||
'@vscode/windows-process-tree',
|
||||
'windows-native-registry'
|
||||
'@orca/windows-registry'
|
||||
]
|
||||
|
||||
const NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"updatedAt": "2026-08-31",
|
||||
"updatedAt": "2026-09-11",
|
||||
"policy": {
|
||||
"maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"],
|
||||
"blockingPromotion": {
|
||||
@@ -10,6 +10,78 @@
|
||||
}
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"id": "agent-session.journal-streaming-replay",
|
||||
"title": "Journal replay bounds obsolete revision memory without changing recovery",
|
||||
"maturity": "experimental",
|
||||
"protection": "partial",
|
||||
"owner": "agent-session-runtime",
|
||||
"layer": "runtime-unit",
|
||||
"surfaces": ["structured chat journal replay", "structured chat recovery"],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "remote-runtime"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local"],
|
||||
"coverageNotes": "Production SQLite and reducer tests on macOS. Execution-host-local storage behavior is shared by remote runtimes; no live SSH or Windows/Linux run. PTY, daemon, WSL process launch, transport framing and mobile rendering are unaffected.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/blob/main/src/main/native-chat/agent-session-journal/journal-open.ts"
|
||||
],
|
||||
"invariant": "Replay preserves latest revisions, original item order, fences, aliases, submissions, repair precedence, read-only schema latching and cursor cleanup while retaining live items rather than all historical bodies.",
|
||||
"oracle": "Replay 2,048 16 KiB revisions into one latest item with less than 8 MiB sampled live heap growth; preserve prefix and future-schema latching after a gap, malformed suffix repair precedence, and hold no SQLite read snapshot across reduction (a mid-replay checkpoint is not busy). Existing journal and subscriber tests cover replayed content and recovery.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts",
|
||||
"src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts",
|
||||
"assertions": [
|
||||
"releases superseded revision bodies while reducing a long journal",
|
||||
"holds no read snapshot while reducing, so a checkpoint can pass mid-replay",
|
||||
"keeps the prefix but latches read-only for a future row beyond a gap",
|
||||
"keeps gap repair precedence when a later row is malformed",
|
||||
"rejects an unanchored prefix before a later gap"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-09-11",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 7.71,
|
||||
"summary": "245 tests passed across 22 files. Retained-heap oracle fails on baseline at 68.6 MB and passes under 8 MiB with streaming; gap/schema and cursor-cleanup assertions passed."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 30,
|
||||
"scope": "Unit fixtures; p95 not established."
|
||||
},
|
||||
"flakeHistory": {
|
||||
"status": "not-started",
|
||||
"evidence": "Local candidate validation only; no CI soak."
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "complete",
|
||||
"evidence": "Production-function AB/BA benchmark samples 132.6 MB live heap in baseline vs 88-92 KB with streaming on a 66.7 MB revision-heavy journal. The retained-heap unit test fails against the original code and passes with streaming; value and cursor assertions pass in both implementations."
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
"evidence": "Paged SQLite reads (one completed statement per page) and immediate reduction retain reduced state plus one page of rows. No cursor or read snapshot outlives its statement, so a WAL checkpoint can pass mid-replay; no new polling, subprocess, provider call or wire change."
|
||||
},
|
||||
"knownGaps": [
|
||||
"No Windows/Linux runtime execution or real remote-host validation.",
|
||||
"Latest live message bodies still require memory proportional to their total size; this removes superseded-history retention, not live-history storage."
|
||||
],
|
||||
"promotionCriteria": ["Retain red/green heap and value assertions and complete CI soak."],
|
||||
"demotionRule": "Keep experimental until cross-platform and soak evidence; investigate failures without weakening content or memory assertions."
|
||||
},
|
||||
{
|
||||
"id": "runtime.connection-owned-host-status",
|
||||
"title": "Host status recovers with its owning connection",
|
||||
@@ -15544,6 +15616,93 @@
|
||||
],
|
||||
"demotionRule": "Cannot promote without metric artifacts and stable p95 runtime history."
|
||||
},
|
||||
{
|
||||
"id": "terminal-performance.daemon-ndjson-wire-parity-and-serialization",
|
||||
"title": "Daemon NDJSON preserves wire bytes within a serialization count budget",
|
||||
"maturity": "experimental",
|
||||
"protection": "partial",
|
||||
"owner": "terminal-performance",
|
||||
"layer": "daemon-provider-contract",
|
||||
"surfaces": ["daemon stream", "NDJSON framing", "stream data batching"],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["daemon", "ssh", "remote-runtime"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["daemon"],
|
||||
"coverageNotes": "Deterministic writer, batcher, droppability and NDJSON suites run locally on macOS with mocked socket/process boundaries. An SSH-shaped session ID is a string fixture, not SSH transport evidence. No live daemon, platform integration or mixed-version client/host pair is exercised; folder and git workspaces are not distinguished by these stream contracts.",
|
||||
"motivatingLinks": ["src/main/daemon/daemon-stream-data-split.ts"],
|
||||
"invariant": "Reusing an encoded unsplit metadata-free frame must preserve the previous writer's exact wire bytes and chunk boundaries while reducing that path to one encode; oversized and metadata-bearing writes retain existing semantics and transformed writes remain uncapped single frames.",
|
||||
"oracle": "Compare emitted lines to the previous writer algorithm across byte caps, escaped and Unicode payloads, session IDs, raw lengths, sequence numbers and transformed spans; assert exact newline framing, reconstruct split payloads, check surrogate boundaries and sequence spans, and count encodeNdjson calls.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/daemon-stream-data-split.test.ts src/main/daemon/daemon-stream-data-batcher.test.ts src/main/daemon/daemon-stream-droppable-membership.test.ts src/main/daemon/daemon-stream-droppability-lifecycle.test.ts src/main/daemon/ndjson.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/main/daemon/daemon-stream-data-split.test.ts",
|
||||
"src/main/daemon/daemon-stream-data-batcher.test.ts",
|
||||
"src/main/daemon/daemon-stream-droppable-membership.test.ts",
|
||||
"src/main/daemon/daemon-stream-droppability-lifecycle.test.ts",
|
||||
"src/main/daemon/ndjson.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "src/main/daemon/daemon-stream-data-split.test.ts",
|
||||
"assertions": [
|
||||
"encodes an unsplit metadata-free frame once: %j",
|
||||
"reuses the encoded frame exactly at the inclusive byte cap",
|
||||
"does not add a duplicate full-data sizing probe to oversized writes",
|
||||
"keeps transformed writes at one encode without applying the ordinary byte cap",
|
||||
"preserves exact frames, chunk boundaries and metadata across payloads and caps",
|
||||
"keeps JSON escaping, Unicode and newline framing byte-for-byte",
|
||||
"keeps split frames within the byte cap and preserves code points and sequence spans"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/daemon/daemon-stream-data-batcher.test.ts",
|
||||
"assertions": ["writes large stream data as parser-sized NDJSON events"]
|
||||
},
|
||||
{
|
||||
"file": "src/main/daemon/ndjson.test.ts",
|
||||
"assertions": ["measures multibyte payloads in UTF-8 bytes, not characters"]
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-09-11",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/daemon-stream-data-split.test.ts src/main/daemon/daemon-stream-data-batcher.test.ts src/main/daemon/daemon-stream-droppable-membership.test.ts src/main/daemon/daemon-stream-droppability-lifecycle.test.ts src/main/daemon/ndjson.test.ts",
|
||||
"result": "passed",
|
||||
"summary": "Five daemon suites passed: 70 tests total, including the new 12-test splitter suite. Vitest reported 14.76 seconds; no app or live transport validation was performed.",
|
||||
"durationSeconds": 14.76
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 60,
|
||||
"scope": "Target budget for the five deterministic suites; one local Vitest run took 14.76 seconds, not an established p95. Native-runtime setup is excluded from the reported Vitest duration."
|
||||
},
|
||||
"flakeHistory": {
|
||||
"status": "not-started",
|
||||
"evidence": "Fresh local five-suite run passed; no sustained CI soak history is established."
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "partial",
|
||||
"evidence": "Historical report states serialization-count tests failed against the old writer. That baseline run was not repeated for this registration; the fresh candidate run passed all 70 tests. The previous-writer oracle checks byte parity, not live cross-version compatibility."
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
"evidence": "Unsplit metadata-free frames, including the inclusive byte-cap boundary, require exactly one encodeNdjson call; oversized writes must not exceed the previous writer's encode count; transformed writes require one encode. These are deterministic call-count budgets, not measured throughput, CPU, heap or input-latency improvements."
|
||||
},
|
||||
"promotionCriteria": [
|
||||
"Capture reproducible old-writer red and candidate green artifacts for the serialization-count assertions.",
|
||||
"Collect CI soak evidence before promotion and validate Linux/Windows runtimes and live SSH/remote and mixed-version pairs before claiming those integrations."
|
||||
],
|
||||
"knownGaps": [
|
||||
"No live daemon/Electron, Linux, Windows, WSL, SSH, remote-runtime or mixed-version client/host evidence.",
|
||||
"The historical red run has not been independently reproduced for this registration; the parity oracle reuses current splitter/encoder helpers.",
|
||||
"No sustained soak, measured p95, wall-clock performance benchmark or native socket backpressure guarantee.",
|
||||
"Byte-cap assertions cover ordinary split frames at a viable cap; tiny caps and transformed frames retain legacy behavior rather than gaining a universal cap guarantee."
|
||||
],
|
||||
"demotionRule": "Keep experimental until reproducible red/green and soak evidence exist; do not relax exact wire-byte, chunk-boundary or encode-count assertions to hide regressions."
|
||||
},
|
||||
{
|
||||
"id": "terminal-performance.daemon-stream-backpressure",
|
||||
"title": "Daemon terminal streams respect socket backpressure under output floods",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Run: node --expose-gc config/scripts/agent-lineage-cycle-cleanup-benchmark.mjs [baseline-ref]
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const sourcePath = 'src/renderer/src/components/dashboard/agent-row-lineage-model.ts'
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const beforeSource = execFileSync('git', ['show', `${baseline}:${sourcePath}`], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true
|
||||
})
|
||||
async function load(source) {
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
return (await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`))
|
||||
.buildAgentRowLineageTree
|
||||
}
|
||||
const before = await load(beforeSource)
|
||||
const after = await load(await readFile(sourcePath, 'utf8'))
|
||||
|
||||
function row(index, parent) {
|
||||
return {
|
||||
paneKey: `pane-${index}`,
|
||||
entry: {
|
||||
terminalHandle: `term-${index}`,
|
||||
orchestration: parent === undefined ? undefined : { parentPaneKey: `pane-${parent}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise duplicate keys, disconnected cycles, missing parents, and handle fallback.
|
||||
let seed = 7391
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return seed % max
|
||||
}
|
||||
for (let sample = 0; sample < 500; sample++) {
|
||||
const rows = Array.from({ length: 40 }, () => {
|
||||
const value = row(random(30), random(40))
|
||||
value.entry.orchestration.parentTerminalHandle = `term-${random(40)}`
|
||||
value.entry.orchestration.coordinatorHandle = `term-${random(40)}`
|
||||
return value
|
||||
})
|
||||
if (sample % 2 === 0) {
|
||||
rows.unshift(row('root', undefined))
|
||||
}
|
||||
assert.deepEqual(after(rows), before(rows))
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const [shape, count] of [
|
||||
['flat', 1000],
|
||||
['all-cycles', 1000],
|
||||
['mixed-cycles', 100],
|
||||
['mixed-cycles', 500],
|
||||
['mixed-cycles', 1000]
|
||||
]) {
|
||||
const rows = Array.from({ length: count }, (_, index) =>
|
||||
row(index, shape === 'flat' ? undefined : index ^ 1)
|
||||
)
|
||||
if (shape === 'mixed-cycles') {
|
||||
rows.unshift(row('root', undefined))
|
||||
}
|
||||
assert.deepEqual(after(rows), before(rows))
|
||||
for (let warmup = 0; warmup < 30; warmup++) {
|
||||
before(rows)
|
||||
after(rows)
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
global.gc?.()
|
||||
const run = arm === 'before' ? before : after
|
||||
const cpu = process.cpuUsage()
|
||||
const start = performance.now()
|
||||
for (let iteration = 0; iteration < 30; iteration++) {
|
||||
run(rows)
|
||||
}
|
||||
const wallMs = (performance.now() - start) / 30
|
||||
const used = process.cpuUsage(cpu)
|
||||
samples[arm].push({ wallMs, cpuMs: (used.user + used.system) / 30_000 })
|
||||
}
|
||||
}
|
||||
results.push({ shape, count, samples })
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, parityGraphs: 500, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <ref>:src/renderer/src/components/dashboard/agent-row-lineage-model.ts | node config/scripts/agent-lineage-reachability-benchmark.mjs
|
||||
async function load(source) {
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
return (await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`))
|
||||
.buildAgentRowLineageTree
|
||||
}
|
||||
const implementations = {
|
||||
before: await load(readFileSync(0, 'utf8')),
|
||||
after: await load(
|
||||
readFileSync('src/renderer/src/components/dashboard/agent-row-lineage-model.ts', 'utf8')
|
||||
)
|
||||
}
|
||||
function orderedTree(tree) {
|
||||
return {
|
||||
roots: tree.rootRows,
|
||||
children: [...tree.childrenByParentPaneKey],
|
||||
childKeys: [...tree.childPaneKeys]
|
||||
}
|
||||
}
|
||||
|
||||
let seed = 42
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 2 ** 32) * max)
|
||||
}
|
||||
let differentialCases = 0
|
||||
for (let trial = 0; trial < 5000; trial += 1) {
|
||||
const count = random(100)
|
||||
const rows = Object.freeze(
|
||||
Array.from({ length: count }, (_, index) =>
|
||||
Object.freeze({
|
||||
paneKey: `pane-${random(count + 4)}`,
|
||||
index,
|
||||
entry: Object.freeze({
|
||||
terminalHandle: random(2) ? `term-${random(count)}` : undefined,
|
||||
orchestration: Object.freeze({
|
||||
parentPaneKey: random(3) ? `pane-${random(count + 4)}` : undefined,
|
||||
parentTerminalHandle: random(2) ? `term-${random(count)}` : undefined,
|
||||
coordinatorHandle: random(2) ? `term-${random(count)}` : undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
assert.deepEqual(
|
||||
orderedTree(implementations.after(rows)),
|
||||
orderedTree(implementations.before(rows))
|
||||
)
|
||||
differentialCases += 1
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const count of [8, 32, 128, 512, 1024]) {
|
||||
for (const shape of ['flat', 'fanout', 'balanced', 'chain']) {
|
||||
const rows = Array.from({ length: count }, (_, index) => {
|
||||
const parent =
|
||||
shape === 'fanout' ? 0 : shape === 'balanced' ? Math.floor((index - 1) / 4) : index - 1
|
||||
return {
|
||||
paneKey: `pane-${index}`,
|
||||
entry: {
|
||||
orchestration:
|
||||
index > 0 && shape !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
const expected = orderedTree(implementations.before(rows))
|
||||
assert.deepEqual(orderedTree(implementations.after(rows)), expected)
|
||||
const iterations = Math.max(5, Math.floor(10_000 / count))
|
||||
for (let warmup = 0; warmup < 20; warmup += 1) {
|
||||
implementations.before(rows)
|
||||
implementations.after(rows)
|
||||
}
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
let result
|
||||
const started = performance.now()
|
||||
for (let repeat = 0; repeat < iterations; repeat += 1) {
|
||||
result = implementations[arm](rows)
|
||||
}
|
||||
samples[arm].push(performance.now() - started)
|
||||
assert.deepEqual(orderedTree(result), expected)
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
shape,
|
||||
iterations,
|
||||
meanMicrosecondsPerTree: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
),
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialCases, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe the baseline check-job-log-tail-slice.ts on stdin; both arms use the actual UTF-8 implementation.
|
||||
const entry = path.resolve('src/shared/check-job-log-tail-slice.ts')
|
||||
const sources = [readFileSync(0, 'utf8'), readFileSync(entry, 'utf8')]
|
||||
assert(sources.every((source) => source.includes('export function sliceCheckLogTail')))
|
||||
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'log-excerpt-source',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /check-job-log-tail-slice\.ts$/ }, () => ({
|
||||
contents: source,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(entry)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundled = `${result.outputFiles[0].text}\n//# sourceURL=check-log-byte-cap-benchmark-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(bundled).toString('base64')}`)
|
||||
}
|
||||
|
||||
const modules = await Promise.all(sources.map(load))
|
||||
const arms = modules.map((module) => module.sliceCheckLogTail)
|
||||
const limit = modules[0].PR_CHECK_LOG_TAIL_BYTES
|
||||
assert.equal(modules[1].PR_CHECK_LOG_TAIL_BYTES, limit)
|
||||
let seed = 0xc0ffee16
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return (seed >>> 8) % max
|
||||
}
|
||||
|
||||
let comparisons = 0
|
||||
function compare(text) {
|
||||
const expected = arms[0](text)
|
||||
assert.equal(arms[1](text), expected)
|
||||
assert(Buffer.byteLength(expected, 'utf8') <= limit)
|
||||
comparisons++
|
||||
return expected
|
||||
}
|
||||
|
||||
const units = ['x', 'é', '界', '😀', '\ud83d', '\udc00', 'x\ud83d界\udc00']
|
||||
for (const unit of units) {
|
||||
for (let delta = -4; delta <= 4; delta++) {
|
||||
const text = unit.repeat(Math.floor(limit / Buffer.byteLength(unit)) + delta)
|
||||
compare(text)
|
||||
compare(`error: ${text}\n${'recent\n'.repeat(103)}`)
|
||||
}
|
||||
}
|
||||
const endings = ['\n', '\r\n', '\r', '']
|
||||
const tokens = [
|
||||
'plain text',
|
||||
'##[error]',
|
||||
'::error::',
|
||||
'error:',
|
||||
'FAILED',
|
||||
'exit code',
|
||||
'ENOENT',
|
||||
'EACCES',
|
||||
'panic:',
|
||||
'AssertionError',
|
||||
'emoji 😀',
|
||||
'\ud83d',
|
||||
'\udc00',
|
||||
'\0',
|
||||
'界',
|
||||
'é',
|
||||
'\r'
|
||||
]
|
||||
for (let iteration = 0; iteration < 5000; iteration++) {
|
||||
const rows = Array.from({ length: random(250) }, (_, index) => {
|
||||
const token = tokens[random(tokens.length)]
|
||||
if (index === 0 && iteration % 20 === 0) {
|
||||
return `${token}${units[random(units.length)].repeat(limit + random(4))}`
|
||||
}
|
||||
return `${token} ${index} ${units[random(units.length)].repeat(random(30))}`
|
||||
})
|
||||
compare(rows.join(endings[random(endings.length)]) + endings[random(endings.length)])
|
||||
}
|
||||
console.log(`${comparisons} full-output differential cases passed`)
|
||||
|
||||
const workloads = [
|
||||
['short ASCII', 'log '.repeat(16)],
|
||||
['short Unicode', '🦀界'.repeat(20)],
|
||||
['8KiB ASCII', 'x'.repeat(8192)],
|
||||
['16KiB ASCII exact cap', 'x'.repeat(limit)],
|
||||
['8Ki code units / 24KiB Unicode', '界'.repeat(8192)],
|
||||
['2MiB ASCII line', 'x'.repeat(2 * 1024 * 1024)],
|
||||
['8MiB ASCII line', 'x'.repeat(8 * 1024 * 1024)],
|
||||
['2MiB Unicode line', '界'.repeat(Math.floor((2 * 1024 * 1024) / 3))],
|
||||
[
|
||||
'2MiB earlier error context',
|
||||
`error: ${'x'.repeat(2 * 1024 * 1024)}\n${'recent\n'.repeat(103)}`
|
||||
],
|
||||
[
|
||||
'220 ordinary lines',
|
||||
Array.from({ length: 220 }, (_, i) => `line ${i} ${'text'.repeat(8)}`).join('\n')
|
||||
],
|
||||
[
|
||||
'220 lines / small earlier error',
|
||||
Array.from(
|
||||
{ length: 220 },
|
||||
(_, i) => `${i === 30 ? 'error:' : 'line'} ${i} ${'text'.repeat(8)}`
|
||||
).join('\n')
|
||||
]
|
||||
]
|
||||
|
||||
function sample(arm, input, expected, repeats) {
|
||||
const started = performance.now()
|
||||
let output
|
||||
for (let i = 0; i < repeats; i++) {
|
||||
output = arm(input)
|
||||
}
|
||||
const elapsed = (performance.now() - started) / repeats
|
||||
assert.equal(output, expected)
|
||||
return elapsed
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
pairs: 8,
|
||||
unit: 'ms'
|
||||
})
|
||||
)
|
||||
for (const [name, input] of workloads) {
|
||||
const expected = compare(input)
|
||||
for (const arm of arms) {
|
||||
const until = performance.now() + 80
|
||||
while (performance.now() < until) {
|
||||
sample(arm, input, expected, 1)
|
||||
}
|
||||
}
|
||||
const repeats = Math.max(1, Math.min(100000, Math.ceil(40 / sample(arms[0], input, expected, 1))))
|
||||
/** @type {number[][]} */
|
||||
const samples = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
samples[index].push(sample(arms[index], input, expected, repeats))
|
||||
}
|
||||
}
|
||||
const median = samples.map((values) => {
|
||||
values.sort((a, b) => a - b)
|
||||
return (values[3] + values[4]) / 2
|
||||
})
|
||||
console.log(JSON.stringify({ name, repeats, median, samples }))
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
// Why: the READMEs embed media from trees other jobs own (docs-site public media,
|
||||
// generated feature-wall tiles), and the docs-only classifier skips the whole CI
|
||||
// matrix for one of them. GitHub renders only committed files, so this checks the
|
||||
// git index rather than the working tree.
|
||||
const TRANSLATED_README_DIR = path.join('docs', 'readme')
|
||||
const EXTERNAL_TARGET = /^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i
|
||||
const HTML_ATTRIBUTE = /\b(?:src|srcset|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
|
||||
const MARKDOWN_LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
||||
|
||||
function readmeFiles(root) {
|
||||
const translated = readdirSync(path.join(root, TRANSLATED_README_DIR))
|
||||
.filter((name) => name.endsWith('.md'))
|
||||
.sort()
|
||||
.map((name) => path.posix.join('docs', 'readme', name))
|
||||
return ['README.md', ...translated]
|
||||
}
|
||||
|
||||
// Why only the referenced paths: a full `git ls-files` of this repo overflows the
|
||||
// default child buffer; asking about a few dozen pathspecs stays bounded.
|
||||
function trackedFiles(root, candidates) {
|
||||
if (candidates.length === 0) {
|
||||
return new Set()
|
||||
}
|
||||
const stdout = execFileSync(
|
||||
'git',
|
||||
['--literal-pathspecs', 'ls-files', '-z', '--', ...candidates],
|
||||
{ cwd: root, encoding: 'utf8' }
|
||||
)
|
||||
return new Set(stdout.split('\0').filter(Boolean))
|
||||
}
|
||||
|
||||
function* localTargets(markdown) {
|
||||
for (const match of markdown.matchAll(HTML_ATTRIBUTE)) {
|
||||
// Why: srcset is a candidate list ("a.gif 1x, b.gif 2x"); each entry starts with a URL.
|
||||
for (const candidate of (match[1] ?? match[2]).split(',')) {
|
||||
const target = candidate.trim().split(/\s+/)[0]
|
||||
if (target) {
|
||||
yield target
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const match of markdown.matchAll(MARKDOWN_LINK)) {
|
||||
yield match[1].replace(/^<|>$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTarget(readme, target) {
|
||||
const bare = target.split(/[?#]/)[0]
|
||||
if (!bare) {
|
||||
return null
|
||||
}
|
||||
const resolved = path.posix.normalize(
|
||||
path.posix.join(path.posix.dirname(readme), decodeURIComponent(bare))
|
||||
)
|
||||
return resolved.startsWith('../') ? null : resolved
|
||||
}
|
||||
|
||||
function collectLinks(root) {
|
||||
const links = []
|
||||
for (const readme of readmeFiles(root)) {
|
||||
const markdown = readFileSync(path.join(root, readme), 'utf8')
|
||||
for (const target of new Set(localTargets(markdown))) {
|
||||
if (EXTERNAL_TARGET.test(target)) {
|
||||
continue
|
||||
}
|
||||
links.push({ readme, target, resolved: resolveTarget(readme, target) })
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
export function findBrokenReadmeLinks(root) {
|
||||
const links = collectLinks(root)
|
||||
const candidates = [...new Set(links.map((link) => link.resolved).filter(Boolean))]
|
||||
const tracked = trackedFiles(root, candidates)
|
||||
return links.filter(({ resolved }) => resolved === null || !tracked.has(resolved))
|
||||
}
|
||||
|
||||
export function main(root = process.cwd()) {
|
||||
const broken = findBrokenReadmeLinks(root)
|
||||
if (broken.length > 0) {
|
||||
console.error(`README local link check failed with ${broken.length} broken link(s):`)
|
||||
for (const { readme, target, resolved } of broken) {
|
||||
console.error(
|
||||
`- ${readme}: ${target} -> ${resolved ?? 'outside the repository'} is not tracked`
|
||||
)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
console.log('README local link check passed.')
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(main())
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { findBrokenReadmeLinks, main } from './check-readme-local-links.mjs'
|
||||
|
||||
const projectDir = path.resolve(import.meta.dirname, '../..')
|
||||
const tempDirs = []
|
||||
|
||||
function git(cwd, args) {
|
||||
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim()
|
||||
}
|
||||
|
||||
function writeFiles(root, files) {
|
||||
for (const [relativePath, contents] of Object.entries(files)) {
|
||||
const target = path.join(root, relativePath)
|
||||
mkdirSync(path.dirname(target), { recursive: true })
|
||||
writeFileSync(target, contents)
|
||||
}
|
||||
}
|
||||
|
||||
function makeFixture(files, { untracked = {} } = {}) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'orca-readme-links-'))
|
||||
tempDirs.push(root)
|
||||
git(root, ['init', '--quiet'])
|
||||
git(root, ['config', 'user.email', 'readme-links-test@example.com'])
|
||||
git(root, ['config', 'user.name', 'README Links Test'])
|
||||
writeFiles(root, files)
|
||||
git(root, ['add', '-A'])
|
||||
git(root, ['commit', '--quiet', '-m', 'fixture'])
|
||||
writeFiles(root, untracked)
|
||||
return root
|
||||
}
|
||||
|
||||
const validReadmes = {
|
||||
'README.md': [
|
||||
'<img src="resources/build/icon.png" />',
|
||||
'<picture><source srcset="docs/site/public/docs/tab-split.gif" type="image/gif"><img src="resources/onboarding/feature-wall/tile-01.poster.jpg" /></picture>',
|
||||
'<a href="docs/readme/README.ja.md">日本語</a>',
|
||||
"<img src='resources/build/icon.png' />",
|
||||
'<img src="https://img.shields.io/badge/x-y-z" />',
|
||||
'[Contributing](.github/CONTRIBUTING.md) [Docs](https://example.com/docs) [Top](#top)',
|
||||
''
|
||||
].join('\n'),
|
||||
'docs/readme/README.ja.md': [
|
||||
'<img src="../../resources/build/icon.png" />',
|
||||
'<source srcset="../site/public/docs/tab-split.gif">',
|
||||
'<a href="../../README.md">English</a> <a href="README.ja.md#top">self</a>',
|
||||
'[LICENSE](../../LICENSE)'
|
||||
].join('\n'),
|
||||
'resources/build/icon.png': 'png',
|
||||
'resources/onboarding/feature-wall/tile-01.poster.jpg': 'jpg',
|
||||
'docs/site/public/docs/tab-split.gif': 'gif',
|
||||
'docs/assets/hero image.jpg': 'jpg',
|
||||
'.github/CONTRIBUTING.md': 'contributing',
|
||||
LICENSE: 'mit'
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { force: true, recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('README local link check', () => {
|
||||
it('accepts the checked-in READMEs', () => {
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
expect(main(projectDir)).toBe(0)
|
||||
})
|
||||
|
||||
it('accepts local links in every supported shape', () => {
|
||||
expect(findBrokenReadmeLinks(makeFixture(validReadmes))).toEqual([])
|
||||
})
|
||||
|
||||
it('reports a deleted media file for the root and translated READMEs', () => {
|
||||
const { 'docs/site/public/docs/tab-split.gif': _gif, ...files } = validReadmes
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const root = makeFixture(files)
|
||||
|
||||
expect(findBrokenReadmeLinks(root)).toEqual([
|
||||
{
|
||||
readme: 'README.md',
|
||||
target: 'docs/site/public/docs/tab-split.gif',
|
||||
resolved: 'docs/site/public/docs/tab-split.gif'
|
||||
},
|
||||
{
|
||||
readme: 'docs/readme/README.ja.md',
|
||||
target: '../site/public/docs/tab-split.gif',
|
||||
resolved: 'docs/site/public/docs/tab-split.gif'
|
||||
}
|
||||
])
|
||||
expect(main(root)).toBe(1)
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('README.md: docs/site/public/docs/tab-split.gif')
|
||||
)
|
||||
})
|
||||
|
||||
// Why: GitHub renders the commit, so a file that only exists on disk is broken.
|
||||
it('reports a referenced file that exists on disk but is not tracked', () => {
|
||||
const { 'resources/build/icon.png': icon, ...files } = validReadmes
|
||||
const root = makeFixture(files, { untracked: { 'resources/build/icon.png': icon } })
|
||||
|
||||
expect(findBrokenReadmeLinks(root).map((link) => link.resolved)).toEqual([
|
||||
'resources/build/icon.png',
|
||||
'resources/build/icon.png'
|
||||
])
|
||||
})
|
||||
|
||||
it('reports a link that escapes the repository', () => {
|
||||
const root = makeFixture({
|
||||
...validReadmes,
|
||||
'docs/readme/README.ja.md': '<img src="../../../outside.png" />'
|
||||
})
|
||||
|
||||
expect(findBrokenReadmeLinks(root)).toEqual([
|
||||
{ readme: 'docs/readme/README.ja.md', target: '../../../outside.png', resolved: null }
|
||||
])
|
||||
})
|
||||
|
||||
// Why: a single-quoted attribute is valid HTML and GitHub renders it, so a parser
|
||||
// that only reads double quotes would pass a README with a broken image.
|
||||
it('reports a missing target in a single-quoted attribute', () => {
|
||||
const files = {
|
||||
...validReadmes,
|
||||
'README.md': `${validReadmes['README.md']}\n<img src='docs/assets/missing.gif' />`
|
||||
}
|
||||
|
||||
expect(findBrokenReadmeLinks(makeFixture(files))).toEqual([
|
||||
{
|
||||
readme: 'README.md',
|
||||
target: 'docs/assets/missing.gif',
|
||||
resolved: 'docs/assets/missing.gif'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
// Why the ungated job: static_analysis is skipped for docs-only diffs, which is
|
||||
// exactly the kind of PR that deletes a docs-site GIF the README embeds.
|
||||
it('runs on every PR through the ungated guard job and in the lint script', () => {
|
||||
const { scripts } = JSON.parse(readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
|
||||
const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
|
||||
const guardJob = workflow.jobs.root_directory_guard
|
||||
const step = guardJob.steps.find((candidate) => candidate.name === 'Check README local links')
|
||||
|
||||
expect(guardJob.if).toBeUndefined()
|
||||
expect(guardJob.needs).toBeUndefined()
|
||||
expect(step.run).toBe('node config/scripts/check-readme-local-links.mjs')
|
||||
expect(scripts['check:readme-local-links']).toBe(
|
||||
'node config/scripts/check-readme-local-links.mjs'
|
||||
)
|
||||
expect(scripts.lint).toContain('pnpm run check:readme-local-links')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const read = (path) => parse(readFileSync(path, 'utf8'))
|
||||
const workflow = (name) => read(`.github/workflows/${name}.yml`)
|
||||
const action = read('.github/actions/install-node-dependencies/action.yml')
|
||||
|
||||
describe('CI dependency download caches', () => {
|
||||
it('scopes desktop stores to the root lockfile and lets mixed installs opt in', () => {
|
||||
expect(action.inputs['cache-dependency-path'].default).toBe('pnpm-lock.yaml')
|
||||
for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) {
|
||||
expect(step.with.cache).toBe('pnpm')
|
||||
expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}')
|
||||
}
|
||||
const install = action.runs.steps.find((step) => step.name === 'Install dependencies')
|
||||
expect(install.if).toBeUndefined()
|
||||
expect(install.run).toContain('pnpm install --frozen-lockfile --ignore-scripts')
|
||||
expect(install.run).toContain(
|
||||
'diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml'
|
||||
)
|
||||
const mobile = workflow('mobile').jobs.verify.steps.find((step) =>
|
||||
step.uses?.includes('install-node-dependencies')
|
||||
)
|
||||
expect(mobile.with['cache-dependency-path'].trim().split('\n')).toEqual([
|
||||
'pnpm-lock.yaml',
|
||||
'mobile/pnpm-lock.yaml'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import {
|
||||
balanceFiles,
|
||||
compareIds,
|
||||
readTimingBaseline,
|
||||
writeAssignment
|
||||
} from './ci-shard-assignment.mjs'
|
||||
|
||||
export function discoverE2eFiles(report) {
|
||||
if (report.errors?.length) {
|
||||
throw new Error('Playwright discovery reported errors')
|
||||
}
|
||||
const files = new Map()
|
||||
function visit(suite) {
|
||||
for (const spec of suite.specs ?? []) {
|
||||
const file = spec.file.replaceAll('\\', '/')
|
||||
if (file.startsWith('/') || file.split('/').includes('..') || /[\n\r>›]/.test(file)) {
|
||||
throw new Error(`Unsafe test-list path: ${file}`)
|
||||
}
|
||||
for (const test of spec.tests) {
|
||||
const id = `${test.projectName}:${spec.id}`
|
||||
const ids = files.get(file) ?? []
|
||||
ids.push(id)
|
||||
files.set(file, ids)
|
||||
}
|
||||
}
|
||||
for (const child of suite.suites ?? []) {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
for (const suite of report.suites) {
|
||||
visit(suite)
|
||||
}
|
||||
if (!files.size) {
|
||||
throw new Error('Playwright discovered no tests')
|
||||
}
|
||||
const ids = [...files.values()].flat()
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
throw new Error('Duplicate discovered test identity')
|
||||
}
|
||||
return Object.fromEntries([...files.entries()].sort(([a], [b]) => compareIds(a, b)))
|
||||
}
|
||||
|
||||
export function planE2e(report, count, baseline) {
|
||||
const testsByFile = discoverE2eFiles(report)
|
||||
const timings = Object.fromEntries(
|
||||
Object.entries(baseline.timings).map(([file, duration]) => [
|
||||
file.replace(/^tests\/e2e\//, ''),
|
||||
duration
|
||||
])
|
||||
)
|
||||
const assignment = balanceFiles(Object.keys(testsByFile), count, timings)
|
||||
return { ...assignment, testsByFile, baselineSha256: baseline.baselineSha256 }
|
||||
}
|
||||
|
||||
export function verifyE2eSelection(assignment, report) {
|
||||
const actual = Object.values(discoverE2eFiles(report)).flat().sort(compareIds)
|
||||
const expected = assignment.shards[assignment.selectedShard - 1].files
|
||||
.flatMap((file) => assignment.testsByFile[file])
|
||||
.sort(compareIds)
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error('Native Playwright selection differs from shard assignment')
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
if (process.argv[2] === '--verify') {
|
||||
verifyE2eSelection(
|
||||
JSON.parse(readFileSync(process.argv[3], 'utf8')),
|
||||
JSON.parse(readFileSync(process.argv[4], 'utf8'))
|
||||
)
|
||||
} else {
|
||||
const [input, shard, directory] = process.argv.slice(2)
|
||||
const match = shard?.match(/^(\d+)\/(\d+)$/)
|
||||
if (!input || !directory || !match) {
|
||||
throw new Error('Usage: ci-e2e-shard-plan.mjs DISCOVERY INDEX/COUNT OUTPUT_DIRECTORY')
|
||||
}
|
||||
const index = Number(match[1])
|
||||
const count = Number(match[2])
|
||||
if (index < 1 || index > count) {
|
||||
throw new Error('Invalid shard index')
|
||||
}
|
||||
const assignment = planE2e(
|
||||
JSON.parse(readFileSync(input, 'utf8')),
|
||||
count,
|
||||
readTimingBaseline('e2e')
|
||||
)
|
||||
const selected = assignment.shards[index - 1].files
|
||||
if (!selected.length) {
|
||||
throw new Error('Empty E2E shard')
|
||||
}
|
||||
writeAssignment(join(directory, 'assignment.json'), { ...assignment, selectedShard: index })
|
||||
writeFileSync(join(directory, 'selected.txt'), `${selected.join('\n')}\n`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { planE2e, verifyE2eSelection } from './ci-e2e-shard-plan.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
it('native Playwright test-list preserves full discovery, serial suites, skips and headful filtering', async () => {
|
||||
const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca-playwright-shards-')))
|
||||
const testPackage = JSON.stringify(require.resolve('@stablyai/playwright-test'))
|
||||
const config = join(directory, 'playwright.config.cjs')
|
||||
writeFileSync(
|
||||
config,
|
||||
`module.exports = { testDir: '.', fullyParallel: true, projects: [{ name: 'electron-headless', grepInvert: /@headful/ }] }`
|
||||
)
|
||||
for (let index = 0; index < 17; index++) {
|
||||
writeFileSync(
|
||||
join(directory, `file-${index}.spec.cjs`),
|
||||
`
|
||||
const { test } = require(${testPackage});
|
||||
test('normal', () => {});
|
||||
test.skip('skipped', () => {});
|
||||
test('visible @headful', () => {});
|
||||
test.describe.serial('serial', () => {
|
||||
test('first', () => {});
|
||||
test('second', () => {});
|
||||
});
|
||||
`
|
||||
)
|
||||
}
|
||||
async function discover(extra = []) {
|
||||
const result = await runProcess({
|
||||
program: process.execPath,
|
||||
cwd: directory,
|
||||
args: [
|
||||
join(dirname(require.resolve('playwright/package.json')), 'cli.js'),
|
||||
'test',
|
||||
'--config',
|
||||
config,
|
||||
'--list',
|
||||
'--reporter=json',
|
||||
...extra
|
||||
],
|
||||
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 20000
|
||||
})
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
return JSON.parse(result.stdout)
|
||||
}
|
||||
try {
|
||||
const full = await discover()
|
||||
const assignment = planE2e(full, 14, { timings: {} })
|
||||
const ids = []
|
||||
for (let index = 0; index < 14; index++) {
|
||||
const path = join(directory, 'selected.txt')
|
||||
writeFileSync(path, `${assignment.shards[index].files.join('\n')}\n`)
|
||||
const selected = await discover(['--test-list', path])
|
||||
verifyE2eSelection({ ...assignment, selectedShard: index + 1 }, selected)
|
||||
for (const suite of selected.suites) {
|
||||
expect(suite.specs.some((spec) => spec.title.includes('@headful'))).toBe(false)
|
||||
}
|
||||
ids.push(...assignment.shards[index].files.flatMap((file) => assignment.testsByFile[file]))
|
||||
}
|
||||
expect(ids).toHaveLength(17 * 4)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
expect(() => verifyE2eSelection({ ...assignment, selectedShard: 1 }, full)).toThrow('differs')
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}, 60000)
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
|
||||
export const compareIds = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
export function balanceFiles(files, count, timings, overheadMs = 0) {
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
throw new Error('Invalid shard count')
|
||||
}
|
||||
if (new Set(files).size !== files.length) {
|
||||
throw new Error('Duplicate discovered file')
|
||||
}
|
||||
const known = Object.values(timings).filter((value) => Number.isFinite(value) && value > 0)
|
||||
known.sort((a, b) => a - b)
|
||||
const fallbackMs = known[Math.floor(known.length / 2)] ?? 1000
|
||||
const weighted = files.map((file) => ({
|
||||
file,
|
||||
durationMs:
|
||||
(Number.isFinite(timings[file]) && timings[file] > 0 ? timings[file] : fallbackMs) +
|
||||
overheadMs
|
||||
}))
|
||||
weighted.sort((a, b) => b.durationMs - a.durationMs || compareIds(a.file, b.file))
|
||||
const shards = Array.from({ length: count }, () => ({ files: [], durationMs: 0 }))
|
||||
for (const entry of weighted) {
|
||||
const target = shards.reduce((best, shard) =>
|
||||
shard.durationMs < best.durationMs ||
|
||||
(shard.durationMs === best.durationMs && shard.files.length < best.files.length)
|
||||
? shard
|
||||
: best
|
||||
)
|
||||
target.files.push(entry.file)
|
||||
target.durationMs += entry.durationMs
|
||||
}
|
||||
for (const shard of shards) {
|
||||
shard.files.sort(compareIds)
|
||||
}
|
||||
const assigned = shards.flatMap((shard) => shard.files).sort(compareIds)
|
||||
if (JSON.stringify(assigned) !== JSON.stringify([...files].sort(compareIds))) {
|
||||
throw new Error('Shard coverage differs from discovery')
|
||||
}
|
||||
return { algorithm: 'file-lpt-v1', fallbackMs, overheadMs, shards }
|
||||
}
|
||||
|
||||
export function readTimingBaseline(suite) {
|
||||
const bytes = readFileSync(new URL('./ci-shard-timings.json', import.meta.url), 'utf8')
|
||||
const baseline = JSON.parse(bytes)
|
||||
return { ...baseline[suite], baselineSha256: createHash('sha256').update(bytes).digest('hex') }
|
||||
}
|
||||
|
||||
export function writeAssignment(path, assignment) {
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(
|
||||
path,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
sourceSha: process.env.ORCA_SHARD_SOURCE_SHA ?? process.env.GITHUB_SHA ?? null,
|
||||
runId: process.env.GITHUB_RUN_ID ?? null,
|
||||
runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
|
||||
...assignment
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BaseSequencer } from 'vitest/node'
|
||||
import { balanceFiles } from './ci-shard-assignment.mjs'
|
||||
import { discoverE2eFiles, planE2e } from './ci-e2e-shard-plan.mjs'
|
||||
import { parseTimingLog } from './ci-shard-timing-import.mjs'
|
||||
import TimingSequencer from './ci-unit-sequencer.mjs'
|
||||
|
||||
const directories = []
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('timing-weighted shard selection', () => {
|
||||
it('distributes long files, includes unknowns exactly once, and ignores discovery order', () => {
|
||||
const files = ['long', 'medium', 'short', 'unknown', 'new', 'zero', 'invalid']
|
||||
const timings = { long: 100, medium: 80, short: 20, zero: 0, invalid: -1, deleted: 20 }
|
||||
const plan = balanceFiles(files, 3, timings, 10)
|
||||
expect(plan).toEqual(balanceFiles(files.toReversed(), 3, timings, 10))
|
||||
expect(plan.fallbackMs).toBe(80)
|
||||
expect(plan.shards.flatMap((shard) => shard.files).sort()).toEqual([...files].sort())
|
||||
expect(Math.max(...plan.shards.map((shard) => shard.durationMs))).toBeLessThan(250)
|
||||
})
|
||||
|
||||
it('has a deterministic cold fallback and permits fewer files than shards', () => {
|
||||
expect(balanceFiles(['b', 'a'], 3, {}).shards).toEqual([
|
||||
{ files: ['a'], durationMs: 1000 },
|
||||
{ files: ['b'], durationMs: 1000 },
|
||||
{ files: [], durationMs: 0 }
|
||||
])
|
||||
expect(() => balanceFiles(['a', 'a'], 8, {})).toThrow('Duplicate')
|
||||
expect(() => balanceFiles(['a'], 0, {})).toThrow('count')
|
||||
})
|
||||
|
||||
it('uses the post-filter Vitest discovery unchanged across eight shards and retains default sort', async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-unit-shards-'))
|
||||
directories.push(directory)
|
||||
vi.stubEnv('ORCA_SHARD_MANIFEST', join(directory, 'assignment.json'))
|
||||
const specs = Array.from({ length: 37 }, (_, i) => ({
|
||||
moduleId: resolve(`src/fixture-${i}.test.ts`)
|
||||
}))
|
||||
const selected = []
|
||||
for (let index = 1; index <= 8; index++) {
|
||||
const sequencer = new TimingSequencer({
|
||||
config: { root: process.cwd(), shard: { index, count: 8 } }
|
||||
})
|
||||
expect(sequencer.sort).toBe(BaseSequencer.prototype.sort)
|
||||
selected.push(...(await sequencer.shard(specs)))
|
||||
const manifest = JSON.parse(readFileSync(join(directory, 'assignment.json'), 'utf8'))
|
||||
expect(manifest.selectedShard).toBe(index)
|
||||
expect(manifest.baselineSha256).toMatch(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
expect(new Set(selected).size).toBe(specs.length)
|
||||
expect(selected).toHaveLength(specs.length)
|
||||
expect(new Set(selected)).toEqual(new Set(specs))
|
||||
})
|
||||
|
||||
it('wires a constructor into the opt-in Vitest config', async () => {
|
||||
vi.stubEnv('ORCA_BALANCE_UNIT_SHARDS', '1')
|
||||
const { default: config } = await import('../vitest.config')
|
||||
expect(config.test.sequence.sequencer).toBe(TimingSequencer)
|
||||
})
|
||||
|
||||
it('keeps nested/serial E2E files atomic and fails closed on discovery errors', () => {
|
||||
const spec = (id, file) => ({ id, file, tests: [{ projectName: 'electron-headless' }] })
|
||||
const report = {
|
||||
suites: [
|
||||
{
|
||||
specs: [spec('a', 'one.spec.ts')],
|
||||
suites: [{ specs: [spec('b', 'one.spec.ts'), spec('c', 'two.spec.ts')] }]
|
||||
}
|
||||
]
|
||||
}
|
||||
const plan = planE2e(report, 14, { timings: { 'tests/e2e/one.spec.ts': 4000 } })
|
||||
expect(plan.shards.flatMap((shard) => shard.files).sort()).toEqual([
|
||||
'one.spec.ts',
|
||||
'two.spec.ts'
|
||||
])
|
||||
expect(plan.testsByFile['one.spec.ts']).toHaveLength(2)
|
||||
expect(() => discoverE2eFiles({ ...report, errors: [{}] })).toThrow('errors')
|
||||
expect(() => discoverE2eFiles({ suites: [] })).toThrow('no tests')
|
||||
expect(() =>
|
||||
discoverE2eFiles({ suites: [{ specs: [spec('a', '../escape.spec.ts')] }] })
|
||||
).toThrow('Unsafe')
|
||||
expect(() =>
|
||||
discoverE2eFiles({
|
||||
suites: [{ specs: [spec('a', 'one.spec.ts'), spec('a', 'one.spec.ts')] }]
|
||||
})
|
||||
).toThrow('Duplicate')
|
||||
})
|
||||
|
||||
it('imports ANSI unit timings and E2E failures without counting headful reruns', () => {
|
||||
const parsed = parseTimingLog(
|
||||
[
|
||||
'\u001b[32m✓\u001b[39m src/a.test.ts (2 tests) 35ms',
|
||||
'Duration 1s (transform 0.1s, setup 0.2s, import 0.3s, tests 0.04s, environment 0.4s)',
|
||||
'✓ 1 [electron-headless] › tests/e2e/a.spec.ts:1:1 › works (2s)',
|
||||
'✘ 2 [electron-headless] › tests/e2e/a.spec.ts:2:1 › fails (1.2m)',
|
||||
'✓ 3 [electron-headful] › tests/e2e/a.spec.ts:3:1 › benchmark (9s)'
|
||||
].join('\n')
|
||||
)
|
||||
expect(parsed).toEqual({
|
||||
unit: { 'src/a.test.ts': 35 },
|
||||
e2e: { 'tests/e2e/a.spec.ts': 74000 },
|
||||
overheadMs: 1000
|
||||
})
|
||||
})
|
||||
|
||||
it('reads mixed units from captured Vitest output', () => {
|
||||
const parsed = parseTimingLog(
|
||||
'Duration 5.14s (transform 952ms, setup 449ms, import 1.18s, tests 9.41s, environment 1ms)'
|
||||
)
|
||||
expect(parsed.overheadMs).toBe(2582)
|
||||
})
|
||||
|
||||
it('rejects incomplete unit evidence instead of silently dropping overhead', () => {
|
||||
expect(() => parseTimingLog('✓ src/a.test.ts (2 tests) 35ms')).toThrow('Duration summary')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { stripVTControlCharacters } from 'node:util'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export function parseTimingLog(text) {
|
||||
const clean = stripVTControlCharacters(text)
|
||||
const unit = {}
|
||||
const e2e = {}
|
||||
for (const match of clean.matchAll(
|
||||
/[✓×❯] ([\w./-]+\.test\.(?:ts|tsx|mjs)) \([^\n]*?\)\s+([\d.]+)ms/g
|
||||
)) {
|
||||
unit[match[1]] = Number(match[2])
|
||||
}
|
||||
for (const match of clean.matchAll(
|
||||
/[✓✘]\s+\d+ \[electron-headless\] › (tests\/e2e\/[^:]+):\d+:\d+ › .*? \(([\d.]+)(ms|s|m)\)/g
|
||||
)) {
|
||||
e2e[match[1]] = (e2e[match[1]] ?? 0) + Number(match[2]) * { ms: 1, s: 1000, m: 60000 }[match[3]]
|
||||
}
|
||||
const summary = clean.match(
|
||||
/Duration\s+[\d.]+(?:ms|s) \(transform ([\d.]+(?:ms|s)), setup ([\d.]+(?:ms|s)), import ([\d.]+(?:ms|s)), tests [\d.]+(?:ms|s), environment ([\d.]+(?:ms|s))\)/
|
||||
)
|
||||
if (Object.keys(unit).length && !summary) {
|
||||
throw new Error('Unit timing log has no supported Duration summary')
|
||||
}
|
||||
return {
|
||||
unit,
|
||||
e2e,
|
||||
overheadMs: summary
|
||||
? summary
|
||||
.slice(1)
|
||||
.reduce(
|
||||
(sum, value) => sum + Number.parseFloat(value) * (value.endsWith('ms') ? 1 : 1000),
|
||||
0
|
||||
)
|
||||
: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function importTimingLogs(directory, unitRun, e2eRun) {
|
||||
const baseline = {
|
||||
unit: { runId: unitRun, jobIds: [], overheadMs: 0, timings: {} },
|
||||
e2e: { runId: e2eRun, jobIds: [], overheadMs: 0, timings: {} }
|
||||
}
|
||||
for (const file of readdirSync(directory)
|
||||
.filter((file) => /^log-\d+\.txt$/.test(file))
|
||||
.sort()) {
|
||||
const parsed = parseTimingLog(readFileSync(join(directory, file), 'utf8'))
|
||||
for (const suite of ['unit', 'e2e']) {
|
||||
if (!Object.keys(parsed[suite]).length) {
|
||||
continue
|
||||
}
|
||||
baseline[suite].jobIds.push(file.match(/\d+/)[0])
|
||||
for (const [name, duration] of Object.entries(parsed[suite])) {
|
||||
if (suite === 'unit' && name in baseline.unit.timings) {
|
||||
throw new Error(`Duplicate unit timing: ${name}`)
|
||||
}
|
||||
baseline[suite].timings[name] = (baseline[suite].timings[name] ?? 0) + duration
|
||||
}
|
||||
}
|
||||
baseline.unit.overheadMs += parsed.overheadMs
|
||||
}
|
||||
for (const suite of ['unit', 'e2e']) {
|
||||
if (!baseline[suite].jobIds.length) {
|
||||
throw new Error(`No ${suite} timing evidence`)
|
||||
}
|
||||
baseline[suite].timings = Object.fromEntries(
|
||||
Object.entries(baseline[suite].timings).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
)
|
||||
}
|
||||
baseline.unit.overheadMs = Math.ceil(
|
||||
baseline.unit.overheadMs / Object.keys(baseline.unit.timings).length
|
||||
)
|
||||
return baseline
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const [directory, unitRun, e2eRun, output] = process.argv.slice(2)
|
||||
if (!directory || !unitRun || !e2eRun || !output) {
|
||||
throw new Error('Usage: ci-shard-timing-import.mjs LOG_DIRECTORY UNIT_RUN E2E_RUN OUTPUT')
|
||||
}
|
||||
writeFileSync(
|
||||
output,
|
||||
`${JSON.stringify(importTimingLogs(directory, unitRun, e2eRun), null, 2)}\n`
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
# Timing-based CI shards
|
||||
|
||||
The eight unit shards and fourteen general E2E shards use longest-processing-time
|
||||
assignment of whole files to the currently lightest shard. Ties use file path and
|
||||
then shard index, independent of filesystem enumeration and locale. Unknown,
|
||||
zero, or invalid durations use the baseline's positive median (1 second when no
|
||||
positive evidence exists). Deleted files never enter discovery. Unit weights add
|
||||
526ms per file for measured transform/setup/import/environment overhead.
|
||||
|
||||
Unit assignment runs inside Vitest's sequencer after discovery and CLI exclusions;
|
||||
Vitest's default sort, workers and isolation remain intact. It is enabled only by
|
||||
`ORCA_BALANCE_UNIT_SHARDS=1`; ordinary local runs and explicit file filters retain
|
||||
their existing behavior. E2E uses Playwright's native `--list` and `--test-list`,
|
||||
retaining project filters, skipped tests and complete serial groups within files.
|
||||
The workflow verifies selected test IDs against full discovery before executing.
|
||||
Dedicated SSH, native IME, WSL and first-paint lanes are unchanged.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`ci-shard-timings.json` records run IDs and every contributing job ID:
|
||||
|
||||
- Unit run **34675583768**, Node 24, all eight successful shards: 8,484 completed
|
||||
file durations. The summed transform/setup/import/environment durations divided
|
||||
by measured file count give a rounded-up **526ms** per-file overhead allowance.
|
||||
The original shard weighted loads were **764–849 worker-seconds**, versus
|
||||
**792–792** after balancing the identical measured files. File counts change
|
||||
from **1,056–1,065** to **1,060–1,061**.
|
||||
- General E2E run **34652504501**, all fourteen shard logs: 291 files with completed
|
||||
headless test durations, including failures. Headful benchmark reruns are not
|
||||
counted. Original completed test loads were **540–1,727 seconds**, versus
|
||||
**1,083–1,093** after whole-file balancing on the same measured files. The longest
|
||||
measured file is **528 seconds**, below the balanced shard load.
|
||||
- Current checkout discovery at validation contained **8,553 unit files** after the
|
||||
workflow's exact exclusions and **733 headless E2E tests in 340 files**. New and
|
||||
unmeasured files remain selected. Projected current loads were about **797
|
||||
worker-seconds** per unit shard (1,068–1,070 files) and **1,190–1,200 seconds** per
|
||||
E2E shard (22–25 files).
|
||||
|
||||
These are scheduling projections, not measured post-change wall-clock gains.
|
||||
Unit durations overlap across workers and the overhead allowance is an average,
|
||||
not a per-file import profile. E2E evidence includes failed shards and can omit
|
||||
unfinished tests; unknowns receive a deterministic estimate. Historical timings
|
||||
age as specs change. Full CI runs on the existing runner classes are required to
|
||||
measure elapsed-time and occupancy improvements, including discovery overhead.
|
||||
No retries, assertions, coverage exclusions, runner classes or shard counts changed.
|
||||
|
||||
## Reproduction and refresh
|
||||
|
||||
Every shard uploads an artifact named with its shard, Node version where relevant,
|
||||
and run attempt. `assignment.json` contains the checked-out source SHA, run ID,
|
||||
attempt, baseline SHA-256, algorithm, fallback, all shard files and chosen shard.
|
||||
E2E also retains both discovery reports and `selected.txt`. Artifacts live for
|
||||
14 days. A rerun of the same source uses the same checked-in baseline rather than
|
||||
mutable timing caches; a GitHub job rerun therefore keeps its assignment.
|
||||
|
||||
For E2E reproduction, check out the recorded source and pass the saved list to the
|
||||
existing command: `pnpm run test:e2e --test-list=/path/to/selected.txt` with the same
|
||||
CI environment/build inputs. For unit reproduction, use the unchanged workflow
|
||||
command and exclusions with `ORCA_BALANCE_UNIT_SHARDS=1` and the recorded
|
||||
`--shard=INDEX/8`. Direct test-file reruns remain supported.
|
||||
|
||||
To refresh the baseline, download `log-JOB_ID.txt` files into one directory from
|
||||
exactly one eight-shard unit run and one fourteen-shard general E2E run. Use the
|
||||
job IDs from the Actions jobs API and fetch each with
|
||||
`gh api repos/stablyai/orca/actions/jobs/JOB_ID/logs`. Do not include dedicated
|
||||
lanes or multiple attempts. Then run:
|
||||
|
||||
```sh
|
||||
node config/scripts/ci-shard-timing-import.mjs LOG_DIRECTORY UNIT_RUN_ID E2E_RUN_ID config/scripts/ci-shard-timings.json
|
||||
```
|
||||
|
||||
The initial source logs are in `/tmp/orca-ci-shard-logs`; two were reused from
|
||||
`/tmp/orca-ci-audit`, and the remaining twenty were fetched read-only. Reimporting
|
||||
those logs reproduced the checked-in JSON byte-for-byte. Review file-count and
|
||||
load projections before adopting a new baseline; no network access is needed to
|
||||
plan or run shards.
|
||||
|
||||
## Validation
|
||||
|
||||
- 74 focused tests passed across the two new test files and existing PR
|
||||
parallelism, E2E gate and release E2E dispatch contracts.
|
||||
- The pinned Playwright CLI selected the real 733-test suite across all fourteen
|
||||
saved test lists with exact-once identity coverage and no missing tests.
|
||||
- A temporary native Playwright fixture checks fourteen shards, serial groups,
|
||||
skipped cases, headful filtering and mismatch rejection without launching UI.
|
||||
- Real Vitest discovery with all workflow exclusions yielded 8,553 files; the
|
||||
sequencer's eight assignments covered each exactly once. An actual opt-in
|
||||
Vitest shard executed successfully and persisted its manifest.
|
||||
- Focused TypeScript checking of `config/vitest.config.ts` and imported modules,
|
||||
oxlint, formatting and baseline reimport checks passed.
|
||||
|
||||
All local tests used `ORCA_BACKGROUND_LAUNCH=1` in background tool sessions. No app
|
||||
windows or full E2E test bodies were launched.
|
||||
@@ -0,0 +1,19 @@
|
||||
import { relative } from 'node:path'
|
||||
import { BaseSequencer } from 'vitest/node'
|
||||
import { balanceFiles, readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
|
||||
|
||||
export default class TimingSequencer extends BaseSequencer {
|
||||
async shard(specs) {
|
||||
const { index, count } = this.ctx.config.shard
|
||||
const key = (spec) => relative(this.ctx.config.root, spec.moduleId).replaceAll('\\', '/')
|
||||
const baseline = readTimingBaseline('unit')
|
||||
const assignment = balanceFiles(specs.map(key), count, baseline.timings, baseline.overheadMs)
|
||||
writeAssignment(process.env.ORCA_SHARD_MANIFEST ?? 'ci-shards/unit-assignment.json', {
|
||||
...assignment,
|
||||
baselineSha256: baseline.baselineSha256,
|
||||
selectedShard: index
|
||||
})
|
||||
const selected = new Set(assignment.shards[index - 1].files)
|
||||
return specs.filter((spec) => selected.has(key(spec)))
|
||||
}
|
||||
}
|
||||
@@ -116,9 +116,9 @@ describe('Electron Vite output contract', () => {
|
||||
expect(external('node:fs', undefined, false)).toBe(true)
|
||||
expect(external('@xterm/headless', undefined, false)).toBe(false)
|
||||
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
|
||||
expect(external('psl', undefined, false)).toBe(false)
|
||||
expect(external('tldts', undefined, false)).toBe(false)
|
||||
expect(external('zod', undefined, false)).toBe(false)
|
||||
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('psl')
|
||||
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts')
|
||||
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod')
|
||||
})
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const runtime = readRuntimeArg()
|
||||
const NATIVE_MODULES = [
|
||||
'node-pty',
|
||||
...(process.platform === 'win32'
|
||||
? ['windows-native-registry', '@vscode/windows-process-tree']
|
||||
? ['@orca/windows-registry', '@vscode/windows-process-tree']
|
||||
: [])
|
||||
]
|
||||
const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe']
|
||||
@@ -275,7 +275,7 @@ function loadNativeModule(moduleName) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (moduleName === 'windows-native-registry') {
|
||||
if (moduleName === '@orca/windows-registry') {
|
||||
const registry = require(moduleName)
|
||||
// Why: the package defers loading its .node addon until the first registry call.
|
||||
registry.getRegistryKey(registry.HK.CU, 'Environment')
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('ensure-native-runtime', () => {
|
||||
const log = readFileSync(logPath, 'utf8')
|
||||
expect(log.match(/pnpm exec node-gyp rebuild\n/g)).toHaveLength(2)
|
||||
expect(log).toContain(join('node_modules', 'node-pty'))
|
||||
expect(log).toContain(join('node_modules', 'windows-native-registry'))
|
||||
expect(log).toContain(join('node_modules', '@orca', 'windows-registry'))
|
||||
} finally {
|
||||
rmSync(projectDir, { recursive: true, force: true })
|
||||
}
|
||||
@@ -299,11 +299,11 @@ function writeFakeWindowsRegistry(projectDir, { requiresMarker = false } = {}) {
|
||||
if (process.platform !== 'win32') {
|
||||
return
|
||||
}
|
||||
const registryDir = join(projectDir, 'node_modules', 'windows-native-registry')
|
||||
const registryDir = join(projectDir, 'node_modules', '@orca', 'windows-registry')
|
||||
mkdirSync(registryDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(registryDir, 'package.json'),
|
||||
'{"name":"windows-native-registry","version":"3.2.2","main":"index.js"}\n'
|
||||
'{"name":"@orca/windows-registry","version":"1.0.0","main":"index.js"}\n'
|
||||
)
|
||||
const markerGate = requiresMarker
|
||||
? `if (!require('node:fs').existsSync(process.env.ORCA_NATIVE_TEST_MARKER)) { throw new Error('registry ABI mismatch sentinel') }`
|
||||
|
||||
@@ -197,9 +197,10 @@ ${uncataloged.map((name) => ` '${name}'`).join(',\n')}
|
||||
|
||||
export type RpcMethodName = keyof typeof RPC_PARAMS_BY_METHOD
|
||||
|
||||
// Why: z.output is the post-parse shape the handler receives. z.input is not a
|
||||
// send-side type here — requiredString is z.unknown().transform(...), so its input
|
||||
// admits any value and loses optional/default semantics.
|
||||
// Why: z.output is the post-parse shape the handler receives, which is not what a
|
||||
// client may send — a .default() field reads as required. z.input is not the answer
|
||||
// either: requiredString is z.unknown().transform(...), so its input admits any value.
|
||||
// Senders use RpcSendParams from ./rpc-send-params, which is derived from this map.
|
||||
export type RpcParams<Method extends RpcMethodName> =
|
||||
(typeof RPC_PARAMS_BY_METHOD)[Method] extends z.ZodType
|
||||
? z.output<(typeof RPC_PARAMS_BY_METHOD)[Method]>
|
||||
|
||||
@@ -6,8 +6,7 @@ import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
// Why: the three artifacts version independently — bumping one shape must not
|
||||
// rewrite the others or bypass the registry's schema-gated append-only guard.
|
||||
// Version artifacts independently to preserve the registry's schema-gated append-only guard.
|
||||
const CURRENT_MANIFEST_SCHEMA_VERSION = 2
|
||||
const SNAPSHOT_REGISTRY_SCHEMA_VERSION = 1
|
||||
const RELEASE_MAPPING_SCHEMA_VERSION = 1
|
||||
@@ -41,18 +40,19 @@ function normalizeText(bytes) {
|
||||
return Buffer.from(text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'), 'utf8')
|
||||
}
|
||||
|
||||
function classifyFile(bytes) {
|
||||
function normalizedTextOrNull(bytes) {
|
||||
if (bytes.includes(0)) {
|
||||
return 'binary'
|
||||
return null
|
||||
}
|
||||
try {
|
||||
normalizeText(bytes)
|
||||
return 'text'
|
||||
return normalizeText(bytes)
|
||||
} catch {
|
||||
return 'binary'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const classifyFile = (bytes) => (normalizedTextOrNull(bytes) === null ? 'binary' : 'text')
|
||||
|
||||
function assertSafeRelativePath(relativePath) {
|
||||
if (
|
||||
path.isAbsolute(relativePath) ||
|
||||
@@ -64,17 +64,17 @@ function assertSafeRelativePath(relativePath) {
|
||||
}
|
||||
|
||||
function describeFile(manifestPath, bytes, executable) {
|
||||
const classification = classifyFile(bytes)
|
||||
const normalized = normalizedTextOrNull(bytes)
|
||||
const exactSha256 = sha256(bytes)
|
||||
const textNormalizedSha256 = classification === 'text' ? sha256(normalizeText(bytes)) : null
|
||||
const textNormalizedSha256 = normalized === null ? null : sha256(normalized)
|
||||
return {
|
||||
path: manifestPath,
|
||||
size: bytes.length,
|
||||
executable,
|
||||
classification,
|
||||
classification: normalized === null ? 'binary' : 'text',
|
||||
exactSha256,
|
||||
textNormalizedSha256,
|
||||
identitySha256: classification === 'text' && !executable ? textNormalizedSha256 : exactSha256,
|
||||
identitySha256: normalized !== null && !executable ? textNormalizedSha256 : exactSha256,
|
||||
gitBlobSha: gitObjectSha('blob', bytes).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { spawnSync } = vi.hoisted(() => ({ spawnSync: vi.fn() }))
|
||||
vi.mock('node:child_process', () => ({ spawnSync }))
|
||||
|
||||
let directory
|
||||
let artifact
|
||||
let originalArgv
|
||||
let originalExitCode
|
||||
const commands = () => spawnSync.mock.calls.map(([, args]) => args)
|
||||
const signalRuns = () => commands().filter((args) => ['INT', 'TERM'].includes(args.at(-1)))
|
||||
const succeeded = { status: 0, stdout: '', stderr: '' }
|
||||
|
||||
async function run(...options) {
|
||||
process.argv = ['node', 'runner', '--appimage', artifact, ...options]
|
||||
await import('./run-headless-serve-shutdown-docker.mjs')
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
spawnSync.mockReset().mockReturnValue(succeeded)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
directory = mkdtempSync(join(tmpdir(), 'orca-shutdown-matrix-'))
|
||||
artifact = join(directory, 'original.AppImage')
|
||||
writeFileSync(artifact, 'original package bytes')
|
||||
originalArgv = process.argv
|
||||
originalExitCode = process.exitCode
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv
|
||||
process.exitCode = originalExitCode
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('packaged shutdown matrix', () => {
|
||||
it('shares extraction but isolates every entrypoint and signal', async () => {
|
||||
await run('--all-entrypoints')
|
||||
expect(commands().filter((args) => args[0] === 'build')).toHaveLength(1)
|
||||
const startup = commands().filter((args) =>
|
||||
args.includes('/usr/local/bin/run-appimage-desktop-startup-case')
|
||||
)
|
||||
const extraction = commands().filter((args) =>
|
||||
args.some((arg) => arg.includes('120s /input/orca.AppImage --appimage-extract'))
|
||||
)
|
||||
expect(startup).toHaveLength(1)
|
||||
expect(extraction).toHaveLength(1)
|
||||
expect(commands().indexOf(startup[0])).toBeLessThan(commands().indexOf(extraction[0]))
|
||||
expect(signalRuns()).toHaveLength(6)
|
||||
const names = new Set()
|
||||
for (const [index, args] of signalRuns().entries()) {
|
||||
const entrypoint = ['app', 'launcher', 'appimage'][Math.floor(index / 2)]
|
||||
expect(args).toContain(`ORCA_TEST_ENTRYPOINT=${entrypoint}`)
|
||||
expect(args).toContain(
|
||||
`ORCA_SIGNAL_TARGET=${entrypoint === 'appimage' ? 'serving-electron' : 'app'}`
|
||||
)
|
||||
expect(args).toContain(
|
||||
`ORCA_INT_DELIVERY=${entrypoint === 'appimage' ? 'pid' : 'foreground-process-group'}`
|
||||
)
|
||||
expect(args.at(-1)).toBe(index % 2 === 0 ? 'INT' : 'TERM')
|
||||
expect(args).toContain(`${artifact}:/input/orca.AppImage:ro`)
|
||||
expect(args.some((arg) => arg.endsWith(':/artifacts:ro'))).toBe(true)
|
||||
expect(args).toContain('--rm')
|
||||
names.add(args[args.indexOf('--name') + 1])
|
||||
}
|
||||
expect(names.size).toBe(6)
|
||||
const evidence = console.log.mock.calls
|
||||
.map(([line]) => line)
|
||||
.filter((line) => line.startsWith('{'))
|
||||
.map(JSON.parse)
|
||||
expect(evidence).toHaveLength(3)
|
||||
expect(
|
||||
evidence.every(
|
||||
(entry) =>
|
||||
entry.sha256 === createHash('sha256').update('original package bytes').digest('hex')
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
commands()
|
||||
.slice(-2)
|
||||
.map((args) => args.slice(0, 2))
|
||||
).toEqual([
|
||||
['volume', 'rm'],
|
||||
['image', 'rm']
|
||||
])
|
||||
})
|
||||
|
||||
it('attributes failures and still attempts later cases before cleanup', async () => {
|
||||
spawnSync.mockImplementation((_, args) =>
|
||||
args.at(-1) === 'INT' ? { ...succeeded, status: 7 } : succeeded
|
||||
)
|
||||
await expect(run('--all-entrypoints')).rejects.toThrow(
|
||||
'app:INT:7, launcher:INT:7, appimage:INT:7'
|
||||
)
|
||||
expect(signalRuns()).toHaveLength(6)
|
||||
expect(commands().at(-2).slice(0, 2)).toEqual(['volume', 'rm'])
|
||||
})
|
||||
|
||||
it('cleans setup resources without running cases after failed extraction', async () => {
|
||||
spawnSync.mockImplementation((_, args) =>
|
||||
args.some((arg) => arg.includes('120s /input/orca.AppImage --appimage-extract'))
|
||||
? { ...succeeded, status: 9 }
|
||||
: succeeded
|
||||
)
|
||||
await expect(run('--all-entrypoints')).rejects.toThrow('docker run failed')
|
||||
expect(signalRuns()).toHaveLength(0)
|
||||
expect(
|
||||
commands()
|
||||
.slice(-2)
|
||||
.map((args) => args.slice(0, 2))
|
||||
).toEqual([
|
||||
['volume', 'rm'],
|
||||
['image', 'rm']
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves individual launcher overlay invocations', async () => {
|
||||
await run('--entrypoint', 'launcher', '--launcher-exec-overlay')
|
||||
expect(signalRuns()).toHaveLength(2)
|
||||
expect(signalRuns().every((args) => args.includes('ORCA_TEST_ENTRYPOINT=launcher'))).toBe(true)
|
||||
expect(
|
||||
commands().some((args) =>
|
||||
args.some((arg) => arg.includes("sed -i 's/^ELECTRON_RUN_AS_NODE=1"))
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects ambiguous matrix overrides before invoking Docker', async () => {
|
||||
await expect(run('--all-entrypoints', '--entrypoint', 'launcher')).rejects.toThrow(
|
||||
'cannot be combined'
|
||||
)
|
||||
expect(spawnSync).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -56,12 +56,6 @@ describe('headless serve shutdown PR gate', () => {
|
||||
const packageStep = steps.find((step) => step.name === 'Package unpacked app')
|
||||
const markerStep = steps.find((step) => step.name === 'Verify root-package marker payloads')
|
||||
const shutdownStep = steps.find((step) => step.name === 'Verify headless serve signal shutdown')
|
||||
const launcherShutdownStep = steps.find(
|
||||
(step) => step.name === 'Verify extracted launcher serve signal shutdown'
|
||||
)
|
||||
const appImageShutdownStep = steps.find(
|
||||
(step) => step.name === 'Verify AppImage CLI registration and serve signal shutdown'
|
||||
)
|
||||
|
||||
expect(workflow.jobs.package['timeout-minutes']).toBe(90)
|
||||
expect(packageStep.run).toContain('--linux AppImage deb rpm --x64 --publish never')
|
||||
@@ -69,19 +63,13 @@ describe('headless serve shutdown PR gate', () => {
|
||||
expect(markerStep.run).toContain('rpm2cpio')
|
||||
expect(steps.indexOf(markerStep)).toBeGreaterThan(steps.indexOf(packageStep))
|
||||
expect(shutdownStep.run).toBe(
|
||||
'node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage'
|
||||
'node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage --all-entrypoints'
|
||||
)
|
||||
expect(launcherShutdownStep.run).toContain(
|
||||
'node config/scripts/run-headless-serve-shutdown-docker.mjs'
|
||||
)
|
||||
expect(launcherShutdownStep.run).toContain('--entrypoint launcher')
|
||||
expect(appImageShutdownStep.run).toContain('--entrypoint appimage')
|
||||
expect(appImageShutdownStep.run).toContain('--signal-target serving-electron')
|
||||
expect(appImageShutdownStep.run).toContain('--int-delivery pid')
|
||||
expect(steps.indexOf(shutdownStep)).toBeGreaterThan(steps.indexOf(packageStep))
|
||||
expect(steps.indexOf(shutdownStep)).toBeGreaterThan(steps.indexOf(markerStep))
|
||||
expect(steps.indexOf(launcherShutdownStep)).toBeGreaterThan(steps.indexOf(shutdownStep))
|
||||
expect(steps.indexOf(appImageShutdownStep)).toBeGreaterThan(steps.indexOf(launcherShutdownStep))
|
||||
expect(
|
||||
steps.filter((step) => step.run?.includes('run-headless-serve-shutdown-docker.mjs'))
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps readiness polling finite and leak-free', () => {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, basename } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
const root = fileURLToPath(new URL('../..', import.meta.url))
|
||||
const fixture = await mkdtemp(join(tmpdir(), 'orca-hermes-correlation-'))
|
||||
const baselineDirectory = process.argv[2]
|
||||
const key = (seconds) =>
|
||||
new Date(Date.UTC(2026, 0, 1) + seconds * 1000)
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, '')
|
||||
.replace('T', '_')
|
||||
.slice(0, 15)
|
||||
try {
|
||||
for (const host of ['native', 'relay']) {
|
||||
const entry =
|
||||
host === 'native'
|
||||
? 'src/main/automations/hermes-cron-run-content.ts'
|
||||
: 'src/relay/hermes-run-correlation.ts'
|
||||
const readers = []
|
||||
for (const mode of baselineDirectory ? ['baseline', 'current'] : ['current']) {
|
||||
const bundle = join(fixture, `${host}-${mode}.cjs`)
|
||||
await build({
|
||||
entryPoints: [join(root, entry)],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: bundle,
|
||||
plugins:
|
||||
mode === 'baseline'
|
||||
? [
|
||||
{
|
||||
name: 'baseline-correlation',
|
||||
setup(builder) {
|
||||
builder.onLoad(
|
||||
{ filter: /hermes-(cron-run-content|run-correlation)\.ts$/ },
|
||||
async (args) => ({
|
||||
contents: await readFile(
|
||||
join(baselineDirectory, basename(args.path)),
|
||||
'utf8'
|
||||
),
|
||||
loader: 'ts'
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
})
|
||||
readers.push({ mode, ...createRequire(import.meta.url)(bundle) })
|
||||
}
|
||||
if (readers.length === 2) {
|
||||
let seed = 92817
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return seed % max
|
||||
}
|
||||
const pool = [
|
||||
null,
|
||||
'',
|
||||
'invalid',
|
||||
'20260101_000000',
|
||||
'20260101_000200',
|
||||
'20260102_000000',
|
||||
'20260103_000000',
|
||||
'20260101_240000'
|
||||
]
|
||||
for (let trial = 0; trial < 200; trial++) {
|
||||
const sessions = Array.from({ length: random(70) }, (_, i) => ({
|
||||
kind: 'session',
|
||||
id: `session-${i}`,
|
||||
job_id: 'job',
|
||||
run_at: null,
|
||||
run_key: pool[random(pool.length)],
|
||||
output_content: `session ${i}`
|
||||
}))
|
||||
const outputs = Array.from({ length: random(70) }, (_, i) => ({
|
||||
kind: 'output',
|
||||
id: `output-${i}`,
|
||||
job_id: 'job',
|
||||
run_at: null,
|
||||
run_key: pool[random(pool.length)],
|
||||
output_path: 'unused',
|
||||
output_content: `output ${i}`
|
||||
}))
|
||||
for (const method of [
|
||||
'mergeHermesOutputAndSessionRunRefs',
|
||||
'mergeHermesOutputAndSessionRuns'
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
readers[1][method](outputs, sessions),
|
||||
readers[0][method](outputs, sessions)
|
||||
)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ host, randomizedParityCases: 400 }))
|
||||
}
|
||||
for (const runs of [100, 1000, 5000]) {
|
||||
const sessions = Array.from({ length: runs }, (_, i) => ({
|
||||
kind: 'session',
|
||||
id: `session-${i}`,
|
||||
job_id: 'job',
|
||||
run_at: null,
|
||||
run_key: key(i * 3600)
|
||||
})).toReversed()
|
||||
const outputs = Array.from({ length: runs }, (_, i) => ({
|
||||
kind: 'output',
|
||||
id: `output-${i}`,
|
||||
job_id: 'job',
|
||||
run_at: null,
|
||||
run_key: key(i * 3600 + 120),
|
||||
output_path: 'unused'
|
||||
}))
|
||||
let expected
|
||||
for (const reader of [...readers, ...readers.toReversed()]) {
|
||||
const start = performance.now()
|
||||
const result = reader.mergeHermesOutputAndSessionRunRefs(outputs, sessions)
|
||||
const durationMs = performance.now() - start
|
||||
assert.equal(result.length, runs)
|
||||
result.forEach((row, i) => assert.equal(row.session.id, `session-${i}`))
|
||||
if (expected) {
|
||||
assert.deepEqual(result, expected)
|
||||
}
|
||||
expected = result
|
||||
console.log(JSON.stringify({ host, mode: reader.mode, runs, durationMs }))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pass a directory containing journal-open.ts and journal-row-table.ts from the base commit.
|
||||
const baselineDir = process.argv[2]
|
||||
assert.ok(
|
||||
baselineDir,
|
||||
'Usage: node --expose-gc journal-replay-retention-benchmark.mjs BASELINE_DIR'
|
||||
)
|
||||
assert.ok(global.gc, 'Run with --expose-gc to measure live backing memory during replay')
|
||||
const root = fileURLToPath(new URL('../..', import.meta.url))
|
||||
const fixture = await mkdtemp(join(tmpdir(), 'orca-journal-replay-bench-'))
|
||||
try {
|
||||
const implementations = {}
|
||||
for (const arm of ['baseline', 'current']) {
|
||||
const outfile = join(fixture, `${arm}.cjs`)
|
||||
await build({
|
||||
stdin: {
|
||||
contents:
|
||||
"export {openAgentSessionJournal} from './src/main/native-chat/agent-session-journal/journal-store-factory'; export {loadJournal} from './src/main/native-chat/agent-session-journal/journal-open'; export {journalDatabaseFile} from './src/main/native-chat/agent-session-journal/journal-paths';",
|
||||
resolveDir: root
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile,
|
||||
plugins: [
|
||||
{
|
||||
name: 'replay-memory-probe',
|
||||
setup(plugin) {
|
||||
plugin.onLoad(
|
||||
{ filter: /journal-(?:open|row-table|reducer)\.ts$/ },
|
||||
async ({ path }) => {
|
||||
const leaf = basename(path)
|
||||
let source = await readFile(
|
||||
arm === 'baseline' && leaf !== 'journal-reducer.ts'
|
||||
? join(baselineDir, leaf)
|
||||
: path,
|
||||
'utf8'
|
||||
)
|
||||
if (leaf === 'journal-reducer.ts') {
|
||||
const marker =
|
||||
'export function applyJournalRow(state: JournalReducerState, row: JournalRow): void {'
|
||||
assert.ok(source.includes(marker))
|
||||
source = source.replace(
|
||||
marker,
|
||||
`${marker}\nglobalThis.__replayMemoryProbe?.(row.seq);`
|
||||
)
|
||||
}
|
||||
return { contents: source, loader: 'ts', resolveDir: dirname(path) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
implementations[arm] = createRequire(import.meta.url)(outfile)
|
||||
}
|
||||
const identity = {
|
||||
sessionId: 'benchmark',
|
||||
workspaceId: 'fixture',
|
||||
hostId: 'local',
|
||||
agent: 'codex',
|
||||
providerHandle: { kind: 'codex', threadId: 'thread' }
|
||||
}
|
||||
const journalDir = join(fixture, 'session')
|
||||
const journal = await implementations.current.openAgentSessionJournal({ identity, journalDir })
|
||||
const item = { provider: 'codex', threadId: 'thread', turnId: 'turn', ordinal: 0 }
|
||||
const text = 'x'.repeat(32768)
|
||||
for (let revision = 0; revision < 2000; revision++) {
|
||||
await journal.appendItem(
|
||||
item,
|
||||
{
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: `${text}${revision}` }]
|
||||
},
|
||||
{ fence: 1 }
|
||||
)
|
||||
}
|
||||
await journal.close()
|
||||
for (const arm of ['baseline', 'current', 'current', 'baseline']) {
|
||||
global.gc()
|
||||
const start = performance.now()
|
||||
let loaded = implementations[arm].loadJournal(journalDir, identity.sessionId)
|
||||
const ms = performance.now() - start
|
||||
assert.equal(loaded.state.items.size, 1)
|
||||
assert.equal([...loaded.state.items.values()][0].revision, 2000)
|
||||
loaded = null
|
||||
global.gc()
|
||||
const initialHeap = process.memoryUsage().heapUsed
|
||||
let peakLiveHeap = initialHeap
|
||||
globalThis.__replayMemoryProbe = (sequence) => {
|
||||
if (sequence !== 1 && sequence % 256 !== 0) {
|
||||
return
|
||||
}
|
||||
global.gc()
|
||||
peakLiveHeap = Math.max(peakLiveHeap, process.memoryUsage().heapUsed)
|
||||
}
|
||||
loaded = implementations[arm].loadJournal(journalDir, identity.sessionId)
|
||||
delete globalThis.__replayMemoryProbe
|
||||
assert.equal(loaded.state.items.size, 1)
|
||||
loaded = null
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
arm,
|
||||
ms,
|
||||
databaseBytes: (await stat(implementations[arm].journalDatabaseFile(journalDir))).size,
|
||||
peakLiveHeapDelta: peakLiveHeap - initialHeap
|
||||
})
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
delete globalThis.__replayMemoryProbe
|
||||
await rm(fixture, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const modulePath = 'src/main/daemon/terminal-history-legacy-scrollback-restore.ts'
|
||||
const baselineSource = readFileSync(0, 'utf8')
|
||||
assert.ok(
|
||||
baselineSource.includes('function truncateAltScreen'),
|
||||
'Pipe the baseline module on stdin'
|
||||
)
|
||||
const arms = {}
|
||||
for (const [name, source] of [
|
||||
['baseline', baselineSource],
|
||||
['indexed', readFileSync(modulePath, 'utf8')]
|
||||
]) {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: `export { truncateAltScreen } from './${modulePath}'`,
|
||||
resolveDir: process.cwd(),
|
||||
loader: 'ts'
|
||||
},
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'private-export',
|
||||
setup(api) {
|
||||
api.onLoad({ filter: /terminal-history-legacy-scrollback-restore\.ts$/ }, () => ({
|
||||
contents: `${source}\nexport { truncateAltScreen }`,
|
||||
loader: 'ts',
|
||||
resolveDir: dirname(resolve(modulePath))
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
arms[name] = (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
).truncateAltScreen
|
||||
}
|
||||
|
||||
const on = '\x1b[?1049h'
|
||||
const off = '\x1b[?1049l'
|
||||
let differentialCases = 0
|
||||
function verify(input) {
|
||||
assert.equal(arms.indexed(input), arms.baseline(input))
|
||||
differentialCases++
|
||||
}
|
||||
const tokens = [on, off, '\x1b[?1049', 'h', 'l', 'x']
|
||||
function enumerate(prefix, depth) {
|
||||
verify(prefix)
|
||||
if (depth === 0) {
|
||||
return
|
||||
}
|
||||
for (const token of tokens) {
|
||||
enumerate(prefix + token, depth - 1)
|
||||
}
|
||||
}
|
||||
enumerate('', 6)
|
||||
|
||||
let seed = 90211
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 0x100000000) * max)
|
||||
}
|
||||
const fragments = [...tokens, '\r\n', '\x1b[?1047h', '\x1b[0m', 'é中😀', '\ud800', '\x00']
|
||||
for (let trial = 0; trial < 5000; trial++) {
|
||||
verify(Array.from({ length: random(300) }, () => fragments[random(fragments.length)]).join(''))
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
differentialCases,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch
|
||||
})
|
||||
)
|
||||
|
||||
const workloads = [
|
||||
['empty', ''],
|
||||
['plain 4KiB', 'x'.repeat(4096)],
|
||||
['plain 16MiB', 'x'.repeat(16 * 1024 * 1024)],
|
||||
['8 balanced', `${'x'.repeat(256)}${on}TUI${off}`.repeat(8)],
|
||||
['1024 balanced', (on + 'x'.repeat(4096) + off + 'x'.repeat(4096)).repeat(1024)],
|
||||
['1024 off', ('x'.repeat(8192) + off).repeat(1024)],
|
||||
['1024 nested on', ('x'.repeat(8192) + on).repeat(1024)],
|
||||
[
|
||||
'1024 nested closed',
|
||||
('x'.repeat(4096) + on).repeat(1024) + ('x'.repeat(4096) + off).repeat(1024)
|
||||
],
|
||||
['4096 off near 16MiB limit', ('x'.repeat(4088) + off).repeat(4096)]
|
||||
]
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[3] + sorted[4]) / 2
|
||||
}
|
||||
for (const [name, input] of workloads) {
|
||||
const expected = arms.baseline(input)
|
||||
assert.equal(arms.indexed(input), expected)
|
||||
const samples = { baseline: [], indexed: [] }
|
||||
const repeats = input.length < 8192 ? 10000 : 1
|
||||
for (const arm of Object.values(arms)) {
|
||||
for (let warmup = 0; warmup < Math.min(100, repeats); warmup++) {
|
||||
assert.equal(arm(input), expected)
|
||||
}
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'indexed')) {
|
||||
for (const name of pair) {
|
||||
const start = performance.now()
|
||||
let result
|
||||
for (let repeat = 0; repeat < repeats; repeat++) {
|
||||
result = arms[name](input)
|
||||
}
|
||||
samples[name].push((performance.now() - start) / repeats)
|
||||
assert.equal(result, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name,
|
||||
bytes: Buffer.byteLength(input),
|
||||
medianMs: Object.fromEntries(
|
||||
Object.entries(samples).map(([name, values]) => [name, median(values)])
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe the baseline policy on stdin. Catalog repair mutates only fresh in-memory copies.
|
||||
const policyPath = path.resolve('config/scripts/locale-translation-policy.mjs')
|
||||
const verifierPath = path.resolve('config/scripts/verify-localization-catalog.mjs')
|
||||
const sources = [readFileSync(0, 'utf8'), readFileSync(policyPath, 'utf8')]
|
||||
assert(sources.every((source) => source.includes('function includesPreservedLatinTerm(')))
|
||||
const modules = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const result = await build({
|
||||
entryPoints: [verifierPath],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'actual-locale-policy',
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /^\.\// }, (args) => {
|
||||
const resolved = path.resolve(args.resolveDir, args.path)
|
||||
return resolved === policyPath
|
||||
? { path: resolved }
|
||||
: { path: pathToFileURL(resolved).href, external: true }
|
||||
})
|
||||
builder.onResolve({ filter: /^typescript-api$/ }, () => ({
|
||||
path: import.meta.resolve('typescript-api'),
|
||||
external: true
|
||||
}))
|
||||
builder.onLoad({ filter: /locale-translation-policy\.mjs$/ }, () => ({
|
||||
contents: `${source}\nexport { includesPreservedLatinTerm };`,
|
||||
resolveDir: path.dirname(policyPath)
|
||||
}))
|
||||
builder.onLoad({ filter: /verify-localization-catalog\.mjs$/ }, () => ({
|
||||
contents: `${readFileSync(verifierPath, 'utf8')}\nexport * from './locale-translation-policy.mjs';`,
|
||||
resolveDir: path.dirname(verifierPath)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const code = `${result.outputFiles[0].text}\n//# sourceURL=locale-brand-prefilter-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
})
|
||||
)
|
||||
|
||||
const brands = [
|
||||
...new Set(Object.values(modules[0].BRAND_MISTRANSLATIONS).flatMap(Object.keys)),
|
||||
'',
|
||||
'_',
|
||||
'a_b',
|
||||
'a.b',
|
||||
'[term]',
|
||||
'界',
|
||||
'\ud800'
|
||||
]
|
||||
const boundaries = ['', ' ', 'X', '_', '2', '.', '-', '\n', '\0', 'é', '界', '😀', '\ud800']
|
||||
let comparisons = 0
|
||||
for (const term of brands) {
|
||||
for (const prefix of boundaries) {
|
||||
for (const suffix of boundaries) {
|
||||
for (const value of [
|
||||
`${prefix}${term}${suffix}`,
|
||||
`X${term}X ${prefix}${term}${suffix}`,
|
||||
`${prefix}${term.toLowerCase()}${suffix}`,
|
||||
`${prefix}${suffix}`
|
||||
]) {
|
||||
assert.equal(
|
||||
modules[1].includesPreservedLatinTerm(value, term),
|
||||
modules[0].includesPreservedLatinTerm(value, term)
|
||||
)
|
||||
comparisons += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`${comparisons} literal/boundary differential cases match`)
|
||||
|
||||
let repairCases = 0
|
||||
for (const [locale, translations] of Object.entries(modules[0].BRAND_MISTRANSLATIONS)) {
|
||||
for (const [brand, wrongForms] of Object.entries(translations)) {
|
||||
for (const wrong of wrongForms) {
|
||||
for (const prefix of boundaries) {
|
||||
for (const key of [
|
||||
'fixture.brand',
|
||||
'fixture.search.brand',
|
||||
'auto.lib.agent.catalog.test'
|
||||
]) {
|
||||
const input = {
|
||||
key,
|
||||
enValue: `${prefix}${brand}${prefix} fixture {{agent}}`,
|
||||
localeValue: `${wrong} ${prefix}${brand}${prefix} ${wrong} {{agent}}`,
|
||||
locale
|
||||
}
|
||||
assert.equal(
|
||||
modules[1].repairTranslatedValue(input),
|
||||
modules[0].repairTranslatedValue(input)
|
||||
)
|
||||
repairCases += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`${repairCases} full policy repair cases match`)
|
||||
|
||||
function measured(run) {
|
||||
const start = performance.now()
|
||||
const value = run()
|
||||
return { elapsed: performance.now() - start, value }
|
||||
}
|
||||
|
||||
function benchmark(name, prepare) {
|
||||
const expected = prepare(modules[0])()
|
||||
assert.deepEqual(prepare(modules[1])(), expected)
|
||||
for (const module of modules) {
|
||||
const until = performance.now() + 150
|
||||
do {
|
||||
assert.deepEqual(prepare(module)(), expected)
|
||||
} while (performance.now() < until)
|
||||
}
|
||||
/** @type {number[][]} */
|
||||
const times = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
const result = measured(prepare(modules[index]))
|
||||
times[index].push(result.elapsed)
|
||||
assert.deepEqual(result.value, expected)
|
||||
}
|
||||
}
|
||||
const median = times.map((values) => {
|
||||
const sorted = values.toSorted((a, b) => a - b)
|
||||
return (sorted[3] + sorted[4]) / 2
|
||||
})
|
||||
console.log(JSON.stringify({ name, median, times }))
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
unit: 'ms'
|
||||
})
|
||||
)
|
||||
const localesDir = path.resolve('src/renderer/src/i18n/locales')
|
||||
const en = JSON.parse(readFileSync(path.join(localesDir, 'en.json'), 'utf8'))
|
||||
const enEntries = new Map(modules[0].collectStringLeaves(en).map(({ key, value }) => [key, value]))
|
||||
for (const locale of ['zh', 'ja', 'ko', 'es', 'fr']) {
|
||||
const catalog = JSON.parse(readFileSync(path.join(localesDir, `${locale}.json`), 'utf8'))
|
||||
const localeEntries = new Map(
|
||||
modules[0].collectStringLeaves(catalog).map(({ key, value }) => [key, value])
|
||||
)
|
||||
const inputs = [...enEntries].flatMap(([key, enValue]) => {
|
||||
const localeValue = localeEntries.get(key)
|
||||
return typeof localeValue === 'string' ? [{ key, enValue, localeValue, locale }] : []
|
||||
})
|
||||
assert.deepEqual(
|
||||
inputs.map(modules[1].repairTranslatedValue),
|
||||
inputs.map(modules[0].repairTranslatedValue)
|
||||
)
|
||||
const expressionCounts = modules.map((module) => {
|
||||
const original = globalThis.RegExp
|
||||
let count = 0
|
||||
globalThis.RegExp = new Proxy(original, {
|
||||
construct(target, args) {
|
||||
if (typeof args[0] === 'string' && args[0].startsWith('(^|[^A-Za-z_])')) {
|
||||
count += 1
|
||||
}
|
||||
return Reflect.construct(target, args)
|
||||
}
|
||||
})
|
||||
try {
|
||||
inputs.forEach(module.repairTranslatedValue)
|
||||
} finally {
|
||||
globalThis.RegExp = original
|
||||
}
|
||||
return count
|
||||
})
|
||||
console.log(JSON.stringify({ locale, leaves: inputs.length, expressionCounts }))
|
||||
benchmark(`${locale}: repairCatalog`, (module) => {
|
||||
const copy = structuredClone(catalog)
|
||||
return () => ({ count: module.repairCatalog(en, copy, locale), catalog: copy })
|
||||
})
|
||||
benchmark(`${locale}: collectGenericTermRegressions`, (module) => {
|
||||
return () => module.collectGenericTermRegressions(enEntries, localeEntries, locale)
|
||||
})
|
||||
}
|
||||
|
||||
for (const [name, enValue, localeValue] of [
|
||||
['absent brands', 'Choose an endpoint.', 'Elegir un destino.'],
|
||||
['matching brand', 'Use Gemini.', 'Usar Géminis.'],
|
||||
['embedded only', 'Use _Gemini_.', 'Usar Géminis.'],
|
||||
['all brands', brands.join(' '), brands.join(' ')]
|
||||
]) {
|
||||
const input = { key: 'fixture.brand', enValue, localeValue, locale: 'es' }
|
||||
benchmark(`10k strings: ${name}`, (module) => () => {
|
||||
let result
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
result = module.repairTranslatedValue(input)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const cache = new Map([
|
||||
['Use Gemini.', 'Usar Géminis.'],
|
||||
['Choose an endpoint.', 'Elegir un destino.'],
|
||||
['Use _Gemini_.', 'Usar Géminis.'],
|
||||
['Use GitHub Copilot.', 'Usar Copiloto de GitHub.']
|
||||
])
|
||||
const caches = modules.map((module) => {
|
||||
const copy = new Map(cache)
|
||||
return { count: module.repairCacheMap(copy, 'es'), entries: [...copy] }
|
||||
})
|
||||
assert.deepEqual(caches[1], caches[0])
|
||||
console.log('Actual catalog outputs, regression reports, repair counts and cache mutation match')
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { repairTranslatedValue } from './locale-translation-policy.mjs'
|
||||
|
||||
describe('locale brand matching', () => {
|
||||
it('does not construct boundary expressions for absent brands', () => {
|
||||
let boundaryExpressions = 0
|
||||
vi.stubGlobal(
|
||||
'RegExp',
|
||||
new Proxy(RegExp, {
|
||||
construct(target, args) {
|
||||
if (typeof args[0] === 'string' && args[0].startsWith('(^|[^A-Za-z_])')) {
|
||||
boundaryExpressions += 1
|
||||
}
|
||||
return Reflect.construct(target, args)
|
||||
}
|
||||
})
|
||||
)
|
||||
try {
|
||||
for (const locale of ['zh', 'ja', 'ko', 'es']) {
|
||||
expect(
|
||||
repairTranslatedValue({
|
||||
key: 'fixture.endpoint',
|
||||
enValue: 'Choose an endpoint.',
|
||||
localeValue: 'fixture translation',
|
||||
locale
|
||||
})
|
||||
).toBe('fixture translation')
|
||||
}
|
||||
} finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
expect(boundaryExpressions).toBe(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Use Gemini.', 'Usar Géminis.', 'Usar Gemini.'],
|
||||
['Use GeminiX.', 'Usar Géminis.', 'Usar Géminis.'],
|
||||
['Use XGemini.', 'Usar Géminis.', 'Usar Géminis.'],
|
||||
['Use _Gemini_.', 'Usar Géminis.', 'Usar Géminis.'],
|
||||
['Use Gemini_2.', 'Usar Géminis.', 'Usar Géminis.'],
|
||||
['Use 2Gemini3.', 'Usar Géminis.', 'Usar Gemini.'],
|
||||
['Use (Gemini).', 'Usar Géminis.', 'Usar Gemini.'],
|
||||
['Use éGemini界.', 'Usar Géminis.', 'Usar Gemini.'],
|
||||
['Use gemini.', 'Usar Géminis.', 'Usar Géminis.'],
|
||||
['Use GeminiX and Gemini.', 'Usar Géminis.', 'Usar Gemini.'],
|
||||
['Use Gemini.', 'Gemini y Géminis.', 'Gemini y Géminis.'],
|
||||
['Use Gemini.', '_Gemini_ y Géminis.', '_Gemini_ y Gemini.'],
|
||||
['Use GitHub Copilot.', 'Usar Copiloto de GitHub.', 'Usar GitHub Copilot.'],
|
||||
['Use XGitHub CopilotY.', 'Usar Copiloto de GitHub.', 'Usar GitHub Copilot.']
|
||||
])('preserves literal and boundary matching for %j / %j', (enValue, localeValue, expected) => {
|
||||
expect(
|
||||
repairTranslatedValue({ key: 'fixture.brand', enValue, localeValue, locale: 'es' })
|
||||
).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -281,8 +281,11 @@ function escapeRegExp(value) {
|
||||
}
|
||||
|
||||
function includesPreservedLatinTerm(value, term) {
|
||||
if (!value.includes(term)) {
|
||||
return false
|
||||
}
|
||||
if (!/^[A-Za-z_]+$/.test(term)) {
|
||||
return value.includes(term)
|
||||
return true
|
||||
}
|
||||
return new RegExp(`(^|[^A-Za-z_])${escapeRegExp(term)}($|[^A-Za-z_])`).test(value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env node
|
||||
// Adverse-input audit: compares the previous scanner with the production line-bounded implementation.
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
const entry = fileURLToPath(
|
||||
new URL(
|
||||
'../../src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.ts',
|
||||
import.meta.url
|
||||
)
|
||||
)
|
||||
const source = await readFile(entry, 'utf8')
|
||||
const current = `${String.raw`/\s*(?:`}\`\`\`|~~~)/y`
|
||||
const replacement = `${String.raw`/[^\S\n]*(?:`}\`\`\`|~~~)/y`
|
||||
assert.ok(
|
||||
source.includes(replacement),
|
||||
'Production fence regex changed; re-review benchmark candidate'
|
||||
)
|
||||
async function load(candidate) {
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: candidate
|
||||
? [
|
||||
{
|
||||
name: 'line-bounded-candidate',
|
||||
setup(plugin) {
|
||||
plugin.onLoad({ filter: /monaco-markdown-doc-link-decorations\.ts$/ }, () => ({
|
||||
contents: source.replace(replacement, current),
|
||||
loader: 'ts',
|
||||
resolveDir: fileURLToPath(
|
||||
new URL('../../src/renderer/src/components/editor/', import.meta.url)
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
).getMarkdownDocLinkDecorationRanges
|
||||
}
|
||||
const baseline = await load(true)
|
||||
const candidate = await load(false)
|
||||
const corpus = [
|
||||
'',
|
||||
'\n```\n[[hidden.md]]\n```\n[[shown.md]]',
|
||||
' \t\r\n~~~\r\n[[hidden.md]]\r\n~~~\r\n[[shown.md]]',
|
||||
'\u00a0\u2028```\n[[hidden.md]]\n```\n[[shown.md]]',
|
||||
'`code` [[shown.md]]'
|
||||
]
|
||||
for (const content of corpus) {
|
||||
assert.deepEqual(candidate(content), baseline(content))
|
||||
}
|
||||
const scenarios = [
|
||||
['ordinary-100k-lines', 'ordinary prose\n'.repeat(100_000)],
|
||||
['blank-10k-lines', '\n'.repeat(10_000)],
|
||||
['blank-30k-lines', '\n'.repeat(30_000)],
|
||||
['blank-100k-lines', '\n'.repeat(100_000)],
|
||||
['indented-blank-10k-lines', `${' '.repeat(80)}\n`.repeat(10_000)]
|
||||
]
|
||||
for (const [name, content] of scenarios) {
|
||||
const samples = { baseline: [], candidate: [] }
|
||||
const scanners = { baseline, candidate }
|
||||
let expected
|
||||
for (const arms of buildCounterbalancedSchedule(2, 'baseline', 'candidate')) {
|
||||
for (const arm of arms) {
|
||||
const started = performance.now()
|
||||
const ranges = scanners[arm](content)
|
||||
samples[arm].push(performance.now() - started)
|
||||
expected ??= ranges
|
||||
assert.deepEqual(ranges, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name,
|
||||
bytes: Buffer.byteLength(content),
|
||||
samples,
|
||||
baseline: summarizeBenchmarkSamples(samples.baseline),
|
||||
candidate: summarizeBenchmarkSamples(samples.candidate)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const sourcePath = 'src/renderer/src/lib/markdown-review-notes.ts'
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(sourcePath)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(
|
||||
execFileSync('git', ['show', `${baseline}:${sourcePath}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(readFileSync(sourcePath, 'utf8'))
|
||||
const results = []
|
||||
for (const [name, lineCount, width, count, iterations] of [
|
||||
['small', 20, 40, 1, 1000],
|
||||
['long-lines', 1000, 1000, 20, 3],
|
||||
['many-lines', 20000, 80, 20, 3],
|
||||
['early-note', 20000, 80, 1, 1000]
|
||||
]) {
|
||||
const content = Array.from({ length: lineCount }, (_, i) => `${i}: ${'x'.repeat(width)}`).join(
|
||||
'\r\n'
|
||||
)
|
||||
const notes = Array.from({ length: count }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
worktreeId: 'bench',
|
||||
filePath: 'README.md',
|
||||
source: 'markdown',
|
||||
lineNumber: name === 'early-note' ? 2 : lineCount - i,
|
||||
body: 'Clarify this line',
|
||||
createdAt: i,
|
||||
side: 'modified'
|
||||
}))
|
||||
assert.equal(
|
||||
after.formatMarkdownReviewNotes(notes, content),
|
||||
before.formatMarkdownReviewNotes(notes, content)
|
||||
)
|
||||
const arms = { before, after }
|
||||
const samples = { before: [], after: [] }
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm].formatMarkdownReviewNotes(notes, content)
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
run('before')
|
||||
run('after')
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(12, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (xs) => xs.sort((a, b) => a - b)[Math.floor(xs.length / 2)]
|
||||
results.push({
|
||||
name,
|
||||
lineCount,
|
||||
width,
|
||||
count,
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after)
|
||||
})
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, baseline, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <baseline-ref>:mobile/src/terminal/terminal-live-text-commit.ts | node config/scripts/mobile-backspace-benchmark.mjs
|
||||
const target = resolve('mobile/src/terminal/terminal-live-text-commit.ts')
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
stdin: { contents: source, loader: 'ts', resolveDir: dirname(target) },
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: 'node',
|
||||
format: 'esm'
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
).getTerminalLiveAccessoryLocalEditText
|
||||
}
|
||||
const baseline = readFileSync(0, 'utf8')
|
||||
assert.ok(
|
||||
baseline.includes('function getTerminalLiveAccessoryLocalEditText'),
|
||||
'Pipe baseline source into stdin'
|
||||
)
|
||||
const implementations = {
|
||||
before: await load(baseline),
|
||||
after: await load(readFileSync(target, 'utf8'))
|
||||
}
|
||||
const tokens = [
|
||||
'',
|
||||
'a',
|
||||
'\u0000',
|
||||
'\r',
|
||||
'\n',
|
||||
'한',
|
||||
'\u0301',
|
||||
'\u200d',
|
||||
'🙂',
|
||||
'\ud800',
|
||||
'\udbff',
|
||||
'\udc00',
|
||||
'\udfff'
|
||||
]
|
||||
let cases = 0
|
||||
for (const first of tokens) {
|
||||
for (const second of tokens) {
|
||||
for (const third of tokens) {
|
||||
for (const localEdit of ['backspace', 'delete']) {
|
||||
const input = { fieldText: first + second + third, localEdit }
|
||||
assert.equal(
|
||||
implementations.after(input),
|
||||
implementations.before(input),
|
||||
JSON.stringify(input)
|
||||
)
|
||||
cases += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = []
|
||||
for (const inputBytes of [32, 4096, 65_536, 262_144]) {
|
||||
for (const glyph of ['a', '🙂']) {
|
||||
const fieldText = glyph.repeat(inputBytes / Buffer.byteLength(glyph))
|
||||
const input = { localEdit: 'backspace', fieldText }
|
||||
const expected = implementations.before(input)
|
||||
assert.equal(implementations.after(input), expected)
|
||||
const iterations = Math.max(10, Math.floor(1_000_000 / inputBytes))
|
||||
for (let warmup = 0; warmup < 100; warmup += 1) {
|
||||
implementations.before(input)
|
||||
implementations.after(input)
|
||||
}
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
let actual
|
||||
const started = performance.now()
|
||||
for (let repeat = 0; repeat < iterations; repeat += 1) {
|
||||
actual = implementations[arm](input)
|
||||
}
|
||||
samples[arm].push(performance.now() - started)
|
||||
assert.equal(actual, expected)
|
||||
}
|
||||
}
|
||||
const means = Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
)
|
||||
results.push({
|
||||
inputBytes,
|
||||
glyph,
|
||||
iterations,
|
||||
meanMicrosecondsPerCall: means,
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialCases: cases, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,236 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe baseline report then submission modules on stdin, in that order. No device/network I/O.
|
||||
const baseline = readFileSync(0, 'utf8')
|
||||
const divider = '\nconst CONNECTION_DIAGNOSTICS_ENDPOINT = '
|
||||
assert.equal(baseline.split(divider).length, 2)
|
||||
const split = baseline.indexOf(divider) + 1
|
||||
const files = ['report', 'submission'].map((name) =>
|
||||
path.resolve(`mobile/src/diagnostics/connection-diagnostics-${name}.ts`)
|
||||
)
|
||||
const sources = [
|
||||
[baseline.slice(0, split), baseline.slice(split)],
|
||||
files.map((file) => readFileSync(file, 'utf8'))
|
||||
]
|
||||
const modules = await Promise.all(
|
||||
sources.map(async (contents) => {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: files.map((file) => `export * from ${JSON.stringify(file)};`).join('\n'),
|
||||
resolveDir: process.cwd()
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'actual-mobile-diagnostics',
|
||||
setup(builder) {
|
||||
builder.onLoad(
|
||||
{ filter: /connection-diagnostics-(report|submission)\.ts$/ },
|
||||
(args) => ({
|
||||
contents: contents[files.indexOf(args.path)],
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(args.path)
|
||||
})
|
||||
)
|
||||
builder.onResolve({ filter: /^@react-native-async-storage\/async-storage$/ }, () => ({
|
||||
path: 'forbidden-device-storage',
|
||||
namespace: 'fixture'
|
||||
}))
|
||||
builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({
|
||||
contents: `function forbidden() { throw new Error('Device storage is forbidden'); }
|
||||
export default { getItem: forbidden, setItem: forbidden };`
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const code = `${result.outputFiles[0].text}\n//# sourceURL=mobile-diagnostics-prefix-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
})
|
||||
)
|
||||
|
||||
const base = {
|
||||
hostName: 'fixture',
|
||||
endpoint: 'ws://192.168.1.2:6768',
|
||||
state: 'reconnecting',
|
||||
reconnectAttempts: 2,
|
||||
lastConnectedAt: null,
|
||||
platform: 'android',
|
||||
appVersion: 'fixture',
|
||||
nowMs: 1700000000000
|
||||
}
|
||||
let seed = 0x20d1a6
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return (seed >>> 8) % max
|
||||
}
|
||||
const tokens = ['a', 'é', '界', '😀', '\ud800', '\udc00', '\n', '\r\n', '\0', 'e\u0301']
|
||||
const limits = [
|
||||
Number.NEGATIVE_INFINITY,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
100,
|
||||
511,
|
||||
2048,
|
||||
65536,
|
||||
Number.POSITIVE_INFINITY,
|
||||
Number.NaN,
|
||||
2.5
|
||||
]
|
||||
for (let trace = 0; trace < 3000; trace++) {
|
||||
const lines = Array.from({ length: 1 + random(12) }, () =>
|
||||
Array.from({ length: 1 + random(5) }, () =>
|
||||
tokens[random(tokens.length)].repeat(random(100))
|
||||
).join('')
|
||||
)
|
||||
if (trace % 2) {
|
||||
lines.splice(random(lines.length), 0, 'Recent connection history (fixture):')
|
||||
}
|
||||
const report = lines.join('\n')
|
||||
const limit = limits[random(limits.length)]
|
||||
assert.equal(
|
||||
modules[1].boundConnectionDiagnosticsReport(report, limit),
|
||||
modules[0].boundConnectionDiagnosticsReport(report, limit)
|
||||
)
|
||||
}
|
||||
console.log('3,000 report-bound differentials match, including nonfinite/fractional limits')
|
||||
|
||||
for (let trace = 0; trace < 600; trace++) {
|
||||
const entries = Object.freeze(
|
||||
Array.from({ length: random(12) }, (_, index) =>
|
||||
Object.freeze({
|
||||
id: String(index),
|
||||
ts: base.nowMs + index,
|
||||
level: ['info', 'error', 'warn'][random(3)],
|
||||
message: ['Authenticated', 'relay director resolve failed (503)', 'fixture'][random(3)],
|
||||
detail: `${tokens[random(tokens.length)].repeat(random(4000))} token=fixture-secret`,
|
||||
code: ['client-session-started', 'liveness-timeout', undefined][random(3)],
|
||||
path: ['relay', 'lan', 'tailscale'][random(3)]
|
||||
})
|
||||
)
|
||||
)
|
||||
const args = Object.freeze({
|
||||
...base,
|
||||
hostName: 'fixture token=host-fixture-secret',
|
||||
endpoint: trace % 2 ? base.endpoint : 'invalid?token=endpoint-fixture-secret',
|
||||
desktopAppVersion: trace % 2 ? '1.2.3' : '\ninvalid',
|
||||
state: ['connected', 'reconnecting', 'connecting'][random(3)],
|
||||
activePath: ['relay', 'lan', 'tailscale'][random(3)],
|
||||
pendingPath: trace % 3 ? null : 'relay',
|
||||
entries
|
||||
})
|
||||
const reports = modules.map((module) => module.buildConnectionDiagnosticsReport(args))
|
||||
assert.equal(reports[1], reports[0])
|
||||
assert(!reports[1].includes('fixture-secret'))
|
||||
const limit = limits[random(limits.length)]
|
||||
assert.equal(
|
||||
modules[1].boundConnectionDiagnosticsReport(reports[1], limit),
|
||||
modules[0].boundConnectionDiagnosticsReport(reports[0], limit)
|
||||
)
|
||||
}
|
||||
console.log('600 frozen report-build + bound journeys preserve redaction, diagnosis and exact text')
|
||||
|
||||
function runSample(run, repeats) {
|
||||
let value
|
||||
const start = performance.now()
|
||||
for (let index = 0; index < repeats; index++) {
|
||||
value = run()
|
||||
}
|
||||
return { value, elapsed: (performance.now() - start) / repeats }
|
||||
}
|
||||
function benchmark(name, arms) {
|
||||
const expected = arms[0]()
|
||||
assert.equal(arms[1](), expected)
|
||||
for (const arm of arms) {
|
||||
const until = performance.now() + 200
|
||||
do {
|
||||
assert.equal(arm(), expected)
|
||||
} while (performance.now() < until)
|
||||
}
|
||||
const repeats = Math.max(3, Math.min(10000, Math.ceil(40 / runSample(arms[0], 1).elapsed)))
|
||||
/** @type {number[][]} */
|
||||
const times = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
const result = runSample(arms[index], repeats)
|
||||
assert.equal(result.value, expected)
|
||||
times[index].push(result.elapsed)
|
||||
}
|
||||
}
|
||||
const median = times.map((values) => {
|
||||
const sorted = values.toSorted((a, b) => a - b)
|
||||
return (sorted[3] + sorted[4]) / 2
|
||||
})
|
||||
console.log(JSON.stringify({ name, repeats, median, times }))
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
unit: 'ms'
|
||||
})
|
||||
)
|
||||
for (const [events, length, token] of [
|
||||
[0, 0, 'a'],
|
||||
[20, 80, 'a'],
|
||||
[200, 80, 'a'],
|
||||
[200, 1000, 'a'],
|
||||
[200, 4000, 'a'],
|
||||
[200, 2000, '😀']
|
||||
]) {
|
||||
const args = {
|
||||
...base,
|
||||
entries: Array.from({ length: events }, (_, i) => ({
|
||||
id: String(i),
|
||||
ts: base.nowMs + i,
|
||||
level: 'error',
|
||||
message: `fixture-${i} ${token.repeat(length)}`
|
||||
}))
|
||||
}
|
||||
const reports = modules.map((module) => module.buildConnectionDiagnosticsReport(args))
|
||||
assert.equal(reports[1], reports[0])
|
||||
const label = `${events} events / ${length} ${token}`
|
||||
benchmark(
|
||||
`${label}: build`,
|
||||
modules.map((module) => () => module.buildConnectionDiagnosticsReport(args))
|
||||
)
|
||||
benchmark(
|
||||
`${label}: bound`,
|
||||
modules.map((module) => () => module.boundConnectionDiagnosticsReport(reports[0]))
|
||||
)
|
||||
}
|
||||
|
||||
for (const token of ['a', '😀', '\ud800']) {
|
||||
const report = token.repeat(100000)
|
||||
const results = await Promise.all(
|
||||
modules.map(async (module) => {
|
||||
let request
|
||||
const result = await module.submitConnectionDiagnostics(
|
||||
{ report, platform: 'android', appVersion: 'fixture' },
|
||||
async (url, options) => {
|
||||
assert.equal(options.signal.aborted, false)
|
||||
request = { url, method: options.method, headers: options.headers, body: options.body }
|
||||
return { ok: true }
|
||||
}
|
||||
)
|
||||
return { result, request }
|
||||
})
|
||||
)
|
||||
assert.deepEqual(results[1], results[0])
|
||||
}
|
||||
console.log('Three fake-fetch submission journeys preserve complete request bytes and results')
|
||||
@@ -1,53 +1,141 @@
|
||||
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'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error('Usage: node config/scripts/mobile-file-ranking-benchmark.mjs <baseline-ref>')
|
||||
throw new Error(
|
||||
'Usage: node config/scripts/mobile-file-ranking-benchmark.mjs <baseline-ref|--autocomplete-stdin>'
|
||||
)
|
||||
}
|
||||
// git show <ref>:mobile/src/session/mobile-native-chat-autocomplete.ts | node config/scripts/mobile-file-ranking-benchmark.mjs --autocomplete-stdin
|
||||
const autocompleteSource = baseline === '--autocomplete-stdin' ? readFileSync(0, 'utf8') : null
|
||||
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 { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
return await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
}
|
||||
const results = []
|
||||
let differentialCases = 0
|
||||
for (const [file, name] of [
|
||||
['src/main/runtime/runtime-mobile-file-path-search.ts', 'rankRuntimeMobileFilePaths'],
|
||||
['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSuggestions']
|
||||
['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSuggestions'],
|
||||
['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSlashCommandSuggestions']
|
||||
]) {
|
||||
if (autocompleteSource !== null && name === 'rankRuntimeMobileFilePaths') {
|
||||
continue
|
||||
}
|
||||
const before = (
|
||||
await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }))
|
||||
await load(
|
||||
autocompleteSource ??
|
||||
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`
|
||||
const slash = name === 'rankSlashCommandSuggestions'
|
||||
const toCandidates = (names) =>
|
||||
slash ? names.map((name, index) => ({ name, description: `Command ${index}` })) : names
|
||||
if (name !== 'rankRuntimeMobileFilePaths') {
|
||||
let seed = 42
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return seed % max
|
||||
}
|
||||
const tokens = ['', 'app', 'src/', 'APP', 'zapp', '🙂', '한', '\ud800', '\u0130', ' ']
|
||||
const limits = [
|
||||
undefined,
|
||||
0,
|
||||
-0,
|
||||
-1,
|
||||
-0.5,
|
||||
-Infinity,
|
||||
Number.NaN,
|
||||
0.5,
|
||||
1.5,
|
||||
2.5,
|
||||
8,
|
||||
16,
|
||||
Infinity
|
||||
]
|
||||
for (let index = 0; index < 3000; index += 1) {
|
||||
const candidates = toCandidates(
|
||||
Array.from(
|
||||
{ length: random(100) },
|
||||
() => tokens[random(tokens.length)] + tokens[random(tokens.length)]
|
||||
)
|
||||
)
|
||||
const query = tokens[random(tokens.length)]
|
||||
const limit = limits[random(limits.length)]
|
||||
assert.deepEqual(after(candidates, query, limit), before(candidates, query, limit))
|
||||
differentialCases += 1
|
||||
}
|
||||
}
|
||||
for (const count of slash ? [16, 100, 1000] : [16, 100, 10_000, 50_000, 100_000]) {
|
||||
const names = Array.from({ length: count }, (_, index) =>
|
||||
slash
|
||||
? `team-review-${index}`
|
||||
: `src/components/workspace/group-${index % 100}/file-${index}.tsx`
|
||||
)
|
||||
for (const query of ['file-9', 'missing', 'workspace']) {
|
||||
assert.deepEqual(after(paths, query, 16), before(paths, query, 16))
|
||||
const limit = slash ? 12 : 16
|
||||
const substringQuery = slash ? 'review' : 'workspace'
|
||||
const workloads = [
|
||||
{ name: 'empty-query', names, query: '' },
|
||||
{ name: 'substring', names, query: substringQuery },
|
||||
{ name: 'no-match', names, query: 'missing' },
|
||||
{ name: 'early-prefix', names, query: slash ? 'team' : 'file' },
|
||||
{
|
||||
name: 'late-prefix',
|
||||
names: [...names, ...Array.from({ length: 4 }, (_, index) => `${substringQuery}-${index}`)],
|
||||
query: substringQuery
|
||||
}
|
||||
]
|
||||
for (const workload of workloads) {
|
||||
const candidates = toCandidates(workload.names)
|
||||
const expected = before(candidates, workload.query, limit)
|
||||
assert.deepEqual(after(candidates, workload.query, limit), expected)
|
||||
const implementations = { before, after }
|
||||
const iterations = Math.max(10, Math.floor(100_000 / count))
|
||||
for (let warmup = 0; warmup < 100; warmup += 1) {
|
||||
before(candidates, workload.query, limit)
|
||||
after(candidates, workload.query, limit)
|
||||
}
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
let actual
|
||||
const start = performance.now()
|
||||
for (let repeat = 0; repeat < iterations; repeat += 1) {
|
||||
actual = implementations[arm](candidates, workload.query, limit)
|
||||
}
|
||||
samples[arm].push(performance.now() - start)
|
||||
assert.deepEqual(actual, expected)
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
function: name,
|
||||
paths: count,
|
||||
query,
|
||||
beforeMs: measure(before, paths, query),
|
||||
afterMs: measure(after, paths, query)
|
||||
candidates: candidates.length,
|
||||
workload: workload.name,
|
||||
iterations,
|
||||
meanMicrosecondsPerCall: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
),
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialCases, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error(
|
||||
'Usage: node config/scripts/mobile-history-scope-paths-benchmark.mjs <baseline-ref>'
|
||||
)
|
||||
}
|
||||
const file = 'mobile/src/agent-history/agent-history-scope-paths.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {}
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
).deriveMobileAiVaultScopePaths
|
||||
}
|
||||
const arms = {
|
||||
before: await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })),
|
||||
after: await load(readFileSync(file, 'utf8'))
|
||||
}
|
||||
const iterations = 200
|
||||
const results = []
|
||||
for (const [count, unique] of [
|
||||
[1, 1],
|
||||
[16, 16],
|
||||
[64, 64],
|
||||
[1000, 32]
|
||||
]) {
|
||||
for (const root of ['/home/ada/café/project', 'C:\\Users\\ada\\café\\project']) {
|
||||
const rows = Array.from({ length: count }, (_, index) => ({
|
||||
worktreeId: `w-${index}`,
|
||||
repoId: 'repo',
|
||||
path: `${root}/workspace-${index % unique}`
|
||||
}))
|
||||
const expected = arms.before('project', rows[0], rows)
|
||||
assert.deepEqual(arms.after('project', rows[0], rows), expected)
|
||||
const samples = { before: [], after: [] }
|
||||
function run(arm) {
|
||||
let length = 0
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
length += arms[arm]('project', rows[0], rows).length
|
||||
}
|
||||
const elapsed = performance.now() - start
|
||||
assert.equal(length, iterations * expected.length)
|
||||
return elapsed / iterations
|
||||
}
|
||||
for (const arm of ['before', 'after']) {
|
||||
run(arm)
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
unique,
|
||||
root,
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ baseline, node: process.version, platform: process.platform, iterations, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error('Usage: node config/scripts/mobile-linear-sort-benchmark.mjs <baseline-ref>')
|
||||
}
|
||||
async function load(file, contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {},
|
||||
plugins: [
|
||||
{
|
||||
name: 'theme-only',
|
||||
setup(bundler) {
|
||||
bundler.onResolve({ filter: /mobile-tasks-dependencies$/ }, () => ({
|
||||
path: resolve('mobile/src/theme/mobile-theme.ts')
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const file = 'mobile/src/tasks/mobile-tasks-reviewer-linear.ts'
|
||||
const before = await load(
|
||||
file,
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(file, readFileSync(file, 'utf8'))
|
||||
const results = []
|
||||
for (const count of [0, 1, 25, 200, 1000]) {
|
||||
const items = Array.from({ length: count }, (_, index) => ({
|
||||
id: `item-${index}`,
|
||||
identifier: `ENG-${(index * 37) % Math.max(1, count)}`,
|
||||
updatedAt: new Date(1700000000000 - index * 100000).toISOString(),
|
||||
priority: index % 5
|
||||
}))
|
||||
for (const sort of ['updated', 'identifier', 'priority']) {
|
||||
const arms = {
|
||||
before: () => [...items].sort((a, b) => before.compareLinearIssues(a, b, sort)),
|
||||
after: () => after.sortLinearIssues(items, sort)
|
||||
}
|
||||
assert.deepEqual(arms.after(), arms.before())
|
||||
const iterations = count < 100 ? 100 : 10
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm]()
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
run('before')
|
||||
run('after')
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
const dateParses = {}
|
||||
const nativeParse = Date.parse
|
||||
for (const arm of ['before', 'after']) {
|
||||
let calls = 0
|
||||
Date.parse = (value) => {
|
||||
calls++
|
||||
return nativeParse(value)
|
||||
}
|
||||
try {
|
||||
arms[arm]()
|
||||
} finally {
|
||||
Date.parse = nativeParse
|
||||
}
|
||||
dateParses[arm] = calls
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
sort,
|
||||
dateParses,
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'mobile/src/transport/connection-log-buffer.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
tsconfigRaw: {}
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(readFileSync(file, 'utf8'))
|
||||
const drain = () => new Promise((resolve) => setImmediate(resolve))
|
||||
async function run(module, count, startup) {
|
||||
let calls = 0
|
||||
let bytes = 0
|
||||
let stored = ''
|
||||
const store = module.createConnectionLogStore(200, {
|
||||
load: async () => [],
|
||||
save: async (_host, snapshot) => {
|
||||
stored = JSON.stringify(snapshot)
|
||||
calls++
|
||||
bytes += Buffer.byteLength(stored)
|
||||
}
|
||||
})
|
||||
if (!startup) {
|
||||
await store.hydrate('a')
|
||||
await drain()
|
||||
calls = 0
|
||||
bytes = 0
|
||||
}
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < count; i++) {
|
||||
store.append('a', { id: `${i}`, ts: i, level: 'info', message: `connection event ${i}` })
|
||||
}
|
||||
await drain()
|
||||
return { ms: performance.now() - start, calls, bytes, stored }
|
||||
}
|
||||
const results = []
|
||||
for (const count of [1, 25, 200, 1000]) {
|
||||
for (const startup of [false, true]) {
|
||||
const arms = { before, after }
|
||||
const initialBefore = await run(before, count, startup)
|
||||
const initialAfter = await run(after, count, startup)
|
||||
assert.equal(initialAfter.stored, initialBefore.stored)
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push((await run(arms[arm], count, startup)).ms)
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
startup,
|
||||
before: {
|
||||
calls: initialBefore.calls,
|
||||
bytes: initialBefore.bytes,
|
||||
ms: median(samples.before)
|
||||
},
|
||||
after: { calls: initialAfter.calls, bytes: initialAfter.bytes, ms: median(samples.after) }
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error(
|
||||
'Usage: node config/scripts/mobile-source-control-collation-benchmark.mjs <baseline-ref>'
|
||||
)
|
||||
}
|
||||
async function load(file, contents, name) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {}
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
)[name]
|
||||
}
|
||||
// Match git's path order, including its numeric-looking names, instead of inflating sort work with a shuffle.
|
||||
const paths = execFileSync('git', ['ls-files', '-z'], { maxBuffer: 16 * 1024 * 1024 })
|
||||
.toString()
|
||||
.split('\0')
|
||||
.filter(Boolean)
|
||||
const results = []
|
||||
for (const [file, name] of [
|
||||
['mobile/src/source-control/mobile-git-status.ts', 'buildMobileSourceControlSections'],
|
||||
['mobile/src/source-control/mobile-branch-compare.ts', 'buildMobileBranchCompareSection'],
|
||||
['mobile/src/session/mobile-diff-review-queue.ts', 'buildMobileDiffReviewQueue']
|
||||
]) {
|
||||
const arms = {
|
||||
before: await load(
|
||||
file,
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }),
|
||||
name
|
||||
),
|
||||
after: await load(file, readFileSync(file, 'utf8'), name)
|
||||
}
|
||||
for (const count of [0, 1, 17, 63, 1000]) {
|
||||
const step = Math.max(1, Math.floor(paths.length / Math.max(1, count)))
|
||||
const entries = Array.from({ length: count }, (_, index) => ({
|
||||
path: paths[index * step],
|
||||
area: 'unstaged',
|
||||
status: 'modified',
|
||||
...(index % 37 === 0 ? { conflictStatus: 'unresolved' } : {})
|
||||
}))
|
||||
const input =
|
||||
name === 'buildMobileDiffReviewQueue'
|
||||
? {
|
||||
worktreeId: 'workspace',
|
||||
statusEntries: entries,
|
||||
branchEntries: [],
|
||||
comments: [],
|
||||
reviewState: { version: 1, files: {} }
|
||||
}
|
||||
: entries
|
||||
assert.deepEqual(arms.after(input), arms.before(input))
|
||||
const iterations = count < 100 ? 100 : 10
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let index = 0; index < iterations; index++) {
|
||||
arms[arm](input)
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
run('before')
|
||||
run('after')
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
name,
|
||||
count,
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
baseline,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
locale: new Intl.Collator().resolvedOptions().locale,
|
||||
results
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error('Usage: node config/scripts/mobile-task-sort-benchmark.mjs <baseline-ref>')
|
||||
}
|
||||
async function load(file, contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {},
|
||||
plugins: [
|
||||
{
|
||||
name: 'theme-only',
|
||||
setup(bundler) {
|
||||
bundler.onResolve({ filter: /mobile-tasks-dependencies$/ }, () => ({
|
||||
path: resolve('mobile/src/theme/mobile-theme.ts')
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const file = 'mobile/src/tasks/mobile-tasks-repository-presentation.ts'
|
||||
const before = await load(
|
||||
file,
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(file, readFileSync(file, 'utf8'))
|
||||
const repos = new Map()
|
||||
const results = []
|
||||
for (const count of [0, 1, 25, 1000]) {
|
||||
const items = Array.from({ length: count }, (_, index) => ({
|
||||
key: `item-${index}`,
|
||||
provider: 'github',
|
||||
title: 'task',
|
||||
subtitle: '',
|
||||
status: 'open',
|
||||
updatedAt: new Date(1700000000000 - index * 100000).toISOString(),
|
||||
source: { repoId: `repo-${index % 25}`, repoName: `Repository ${index % 25}` }
|
||||
}))
|
||||
for (const sort of ['updated', 'repository']) {
|
||||
const arms = {
|
||||
before: () =>
|
||||
[...items].sort(
|
||||
sort === 'repository'
|
||||
? (a, b) => before.compareTasksByRepository(a, b, repos)
|
||||
: before.compareTasksByUpdated
|
||||
),
|
||||
after: () => after.sortMobileTaskItems(items, sort, repos)
|
||||
}
|
||||
assert.deepEqual(arms.after(), arms.before())
|
||||
const iterations = count < 100 ? 100 : 10
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm]()
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
run('before')
|
||||
run('after')
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
const dateParses = {}
|
||||
const nativeParse = Date.parse
|
||||
for (const arm of ['before', 'after']) {
|
||||
let calls = 0
|
||||
Date.parse = (value) => {
|
||||
calls++
|
||||
return nativeParse(value)
|
||||
}
|
||||
try {
|
||||
arms[arm]()
|
||||
} finally {
|
||||
Date.parse = nativeParse
|
||||
}
|
||||
dateParses[arm] = calls
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
sort,
|
||||
dateParses,
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,320 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe the baseline client module on stdin. Native process/filesystem operations are forbidden here.
|
||||
const entry = path.resolve('src/main/computer/macos-native-provider-client.ts')
|
||||
const sources = [readFileSync(0, 'utf8'), readFileSync(entry, 'utf8')]
|
||||
assert(sources.every((source) => source.includes('export class MacOSNativeProviderClient')))
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'native-client-receive-fixture',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /macos-native-provider-client\.ts$/ }, () => ({
|
||||
contents: `${source}\nexport { NativeProviderLineBuffer, consumeNativeProviderLines } from './macos-native-provider-transport';`,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(entry)
|
||||
}))
|
||||
builder.onResolve({ filter: /^node:(fs|child_process)$/ }, (args) => ({
|
||||
path: args.path,
|
||||
namespace: 'forbidden-native-operation'
|
||||
}))
|
||||
builder.onLoad({ filter: /.*/, namespace: 'forbidden-native-operation' }, (args) => ({
|
||||
contents: `function forbidden() { throw new Error('Native operations are forbidden in this benchmark'); }
|
||||
export { forbidden as ${
|
||||
args.path === 'node:fs'
|
||||
? 'chmodSync, forbidden as mkdtempSync, forbidden as rmSync, forbidden as writeFileSync, forbidden as existsSync'
|
||||
: 'spawn'
|
||||
} };`,
|
||||
loader: 'js'
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundled = `${result.outputFiles[0].text}\n//# sourceURL=native-provider-line-gate-benchmark-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(bundled).toString('base64')}`)
|
||||
}
|
||||
const modules = await Promise.all(sources.map(load))
|
||||
|
||||
class FixtureSocket {
|
||||
destroyed = false
|
||||
writes = []
|
||||
write(line) {
|
||||
this.writes.push(line)
|
||||
}
|
||||
end() {
|
||||
this.destroyed = true
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true
|
||||
}
|
||||
}
|
||||
|
||||
function clientFixture(module) {
|
||||
const client = new module.MacOSNativeProviderClient()
|
||||
const socket = new FixtureSocket()
|
||||
client.socket = socket
|
||||
return { client, socket, stale: new FixtureSocket(), events: [] }
|
||||
}
|
||||
|
||||
function state(fixture) {
|
||||
const { client, socket, events } = fixture
|
||||
return {
|
||||
buffered:
|
||||
typeof client.socketBuffer === 'string' ? client.socketBuffer : client.socketBuffer.pending,
|
||||
pending: [...client.pending.keys()],
|
||||
active: client.socket === socket,
|
||||
generation: client.socketStartGeneration,
|
||||
destroyed: socket.destroyed,
|
||||
writes: socket.writes,
|
||||
events
|
||||
}
|
||||
}
|
||||
|
||||
function register(fixture, id, throwCallback) {
|
||||
fixture.client.pending.set(id, {
|
||||
timer: undefined,
|
||||
resolve(value) {
|
||||
fixture.events.push(['resolve', id, value])
|
||||
if (throwCallback) {
|
||||
throw new Error('fixture callback failure')
|
||||
}
|
||||
},
|
||||
reject(error) {
|
||||
fixture.events.push(['reject', id, error.code, error.message])
|
||||
if (throwCallback) {
|
||||
throw new Error('fixture callback failure')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let seed = 0x18c0ffee
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return (seed >>> 8) % max
|
||||
}
|
||||
for (let trace = 0; trace < 2000; trace++) {
|
||||
const fixtures = modules.map(clientFixture)
|
||||
let remainder = ''
|
||||
for (let step = 0; step < 40; step++) {
|
||||
const op = random(20)
|
||||
const id = random(8)
|
||||
const throwCallback = random(12) === 0
|
||||
if (!remainder) {
|
||||
remainder = [
|
||||
`${JSON.stringify({ id, ok: true, result: { text: 'Unicode 界😀', value: step } })}\n`,
|
||||
`${JSON.stringify({ id, ok: false, error: { code: 'fixture', message: 'failed' } })}\r\n`,
|
||||
`${JSON.stringify({ id, ok: false })}\n`,
|
||||
' \t\r\n',
|
||||
'invalid json\n',
|
||||
'null\n',
|
||||
'\ud83d\udc00\n'
|
||||
][random(7)]
|
||||
}
|
||||
const length = random(remainder.length + 1)
|
||||
const chunk = remainder.slice(0, length)
|
||||
if (op > 5) {
|
||||
remainder = remainder.slice(length)
|
||||
}
|
||||
for (const fixture of fixtures) {
|
||||
const { client, socket } = fixture
|
||||
try {
|
||||
if (op === 0) {
|
||||
client.shutdown()
|
||||
} else if (op === 1) {
|
||||
client.handleSocketClose(socket)
|
||||
} else if (op === 2) {
|
||||
client.handleTransportError(socket, new Error('fixture transport error'))
|
||||
} else if (op === 3) {
|
||||
client.invalidateActiveSocketAfterWriteFailure(socket, new Error('fixture write error'))
|
||||
} else if (op === 4) {
|
||||
fixture.stale = socket
|
||||
fixture.socket = new FixtureSocket()
|
||||
client.socket = fixture.socket
|
||||
} else if (op === 5) {
|
||||
register(fixture, id, throwCallback)
|
||||
} else {
|
||||
client.handleSocketData(op === 6 ? fixture.stale : socket, chunk)
|
||||
}
|
||||
} catch (error) {
|
||||
fixture.events.push(['throw', error.name, error.message])
|
||||
}
|
||||
}
|
||||
assert.deepEqual(state(fixtures[1]), state(fixtures[0]))
|
||||
}
|
||||
}
|
||||
console.log('2,000 actual client receive/lifecycle traces / 80,000 commands match')
|
||||
|
||||
for (let trace = 0; trace < 1000; trace++) {
|
||||
const fixtures = modules.map(clientFixture)
|
||||
const failThird = random(2) === 0
|
||||
for (const fixture of fixtures) {
|
||||
for (let id = 1; id <= 4; id++) {
|
||||
register(fixture, id, id === 3 && failThird)
|
||||
}
|
||||
}
|
||||
const input = [
|
||||
JSON.stringify({ id: 1, ok: true, result: { text: `界😀 ${trace}` } }),
|
||||
JSON.stringify({ id: 2, ok: false, error: { code: 'fixture', message: 'failed' } }),
|
||||
JSON.stringify({ id: 3, ok: true, result: trace }),
|
||||
JSON.stringify({ id: 4, ok: true, result: 'final reply' }),
|
||||
''
|
||||
].join('\n')
|
||||
let offset = 0
|
||||
while (offset < input.length) {
|
||||
const length = 1 + random(80)
|
||||
const chunk = input.slice(offset, offset + length)
|
||||
offset += length
|
||||
for (const fixture of fixtures) {
|
||||
try {
|
||||
fixture.client.handleSocketData(fixture.socket, chunk)
|
||||
} catch (error) {
|
||||
fixture.events.push(['throw', error.name, error.message])
|
||||
}
|
||||
}
|
||||
assert.deepEqual(state(fixtures[1]), state(fixtures[0]))
|
||||
}
|
||||
for (const fixture of fixtures) {
|
||||
fixture.client.handleSocketData(fixture.socket, '')
|
||||
assert.equal(fixture.client.pending.size, 0)
|
||||
assert.deepEqual(fixture.events.at(-1), ['resolve', 4, 'final reply'])
|
||||
}
|
||||
assert.deepEqual(state(fixtures[1]), state(fixtures[0]))
|
||||
}
|
||||
console.log('1,000 fragmented multi-reply client journeys / 4,000 request settlements match')
|
||||
|
||||
class BaselineBuffer {
|
||||
pending = ''
|
||||
push(chunk, onLine) {
|
||||
this.pending += chunk
|
||||
this.pending = modules[0].consumeNativeProviderLines(this.pending, onLine)
|
||||
}
|
||||
clear() {
|
||||
this.pending = ''
|
||||
}
|
||||
}
|
||||
for (let trace = 0; trace < 3000; trace++) {
|
||||
const buffers = [new BaselineBuffer(), new modules[1].NativeProviderLineBuffer()]
|
||||
const events = [[], []]
|
||||
for (let step = 0; step < 30; step++) {
|
||||
const clear = random(25) === 0
|
||||
const fail = random(10) === 0
|
||||
const chunk = ['abc', '\n', '\r\n', '\ud83d', '\udc00', '\n\n', '界', '', 'ok\nfault\npartial'][
|
||||
random(9)
|
||||
]
|
||||
buffers.forEach((buffer, index) => {
|
||||
if (clear) {
|
||||
buffer.clear()
|
||||
}
|
||||
try {
|
||||
buffer.push(chunk, (line) => {
|
||||
events[index].push(line)
|
||||
if (fail) {
|
||||
throw new Error('fixture callback failure')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
events[index].push({ error: error.message })
|
||||
}
|
||||
})
|
||||
assert.deepEqual(events[1], events[0])
|
||||
assert.equal(buffers[1].pending, buffers[0].pending)
|
||||
}
|
||||
}
|
||||
console.log('3,000 actual line-buffer traces / 90,000 feeds match')
|
||||
|
||||
function receiveArm(module) {
|
||||
const fixture = clientFixture(module)
|
||||
return (chunks) => {
|
||||
let result
|
||||
fixture.client.pending.set(1, {
|
||||
timer: undefined,
|
||||
resolve: (value) => {
|
||||
result = value
|
||||
},
|
||||
reject: (error) => {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
for (const chunk of chunks) {
|
||||
fixture.client.handleSocketData(fixture.socket, chunk)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
function sample(arm, input, repeats) {
|
||||
const start = performance.now()
|
||||
let result
|
||||
for (let i = 0; i < repeats; i++) {
|
||||
result = arm(input)
|
||||
}
|
||||
return { elapsed: (performance.now() - start) / repeats, result }
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
unit: 'ms',
|
||||
pairs: 8
|
||||
})
|
||||
)
|
||||
for (const [size, chunkBytes] of [
|
||||
[64, 65536],
|
||||
[120000, 65536],
|
||||
[1200000, 65536],
|
||||
[1200000, 4096],
|
||||
[4800000, 65536],
|
||||
[1200000, Number.POSITIVE_INFINITY]
|
||||
]) {
|
||||
const expected = { screenshot: { data: 'A'.repeat(size) }, text: 'fixture' }
|
||||
const input = `${JSON.stringify({ id: 1, ok: true, result: expected })}\n`
|
||||
const chunks = []
|
||||
for (let offset = 0; offset < input.length; offset += chunkBytes) {
|
||||
chunks.push(input.slice(offset, offset + chunkBytes))
|
||||
}
|
||||
const arms = modules.map(receiveArm)
|
||||
for (const arm of arms) {
|
||||
assert.deepEqual(arm(chunks), expected)
|
||||
const until = performance.now() + 150
|
||||
while (performance.now() < until) {
|
||||
sample(arm, chunks, 1)
|
||||
}
|
||||
}
|
||||
const repeats = Math.max(3, Math.min(100000, Math.ceil(50 / sample(arms[0], chunks, 1).elapsed)))
|
||||
/** @type {number[][]} */
|
||||
const times = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
const result = sample(arms[index], chunks, repeats)
|
||||
assert.deepEqual(result.result, expected)
|
||||
times[index].push(result.elapsed)
|
||||
}
|
||||
}
|
||||
const median = times.map((values) => {
|
||||
values.sort((a, b) => a - b)
|
||||
return (values[3] + values[4]) / 2
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
size,
|
||||
chunkBytes: Number.isFinite(chunkBytes) ? chunkBytes : 'whole frame',
|
||||
chunks: chunks.length,
|
||||
repeats,
|
||||
median,
|
||||
times
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -19,24 +19,18 @@ describe('Electron runtime package contract', () => {
|
||||
})
|
||||
|
||||
it('keeps the native Windows registry addon optional and platform-gated', () => {
|
||||
const rebuildScript = readFileSync(
|
||||
join(projectDir, 'config/scripts/rebuild-native-deps.mjs'),
|
||||
'utf8'
|
||||
)
|
||||
const ensureScript = readFileSync(
|
||||
join(projectDir, 'config/scripts/ensure-native-runtime.mjs'),
|
||||
'utf8'
|
||||
)
|
||||
expect(packageJson.optionalDependencies['windows-native-registry']).toBe('3.2.2')
|
||||
const rebuildScript = readProject('config/scripts/rebuild-native-deps.mjs')
|
||||
const ensureScript = readProject('config/scripts/ensure-native-runtime.mjs')
|
||||
expect(packageJson.optionalDependencies['@orca/windows-registry']).toBe('workspace:*')
|
||||
// Why: pnpm installs optional target architectures on every host; the root
|
||||
// Windows-only rebuild owns this addon so macOS/Linux never run node-gyp for it.
|
||||
expect(pnpmWorkspace.allowBuilds['windows-native-registry']).toBe(false)
|
||||
expect(pnpmWorkspace.allowBuilds['@orca/windows-registry']).toBe(false)
|
||||
// Why assert the guard and the member separately: the list now carries more
|
||||
// than one addon, so pinning the whole literal only tested its formatting.
|
||||
expect(rebuildScript).toContain("rebuildPlatform === 'win32'")
|
||||
expect(rebuildScript).toContain("'windows-native-registry'")
|
||||
expect(rebuildScript).toContain("'@orca/windows-registry'")
|
||||
expect(ensureScript).toContain("process.platform === 'win32'")
|
||||
expect(ensureScript).toContain("'windows-native-registry'")
|
||||
expect(ensureScript).toContain("'@orca/windows-registry'")
|
||||
const packageTargets = {
|
||||
win32: createPackagedRuntimeNodeModuleResources('win32'),
|
||||
darwin: createPackagedRuntimeNodeModuleResources('darwin'),
|
||||
@@ -44,14 +38,14 @@ describe('Electron runtime package contract', () => {
|
||||
}
|
||||
expect(packageTargets.win32).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', 'windows-native-registry') }),
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-registry') }),
|
||||
expect.objectContaining({ to: join('node_modules', 'node-addon-api') })
|
||||
])
|
||||
)
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
expect(packageTargets[platform]).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', 'windows-native-registry') })
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-registry') })
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error('Usage: node config/scripts/plugin-command-bindings-benchmark.mjs <baseline-ref>')
|
||||
}
|
||||
async function load(file, contents, name) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {},
|
||||
banner: {
|
||||
js: "import { createRequire as benchmarkRequire } from 'node:module'; import { resolve as benchmarkPath } from 'node:path'; const require = benchmarkRequire(benchmarkPath('package.json'));"
|
||||
}
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
)[name]
|
||||
}
|
||||
const file = 'src/main/plugins/plugin-command-registry.ts'
|
||||
const before = await load(
|
||||
file,
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }),
|
||||
'PluginCommandRegistry'
|
||||
)
|
||||
const after = await load(file, readFileSync(file, 'utf8'), 'PluginCommandRegistry')
|
||||
const results = []
|
||||
for (const count of [1, 16, 64, 256]) {
|
||||
const plugins = [
|
||||
{
|
||||
pluginKey: 'sample.commands',
|
||||
manifest: {
|
||||
contributes: {
|
||||
commands: Array.from({ length: count }, (_, index) => ({
|
||||
id: `command-${index}`,
|
||||
title: `Command ${index}`,
|
||||
action: 'view.tasks'
|
||||
})),
|
||||
keybindings: Array.from({ length: Math.min(count, 104) }, (_, index) => ({
|
||||
command: `command-${index}`,
|
||||
key: `Mod+${Math.floor(index / 26) & 1 ? 'Alt+' : ''}${Math.floor(index / 26) & 2 ? 'Shift+' : ''}${String.fromCharCode(65 + (index % 26))}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
const arms = { before: new before(), after: new after() }
|
||||
for (const platform of ['darwin', 'linux', 'win32']) {
|
||||
for (const arm of Object.values(arms)) {
|
||||
arm.reconcile(plugins, () => true, {}, platform)
|
||||
}
|
||||
const snapshot = (registry) => ({
|
||||
active: registry.list(),
|
||||
previews: plugins.map((plugin) => registry.preview(plugin.pluginKey)),
|
||||
errors: plugins.map((plugin) => registry.error(plugin.pluginKey))
|
||||
})
|
||||
assert.deepEqual(snapshot(arms.after), snapshot(arms.before))
|
||||
}
|
||||
const iterations = 100
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm].reconcile(plugins, () => true, {}, 'linux')
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
run('before')
|
||||
run('after')
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
commands: count,
|
||||
bindings: Math.min(count, 104),
|
||||
beforeMs: median(samples.before),
|
||||
afterMs: median(samples.after),
|
||||
samples
|
||||
})
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'src/shared/plugins/plugin-panel-message-budget.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(readFileSync(file, 'utf8'))
|
||||
const results = []
|
||||
for (const count of [5, 1000, 100000]) {
|
||||
const entries = Array.from({ length: count }, (_, i) => [`key-${i}`, `value-${i}`])
|
||||
for (const [kind, value] of [
|
||||
['array', entries.map(([, value]) => value)],
|
||||
['map', new Map(entries)],
|
||||
['set', new Set(entries.map(([, value]) => value))],
|
||||
['object', Object.fromEntries(entries)]
|
||||
]) {
|
||||
const input = structuredClone(value)
|
||||
for (const cap of [0, 1, 64, 1024, 65536, Infinity]) {
|
||||
assert.equal(
|
||||
after.structuredCloneMessageBytes(input, cap),
|
||||
before.structuredCloneMessageBytes(input, cap)
|
||||
)
|
||||
}
|
||||
const arms = { before, after }
|
||||
const iterations = count < 100 ? 1000 : 10
|
||||
const run = (arm) => {
|
||||
global.gc?.()
|
||||
const start = performance.now()
|
||||
const cpuStart = process.cpuUsage()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm].structuredCloneMessageBytes(input)
|
||||
}
|
||||
const cpu = process.cpuUsage(cpuStart)
|
||||
return {
|
||||
ms: (performance.now() - start) / iterations,
|
||||
cpuMs: (cpu.user + cpu.system) / 1000 / iterations
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
run('before')
|
||||
run('after')
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
kind,
|
||||
beforeMs: median(samples.before.map((sample) => sample.ms)),
|
||||
beforeCpuMs: median(samples.before.map((sample) => sample.cpuMs)),
|
||||
afterMs: median(samples.after.map((sample) => sample.ms)),
|
||||
afterCpuMs: median(samples.after.map((sample) => sample.cpuMs)),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
baseline,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
forcedGc: Boolean(global.gc),
|
||||
results
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2]
|
||||
if (!baseline) {
|
||||
throw new Error(
|
||||
'Usage: node config/scripts/plugin-shortcut-conflicts-benchmark.mjs <baseline-ref>'
|
||||
)
|
||||
}
|
||||
async function load(file, contents, name) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
logLevel: 'silent',
|
||||
tsconfigRaw: {},
|
||||
banner: {
|
||||
js: "import { createRequire as benchmarkRequire } from 'node:module'; import { resolve as benchmarkPath } from 'node:path'; const require = benchmarkRequire(benchmarkPath('package.json'));"
|
||||
}
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
)[name]
|
||||
}
|
||||
const file = 'src/main/plugins/plugin-command-registry.ts'
|
||||
const before = await load(
|
||||
file,
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }),
|
||||
'PluginCommandRegistry'
|
||||
)
|
||||
const after = await load(file, readFileSync(file, 'utf8'), 'PluginCommandRegistry')
|
||||
const results = []
|
||||
for (const count of [1, 8, 32, 128]) {
|
||||
const plugins = Array.from({ length: count }, (_, index) => ({
|
||||
pluginKey: `sample.plugin-${index}`,
|
||||
manifest: {
|
||||
contributes: {
|
||||
commands: [
|
||||
{
|
||||
id: 'open',
|
||||
title: 'Open',
|
||||
action: 'view.tasks',
|
||||
context: index % 2 ? 'worktree' : 'global'
|
||||
}
|
||||
],
|
||||
keybindings: [{ command: 'open', key: 'Mod+Alt+T' }]
|
||||
}
|
||||
}
|
||||
}))
|
||||
const arms = { before: new before(), after: new after() }
|
||||
for (const platform of ['darwin', 'linux', 'win32']) {
|
||||
for (const arm of Object.values(arms)) {
|
||||
arm.reconcile(plugins, () => true, {}, platform)
|
||||
}
|
||||
const snapshot = (registry) => ({
|
||||
active: registry.list(),
|
||||
previews: plugins.map((plugin) => registry.preview(plugin.pluginKey)),
|
||||
errors: plugins.map((plugin) => registry.error(plugin.pluginKey))
|
||||
})
|
||||
assert.deepEqual(snapshot(arms.after), snapshot(arms.before))
|
||||
}
|
||||
const iterations = 100
|
||||
function run(arm) {
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm].reconcile(plugins, () => true, {}, 'linux')
|
||||
}
|
||||
return (performance.now() - start) / iterations
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
run('before')
|
||||
run('after')
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({ count, beforeMs: median(samples.before), afterMs: median(samples.after), samples })
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -215,6 +215,7 @@ const WINDOWS_PACKAGE_TESTS = [
|
||||
...LINUX_PACKAGE_TESTS,
|
||||
'config/scripts/rebuild-native-deps.test.mjs',
|
||||
'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs',
|
||||
'src/main/windows-registry-addon.test.ts',
|
||||
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
|
||||
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
|
||||
'src/shared/child-process/windows-command-line.win32.test.ts',
|
||||
|
||||
@@ -389,7 +389,7 @@ describe('PR workflow parallelism', () => {
|
||||
expect(cacheStep.with.key).toContain('config/scripts/ensure-native-runtime.mjs')
|
||||
expect(cacheStep.with.key).toContain('config/scripts/rebuild-native-deps.mjs')
|
||||
expect(cacheStep.with.path).toContain('node-pty@*/node_modules/node-pty/build')
|
||||
expect(cacheStep.with.path).toContain('windows-native-registry@')
|
||||
expect(cacheStep.with.path).toContain('@orca+windows-registry@')
|
||||
expect(cacheStep.with.path).toContain('@vscode+windows-process-tree@')
|
||||
expect(cacheStep.with['restore-keys']).toBeUndefined()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'src/renderer/src/components/editor/raw-markdown-html.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: `${contents}\nexport { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'`,
|
||||
loader: 'ts',
|
||||
resolveDir: dirname(resolve(file))
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
banner: {
|
||||
js: `import { createRequire } from 'node:module'; const require = createRequire(${JSON.stringify(resolve('package.json'))});`
|
||||
}
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const arms = {
|
||||
before: await load(
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8', windowsHide: true })
|
||||
),
|
||||
after: await load(readFileSync(file, 'utf8'))
|
||||
}
|
||||
const key = '0123456789abcdef0123456789abcdef'
|
||||
const codecs = Object.fromEntries(
|
||||
Object.entries(arms).map(([arm, module]) => [arm, module.createRichMarkdownEditorCodec(key)])
|
||||
)
|
||||
const results = []
|
||||
for (const [name, input] of [
|
||||
['plain', '# Heading\nOrdinary prose.'],
|
||||
['complete', 'before <!--metadata--> after <b>text</b>\n'.repeat(100)],
|
||||
['unclosed-1000', `prefix ${'<!--x'.repeat(1000)}`],
|
||||
['unclosed-8000', `prefix ${'<!--x'.repeat(8000)} <b>tail</b>`],
|
||||
['mixed-8000', `prefix <!--complete-->${'<!--x'.repeat(8000)} <b>tail</b>`],
|
||||
['protected', '\\<!--x `<!--x`\n```html\n<!--x\n```\n'],
|
||||
['transport', `before [[ORCA_RICH_MD:${key}:inline-html:%3Cb%3E]] and [[README.md]]`]
|
||||
]) {
|
||||
for (const htmlSuperscriptLinks of [false, true]) {
|
||||
const options = { htmlSuperscriptLinks }
|
||||
const invoke = (arm) =>
|
||||
arms[arm].encodeRawMarkdownHtmlForRichEditor(input, codecs[arm], options)
|
||||
assert.equal(invoke('after'), invoke('before'))
|
||||
const iterations = name.includes('8000') || name.includes('1000') ? 2 : 100
|
||||
function run(arm) {
|
||||
global.gc?.()
|
||||
const start = performance.now()
|
||||
const cpuStart = process.cpuUsage()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
invoke(arm)
|
||||
}
|
||||
const cpu = process.cpuUsage(cpuStart)
|
||||
return {
|
||||
ms: (performance.now() - start) / iterations,
|
||||
cpuMs: (cpu.user + cpu.system) / 1000 / iterations
|
||||
}
|
||||
}
|
||||
run('before')
|
||||
run('after')
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
name,
|
||||
htmlSuperscriptLinks,
|
||||
beforeCpuMs: median(samples.before.map((s) => s.cpuMs)),
|
||||
afterCpuMs: median(samples.after.map((s) => s.cpuMs)),
|
||||
beforeMs: median(samples.before.map((s) => s.ms)),
|
||||
afterMs: median(samples.after.map((s) => s.ms)),
|
||||
samples
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -239,9 +239,9 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
|
||||
})
|
||||
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(result.stdout).toContain('Rebuilding failed native modules: windows-native-registry')
|
||||
expect(result.stdout).toContain('Rebuilding failed native modules: @orca/windows-registry')
|
||||
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
|
||||
expect(rebuildCall.onlyModules).toEqual(['windows-native-registry'])
|
||||
expect(rebuildCall.onlyModules).toEqual(['@orca/windows-registry'])
|
||||
} finally {
|
||||
removeTreeSync(projectDir)
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ exports.loadNativeModule = function loadNativeModule(nativeName) {
|
||||
}
|
||||
|
||||
export function writeFakeWindowsRegistry(projectDir) {
|
||||
const registryDir = join(projectDir, 'node_modules', 'windows-native-registry')
|
||||
const registryDir = join(projectDir, 'node_modules', '@orca', 'windows-registry')
|
||||
mkdirSync(registryDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(registryDir, 'index.js'),
|
||||
|
||||
@@ -76,9 +76,7 @@ if (ignoreModules.length > 0) {
|
||||
const NATIVE_MODULES = [
|
||||
'node-pty',
|
||||
'cpu-features',
|
||||
...(rebuildPlatform === 'win32'
|
||||
? ['windows-native-registry', '@vscode/windows-process-tree']
|
||||
: [])
|
||||
...(rebuildPlatform === 'win32' ? ['@orca/windows-registry', '@vscode/windows-process-tree'] : [])
|
||||
]
|
||||
const onlyModules = NATIVE_MODULES.filter((m) => !ignoreModules.includes(m))
|
||||
const forceRebuild =
|
||||
@@ -542,7 +540,7 @@ if (failures.length > 0) {
|
||||
}
|
||||
|
||||
function loadNativeModule(moduleName) {
|
||||
if (moduleName === 'windows-native-registry') {
|
||||
if (moduleName === '@orca/windows-registry') {
|
||||
const registry = projectRequire(moduleName)
|
||||
// Why: the package defers loading its .node addon until the first registry call.
|
||||
registry.getRegistryKey(registry.HK.CU, 'Environment')
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'src/renderer/src/components/right-sidebar/pr-comment-fixing-reply-body.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })
|
||||
)
|
||||
const after = await load(readFileSync(file, 'utf8'))
|
||||
const results = []
|
||||
for (const [name, body] of [
|
||||
['short', 'Please rename this variable.'],
|
||||
[
|
||||
'100-lines',
|
||||
`<!-- metadata -->\n## Review findings\n${'Code sample with details\n'.repeat(100)}`
|
||||
],
|
||||
[
|
||||
'10000-lines',
|
||||
`<!-- metadata -->\n## Review findings\n${'Code sample with details\n'.repeat(10000)}`
|
||||
],
|
||||
['blank-10000-lines', '# > * - _ `\n'.repeat(10000)]
|
||||
]) {
|
||||
const comments = Array.from({ length: 10 }, (_, id) => ({
|
||||
id,
|
||||
author: 'reviewer',
|
||||
authorAvatarUrl: '',
|
||||
createdAt: '',
|
||||
url: '',
|
||||
body
|
||||
}))
|
||||
const arms = { before, after }
|
||||
assert.equal(
|
||||
after.buildPRCommentBatchConversationReplyBody(comments),
|
||||
before.buildPRCommentBatchConversationReplyBody(comments)
|
||||
)
|
||||
const run = (arm) => {
|
||||
global.gc?.()
|
||||
const start = performance.now()
|
||||
const cpuStart = process.cpuUsage()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
arms[arm].buildPRCommentBatchConversationReplyBody(comments)
|
||||
}
|
||||
const cpu = process.cpuUsage(cpuStart)
|
||||
return { ms: (performance.now() - start) / 10, cpuMs: (cpu.user + cpu.system) / 10000 }
|
||||
}
|
||||
run('before')
|
||||
run('after')
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
name,
|
||||
beforeCpuMs: median(samples.before.map((s) => s.cpuMs)),
|
||||
afterCpuMs: median(samples.after.map((s) => s.cpuMs)),
|
||||
beforeMs: median(samples.before.map((s) => s.ms)),
|
||||
afterMs: median(samples.after.map((s) => s.ms)),
|
||||
samples
|
||||
})
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, platform: process.platform, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../..', import.meta.url))
|
||||
const entry = join(root, 'src/renderer/src/components/editor/raw-markdown-html.ts')
|
||||
const source = await readFile(entry, 'utf8')
|
||||
const cachedProbe =
|
||||
/if \(index > fenceProbe\) \{[\s\S]*?fenceMatch = fencePrefix.exec\(normalizedContent\)\n \}/
|
||||
assert.match(source, cachedProbe)
|
||||
const oldProbe = String.raw`fenceMatch = normalizedContent.slice(index).match(/^\s*(\x60{3,}|~{3,})/)`
|
||||
const temp = await mkdtemp(join(tmpdir(), 'orca-rich-blank-bench-'))
|
||||
try {
|
||||
const scanners = {}
|
||||
for (const arm of ['baseline', 'current']) {
|
||||
const outfile = join(temp, `${arm}.cjs`)
|
||||
await build({
|
||||
stdin: {
|
||||
contents: `export { encodeRawMarkdownHtmlForRichEditor as encode } from './src/renderer/src/components/editor/raw-markdown-html'; export { createRichMarkdownEditorCodec as codec } from './src/renderer/src/components/editor/rich-markdown-source-transport';`,
|
||||
resolveDir: root
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile,
|
||||
plugins:
|
||||
arm === 'baseline'
|
||||
? [
|
||||
{
|
||||
name: 'old-probe',
|
||||
setup(plugin) {
|
||||
plugin.onLoad({ filter: /raw-markdown-html\.ts$/ }, () => ({
|
||||
contents: source.replace(cachedProbe, oldProbe),
|
||||
loader: 'ts',
|
||||
resolveDir: join(root, 'src/renderer/src/components/editor')
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
})
|
||||
const { encode, codec } = createRequire(import.meta.url)(outfile)
|
||||
scanners[arm] = (content) => encode(content, codec('0'.repeat(32)))
|
||||
}
|
||||
const fragments = [
|
||||
'\n',
|
||||
' \r\n',
|
||||
'\u00a0\u2028',
|
||||
'```\n',
|
||||
'~~~~\n',
|
||||
'<div>\n',
|
||||
'</div>\n',
|
||||
'[[doc.md]]\n',
|
||||
'`inline`\n',
|
||||
'prose\n'
|
||||
]
|
||||
let seed = 42
|
||||
for (let sample = 0; sample < 256; sample++) {
|
||||
let content = ''
|
||||
for (let i = 0; i < 20; i++) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
content += fragments[seed % fragments.length]
|
||||
}
|
||||
assert.equal(scanners.current(content), scanners.baseline(content))
|
||||
}
|
||||
for (const [name, content] of [
|
||||
['ordinary', 'ordinary prose\n'.repeat(10000)],
|
||||
['blank-100k', '\n'.repeat(100000)],
|
||||
['blank-before-fence', `${'\n'.repeat(30000)}\x60\x60\x60\n<div>\n\x60\x60\x60\n[[doc.md]]`]
|
||||
]) {
|
||||
const samples = { baseline: [], current: [] }
|
||||
let expected
|
||||
for (const arms of buildCounterbalancedSchedule(2, 'baseline', 'current')) {
|
||||
for (const arm of arms) {
|
||||
const start = performance.now()
|
||||
const result = scanners[arm](content)
|
||||
samples[arm].push(performance.now() - start)
|
||||
expected ??= result
|
||||
assert.equal(result, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name,
|
||||
bytes: Buffer.byteLength(content),
|
||||
samples,
|
||||
baseline: summarizeBenchmarkSamples(samples.baseline),
|
||||
current: summarizeBenchmarkSamples(samples.current)
|
||||
})
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
await rm(temp, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'src/renderer/src/components/editor/markdown-rich-mode.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'cached-round-trip',
|
||||
setup(bundler) {
|
||||
bundler.onResolve({ filter: /markdown-round-trip$|^@\/i18n\/i18n$/ }, (args) => ({
|
||||
path: args.path,
|
||||
namespace: 'bench'
|
||||
}))
|
||||
bundler.onLoad({ filter: /.*/, namespace: 'bench' }, (args) => ({
|
||||
contents: args.path.endsWith('markdown-round-trip')
|
||||
? 'export const getRichMarkdownRoundTripOutput = (content) => content'
|
||||
: 'export const translate = (_key, fallback) => fallback',
|
||||
loader: 'js'
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const arms = {
|
||||
before: await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })),
|
||||
after: await load(readFileSync(file, 'utf8'))
|
||||
}
|
||||
const results = []
|
||||
for (const [name, content] of [
|
||||
['plain', '# Heading\nOrdinary prose with <placeholder>.'],
|
||||
['complete', '<!-- metadata --><span>text</span>'],
|
||||
['unclosed-1000', '<!--x'.repeat(1000)],
|
||||
['unclosed-8000', '<!--x'.repeat(8000)],
|
||||
['preserved-html-and-unclosed-8000', `<span>text</span>${'<!--x'.repeat(8000)}<b>tail</b>`]
|
||||
]) {
|
||||
assert.equal(
|
||||
arms.after.getMarkdownRichModeUnsupportedReason(content),
|
||||
arms.before.getMarkdownRichModeUnsupportedReason(content)
|
||||
)
|
||||
const iterations = name.includes('unclosed') ? 2 : 100
|
||||
function run(arm) {
|
||||
global.gc?.()
|
||||
const start = performance.now()
|
||||
const cpuStart = process.cpuUsage()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
arms[arm].getMarkdownRichModeUnsupportedReason(content)
|
||||
}
|
||||
const cpu = process.cpuUsage(cpuStart)
|
||||
return {
|
||||
ms: (performance.now() - start) / iterations,
|
||||
cpuMs: (cpu.user + cpu.system) / 1000 / iterations
|
||||
}
|
||||
}
|
||||
run('before')
|
||||
run('after')
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(10, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm))
|
||||
}
|
||||
}
|
||||
const median = (values) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[4] + sorted[5]) / 2
|
||||
}
|
||||
results.push({
|
||||
name,
|
||||
beforeCpuMs: median(samples.before.map((s) => s.cpuMs)),
|
||||
afterCpuMs: median(samples.after.map((s) => s.cpuMs)),
|
||||
beforeMs: median(samples.before.map((s) => s.ms)),
|
||||
afterMs: median(samples.after.map((s) => s.ms)),
|
||||
samples
|
||||
})
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
baseline,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
roundTrip:
|
||||
'identity stub, modeling an already-cached lossless round trip; editor parsing excluded',
|
||||
results
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const file = 'src/renderer/src/components/editor/markdown-rich-mode.ts'
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'cached-round-trip',
|
||||
setup(bundler) {
|
||||
bundler.onResolve({ filter: /markdown-round-trip$|^@\/i18n\/i18n$/ }, (args) => ({
|
||||
path: args.path,
|
||||
namespace: 'bench'
|
||||
}))
|
||||
bundler.onLoad({ filter: /.*/, namespace: 'bench' }, (args) => ({
|
||||
contents: args.path.endsWith('markdown-round-trip')
|
||||
? 'export const getRichMarkdownRoundTripOutput = () => { throw new Error("unexpected editor round trip") }'
|
||||
: 'export const translate = (_key, fallback) => fallback',
|
||||
loader: 'js'
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const arms = {
|
||||
before: await load(
|
||||
execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8', windowsHide: true })
|
||||
),
|
||||
after: await load(readFileSync(file, 'utf8'))
|
||||
}
|
||||
const results = []
|
||||
for (const size of [20_000, 200_000, 600_000]) {
|
||||
for (const shape of ['lines', 'long-line']) {
|
||||
const phrase =
|
||||
shape === 'lines'
|
||||
? 'Ordinary prose with a little `code`.\n'
|
||||
: 'Ordinary prose with a little `code`. '
|
||||
const content = phrase.repeat(Math.ceil(size / phrase.length)).slice(0, size)
|
||||
const invoke = (arm) =>
|
||||
arms[arm].getMarkdownRichModeEligibilityDecision({ content, sizeOverridden: false })
|
||||
assert.deepEqual(invoke('after'), invoke('before'))
|
||||
assert.equal(invoke('after').exceedsSizeLimit, false)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
invoke('before')
|
||||
invoke('after')
|
||||
}
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
global.gc?.()
|
||||
const cpuStart = process.cpuUsage()
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
invoke(arm)
|
||||
}
|
||||
const ms = (performance.now() - start) / 20
|
||||
const cpu = process.cpuUsage(cpuStart)
|
||||
samples[arm].push({ ms, cpuMs: (cpu.user + cpu.system) / 20_000 })
|
||||
}
|
||||
}
|
||||
results.push({ size, shape, samples })
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
baseline,
|
||||
node: process.version,
|
||||
roundTrip: 'throwing stub; these inputs must never invoke it',
|
||||
results
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -11,6 +11,22 @@ const signalTarget = valueAfter('--signal-target') ?? 'app'
|
||||
const entrypoint = valueAfter('--entrypoint') ?? 'app'
|
||||
const intDelivery = valueAfter('--int-delivery') ?? 'foreground-process-group'
|
||||
const launcherExecOverlay = args.includes('--launcher-exec-overlay')
|
||||
const allEntrypoints = args.includes('--all-entrypoints')
|
||||
if (
|
||||
allEntrypoints &&
|
||||
['--entrypoint', '--signal-target', '--int-delivery', '--launcher-exec-overlay'].some((flag) =>
|
||||
args.includes(flag)
|
||||
)
|
||||
) {
|
||||
fail('--all-entrypoints cannot be combined with individual case options')
|
||||
}
|
||||
const cases = allEntrypoints
|
||||
? [
|
||||
{ entrypoint: 'app', signalTarget: 'app', intDelivery: 'foreground-process-group' },
|
||||
{ entrypoint: 'launcher', signalTarget: 'app', intDelivery: 'foreground-process-group' },
|
||||
{ entrypoint: 'appimage', signalTarget: 'serving-electron', intDelivery: 'pid' }
|
||||
]
|
||||
: [{ entrypoint, signalTarget, intDelivery }]
|
||||
if (!appImageArg) {
|
||||
fail('Usage: run-headless-serve-shutdown-docker.mjs --appimage /path/to/orca.AppImage')
|
||||
}
|
||||
@@ -98,50 +114,52 @@ try {
|
||||
].join(' && ')
|
||||
])
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
type: 'appimage_under_test',
|
||||
appImage,
|
||||
sha256,
|
||||
platform,
|
||||
signalTarget,
|
||||
entrypoint,
|
||||
intDelivery,
|
||||
launcherExecOverlay
|
||||
})
|
||||
)
|
||||
const failedSignals = []
|
||||
for (const signal of ['INT', 'TERM']) {
|
||||
const result = docker(
|
||||
[
|
||||
'run',
|
||||
'--rm',
|
||||
'--init',
|
||||
'--platform',
|
||||
for (const { entrypoint, signalTarget, intDelivery } of cases) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
type: 'appimage_under_test',
|
||||
appImage,
|
||||
sha256,
|
||||
platform,
|
||||
'--shm-size',
|
||||
'256m',
|
||||
'--name',
|
||||
`orca-headless-serve-shutdown-${signal.toLowerCase()}-${suffix}`,
|
||||
'-e',
|
||||
`ORCA_SIGNAL_TARGET=${signalTarget}`,
|
||||
'-e',
|
||||
`ORCA_TEST_ENTRYPOINT=${entrypoint}`,
|
||||
'-e',
|
||||
`ORCA_INT_DELIVERY=${intDelivery}`,
|
||||
'-v',
|
||||
`${appImage}:/input/orca.AppImage:ro`,
|
||||
'-v',
|
||||
`${artifactVolume}:/artifacts:ro`,
|
||||
image,
|
||||
signal
|
||||
],
|
||||
{ allowFailure: true }
|
||||
signalTarget,
|
||||
entrypoint,
|
||||
intDelivery,
|
||||
launcherExecOverlay
|
||||
})
|
||||
)
|
||||
process.stdout.write(result.stdout)
|
||||
process.stderr.write(result.stderr)
|
||||
if (result.status !== 0) {
|
||||
failedSignals.push(`${signal}:${result.status}`)
|
||||
for (const signal of ['INT', 'TERM']) {
|
||||
const result = docker(
|
||||
[
|
||||
'run',
|
||||
'--rm',
|
||||
'--init',
|
||||
'--platform',
|
||||
platform,
|
||||
'--shm-size',
|
||||
'256m',
|
||||
'--name',
|
||||
`orca-headless-serve-shutdown-${entrypoint}-${signal.toLowerCase()}-${suffix}`,
|
||||
'-e',
|
||||
`ORCA_SIGNAL_TARGET=${signalTarget}`,
|
||||
'-e',
|
||||
`ORCA_TEST_ENTRYPOINT=${entrypoint}`,
|
||||
'-e',
|
||||
`ORCA_INT_DELIVERY=${intDelivery}`,
|
||||
'-v',
|
||||
`${appImage}:/input/orca.AppImage:ro`,
|
||||
'-v',
|
||||
`${artifactVolume}:/artifacts:ro`,
|
||||
image,
|
||||
signal
|
||||
],
|
||||
{ allowFailure: true }
|
||||
)
|
||||
process.stdout.write(result.stdout)
|
||||
process.stderr.write(result.stderr)
|
||||
if (result.status !== 0) {
|
||||
failedSignals.push(`${entrypoint}:${signal}:${result.status}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failedSignals.length > 0) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <ref>:src/main/emulator/android/scrcpy-video-frame-parser.ts | node config/scripts/scrcpy-frame-buffering-benchmark.mjs
|
||||
async function load(source) {
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
}
|
||||
const before = (await load(readFileSync(0, 'utf8'))).parseScrcpyVideoFrames
|
||||
const after = (
|
||||
await load(readFileSync('src/main/emulator/android/scrcpy-video-frame-parser.ts', 'utf8'))
|
||||
).parseScrcpyVideoFrames
|
||||
const { RelayFrameBuffer } = await load(readFileSync('src/shared/relay-frame-buffer.ts', 'utf8'))
|
||||
|
||||
function reader(arm) {
|
||||
if (arm === 'before') {
|
||||
let pending = Buffer.alloc(0)
|
||||
return {
|
||||
read(chunk) {
|
||||
// Match the baseline session's copy before invoking its production parser.
|
||||
const result = before(Buffer.alloc(0), Buffer.concat([pending, chunk]))
|
||||
pending = result.pending
|
||||
return result.frames
|
||||
},
|
||||
pending: () => pending
|
||||
}
|
||||
}
|
||||
const pending = new RelayFrameBuffer()
|
||||
return {
|
||||
read(chunk) {
|
||||
// Include the session's mandatory ownership copy in the queued parser arm.
|
||||
if (chunk.length > 0) {
|
||||
pending.append(Buffer.from(chunk))
|
||||
}
|
||||
return after(pending)
|
||||
},
|
||||
pending: () => (pending.length > 0 ? pending.peek(pending.length) : Buffer.alloc(0))
|
||||
}
|
||||
}
|
||||
|
||||
function packet(size, meta = 123n) {
|
||||
const frame = Buffer.alloc(size + 12, 7)
|
||||
frame.writeBigUInt64BE(meta, 0)
|
||||
frame.writeUInt32BE(size, 8)
|
||||
return frame
|
||||
}
|
||||
|
||||
let seed = 42
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return seed % max
|
||||
}
|
||||
let differentialChunks = 0
|
||||
for (let trial = 0; trial < 1000; trial += 1) {
|
||||
const stream = Buffer.concat(
|
||||
Array.from({ length: 1 + random(8) }, (_, index) =>
|
||||
packet(random(256), (BigInt(random(4)) << 62n) | BigInt(index))
|
||||
)
|
||||
)
|
||||
const oldReader = reader('before')
|
||||
const newReader = reader('after')
|
||||
for (let offset = 0; offset < stream.length;) {
|
||||
const size = 1 + random(128)
|
||||
const chunk = stream.subarray(offset, offset + size)
|
||||
assert.deepEqual(newReader.read(chunk), oldReader.read(chunk))
|
||||
assert.deepEqual(newReader.pending(), oldReader.pending())
|
||||
assert.deepEqual(newReader.read(Buffer.alloc(0)), oldReader.read(Buffer.alloc(0)))
|
||||
differentialChunks += 1
|
||||
offset += size
|
||||
}
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const frameBytes of [32, 4096, 65_536, 1_048_576]) {
|
||||
const frame = packet(frameBytes)
|
||||
const expected = reader('before').read(frame)
|
||||
for (const chunkBytes of new Set([frame.length, 65_536, 4096, 1024])) {
|
||||
if (chunkBytes > frame.length) {
|
||||
continue
|
||||
}
|
||||
const chunks = []
|
||||
for (let offset = 0; offset < frame.length; offset += chunkBytes) {
|
||||
chunks.push(frame.subarray(offset, offset + chunkBytes))
|
||||
}
|
||||
const iterations = Math.max(10, Math.floor(4_194_304 / frameBytes))
|
||||
function run(arm, repeats) {
|
||||
const parser = reader(arm)
|
||||
let frames
|
||||
const started = performance.now()
|
||||
for (let iteration = 0; iteration < repeats; iteration += 1) {
|
||||
for (const chunk of chunks) {
|
||||
frames = parser.read(chunk)
|
||||
}
|
||||
}
|
||||
const ms = performance.now() - started
|
||||
assert.deepEqual(frames, expected)
|
||||
assert.equal(parser.pending().length, 0)
|
||||
return ms
|
||||
}
|
||||
run('before', iterations)
|
||||
run('after', iterations)
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm, iterations))
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
frameBytes,
|
||||
chunkBytes,
|
||||
iterations,
|
||||
meanMicrosecondsPerFrame: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
),
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialChunks, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,188 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const bundled = await build({
|
||||
stdin: {
|
||||
contents: "export * from './src/main/ai-vault/codex-session-root-dedup.ts'",
|
||||
resolveDir: process.cwd(),
|
||||
loader: 'ts'
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
const production = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
|
||||
function baseline(input) {
|
||||
const sessions = []
|
||||
for (let offset = 0; offset < input.length; offset += 8) {
|
||||
sessions.push(...input.slice(offset, offset + 8))
|
||||
const unique = production.dedupeCodexSessionsBySessionId(sessions)
|
||||
sessions.splice(0, sessions.length, ...unique)
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
||||
function incremental(input) {
|
||||
const sessions = new production.CodexSessionCollection()
|
||||
for (let offset = 0; offset < input.length; offset += 8) {
|
||||
for (const session of input.slice(offset, offset + 8)) {
|
||||
sessions.add(session)
|
||||
}
|
||||
}
|
||||
return [...sessions.values()]
|
||||
}
|
||||
|
||||
function makeSession(index, overrides = {}) {
|
||||
return Object.freeze({
|
||||
agent: 'codex',
|
||||
executionHostId: 'local',
|
||||
sessionId: `session-${index}`,
|
||||
filePath: `/home/ada/.codex/sessions/2026/09/11/rollout-session-${index}.jsonl`,
|
||||
codexHome: null,
|
||||
updatedAt: '2026-09-11T10:00:00.000Z',
|
||||
createdAt: null,
|
||||
modifiedAt: '2026-09-11T10:00:00.000Z',
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
function checkIdentities(actual, expected) {
|
||||
assert.equal(actual.length, expected.length)
|
||||
actual.forEach((session, index) => assert.equal(session, expected[index]))
|
||||
}
|
||||
|
||||
let seed = 90211
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 0x100000000) * max)
|
||||
}
|
||||
|
||||
if (production.CodexSessionCollection) {
|
||||
let batches = 0
|
||||
for (let trial = 0; trial < 2000; trial++) {
|
||||
const input = []
|
||||
const current = new production.CodexSessionCollection()
|
||||
let expected = []
|
||||
for (let batch = 0; batch < 20; batch++) {
|
||||
const added = Array.from({ length: 1 + random(8) }, () => {
|
||||
if (input.length && random(4) === 0) {
|
||||
return input[random(input.length)]
|
||||
}
|
||||
const index = random(12)
|
||||
const root = [
|
||||
'/home/ada/.codex',
|
||||
'/tmp/codex-runtime-home/home',
|
||||
'/tmp/codex-accounts/account/home',
|
||||
'/tmp/custom',
|
||||
'\\\\wsl$\\Ubuntu\\home\\ada\\.codex',
|
||||
'\\\\wsl.localhost\\ubuntu\\home\\ada\\.codex',
|
||||
'\\\\wsl$\\Debian\\home\\ada\\.codex',
|
||||
'C:\\Users\\Ada\\.codex'
|
||||
][random(8)]
|
||||
return makeSession(index, {
|
||||
agent: random(6) ? 'codex' : 'claude',
|
||||
executionHostId: random(5) ? 'local' : 'ssh:dev',
|
||||
sessionId: `session-${random(3)}`,
|
||||
codexHome: random(4) ? root : null,
|
||||
filePath: `${root}/sessions/${random(6) ? 'rollout-' : ''}${index}.jsonl`,
|
||||
updatedAt: [null, 'invalid', '2026-09-11T10:00:00Z', '2026-09-11T10:01:00Z'][random(4)],
|
||||
modifiedAt: random(4) ? '2026-09-11T10:00:00Z' : 'invalid'
|
||||
})
|
||||
})
|
||||
input.push(...added)
|
||||
expected = production.dedupeCodexSessionsBySessionId([...expected, ...added])
|
||||
added.forEach((session) => current.add(session))
|
||||
checkIdentities([...current.values()], expected)
|
||||
assert.equal(current.size, expected.length)
|
||||
batches++
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ differentialBatches: batches }))
|
||||
}
|
||||
|
||||
const workloads = []
|
||||
if (process.argv.includes('--verify-only')) {
|
||||
process.exit(0)
|
||||
}
|
||||
for (const count of [8, 100, 1000, 5000, 10000]) {
|
||||
workloads.push([`${count} unique Codex`, Array.from({ length: count }, (_, i) => makeSession(i))])
|
||||
workloads.push([
|
||||
`${count} Claude`,
|
||||
Array.from({ length: count }, (_, i) => makeSession(i, { agent: 'claude' }))
|
||||
])
|
||||
}
|
||||
for (const count of [1000, 5000]) {
|
||||
const input = Array.from({ length: count }, (_, i) => makeSession(i))
|
||||
const aliases = input.map((session) =>
|
||||
makeSession(0, {
|
||||
...session,
|
||||
codexHome: '/tmp/custom',
|
||||
filePath: session.filePath.replace('/home/ada/.codex', '/tmp/custom')
|
||||
})
|
||||
)
|
||||
workloads.push([`${count} late preferred roots`, [...aliases, ...input]])
|
||||
workloads.push([`${count} late losing roots`, [...input, ...aliases]])
|
||||
}
|
||||
|
||||
function median(samples) {
|
||||
const sorted = [...samples].sort((a, b) => a - b)
|
||||
const mid = Math.floor(sorted.length / 2)
|
||||
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch })
|
||||
)
|
||||
for (const [name, input] of workloads) {
|
||||
const expected = baseline(input)
|
||||
const arms = { baseline, ...(production.CodexSessionCollection ? { incremental } : {}) }
|
||||
const repeats = Math.max(1, Math.floor(5000 / input.length))
|
||||
const samples = { baseline: [], incremental: [] }
|
||||
for (const run of Object.values(arms)) {
|
||||
checkIdentities(run(input), expected)
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'incremental')) {
|
||||
for (const arm of pair) {
|
||||
if (!arms[arm]) {
|
||||
continue
|
||||
}
|
||||
const start = performance.now()
|
||||
let result
|
||||
for (let repeat = 0; repeat < repeats; repeat++) {
|
||||
result = arms[arm](input)
|
||||
}
|
||||
samples[arm].push((performance.now() - start) / repeats)
|
||||
checkIdentities(result, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name,
|
||||
medianMs: Object.fromEntries(
|
||||
Object.entries(samples)
|
||||
.filter(([, values]) => values.length)
|
||||
.map(([arm, values]) => [arm, median(values)])
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (global.gc && production.CodexSessionCollection) {
|
||||
for (const count of [1000, 10000]) {
|
||||
const input = Array.from({ length: count }, (_, index) => makeSession(index))
|
||||
global.gc()
|
||||
const before = process.memoryUsage().heapUsed
|
||||
const collection = new production.CodexSessionCollection()
|
||||
input.forEach((session) => collection.add(session))
|
||||
global.gc()
|
||||
const retainedBytes = process.memoryUsage().heapUsed - before
|
||||
checkIdentities([...collection.values()], input)
|
||||
console.log(JSON.stringify({ name: `${count} scan-local index`, retainedBytes }))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env node
|
||||
// git show <base>:src/main/ai-vault/session-scanner.ts | node config/scripts/session-scan-cutoff-benchmark.mjs
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import ts from 'typescript-api'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const baselineSource = ts.createSourceFile(
|
||||
'session-scanner.ts',
|
||||
readFileSync(0, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
)
|
||||
const baselineFunction = baselineSource.statements.find(
|
||||
(node) => ts.isFunctionDeclaration(node) && node.name?.text === 'canStopParsingSessions'
|
||||
)
|
||||
assert(baselineFunction, 'Pipe the baseline session-scanner.ts on stdin')
|
||||
|
||||
async function load(contents) {
|
||||
const result = await build({
|
||||
stdin: { contents, resolveDir: path.resolve('src/main/ai-vault'), loader: 'ts' },
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
bundle: true,
|
||||
write: false
|
||||
})
|
||||
const encoded = Buffer.from(result.outputFiles[0].text).toString('base64')
|
||||
return import(`data:text/javascript;base64,${encoded}`)
|
||||
}
|
||||
const [baselineModule, currentModule] = await Promise.all([
|
||||
load(`import { sessionSortTime } from './session-scanner-accumulator';
|
||||
export ${baselineFunction.getText(baselineSource)}`),
|
||||
load(`export { canStopParsingSessions } from './session-scan-cutoff';
|
||||
export { CodexSessionCollection } from './codex-session-root-dedup';`)
|
||||
])
|
||||
const baseline = baselineModule.canStopParsingSessions
|
||||
const current = currentModule.canStopParsingSessions
|
||||
const { CodexSessionCollection } = currentModule
|
||||
|
||||
let randomState = 91114
|
||||
function random(bound) {
|
||||
randomState = (Math.imul(randomState, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((randomState / 2 ** 32) * bound)
|
||||
}
|
||||
function session(index, overrides = {}) {
|
||||
return Object.freeze({
|
||||
agent: 'claude',
|
||||
executionHostId: 'local',
|
||||
sessionId: `session-${index}`,
|
||||
filePath: `/home/ada/.codex/sessions/rollout-${index}.jsonl`,
|
||||
codexHome: null,
|
||||
updatedAt: new Date(index).toISOString(),
|
||||
modifiedAt: new Date(0).toISOString(),
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
function collection(rows) {
|
||||
const result = new CodexSessionCollection()
|
||||
for (const row of rows) {
|
||||
result.add(row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
function check(sessions, limit, next) {
|
||||
const rows = [...sessions.values()]
|
||||
assert.equal(current(sessions, limit, next), baseline(sessions, limit, next))
|
||||
assert.deepEqual([...sessions.values()], rows)
|
||||
}
|
||||
|
||||
const dates = [
|
||||
null,
|
||||
'',
|
||||
'invalid',
|
||||
'1970-01-01T00:00:00Z',
|
||||
'1970-01-01T00:00:02+00:00',
|
||||
'-000001-01-01T00:00:00Z',
|
||||
'+010000-01-01T00:00:00Z',
|
||||
'-271821-04-20T00:00:00.000Z'
|
||||
]
|
||||
const limits = [0, -1, -3, 0.5, 1.5, Number.NaN, Infinity, -Infinity]
|
||||
const nextTimes = [undefined, Number.NaN, Infinity, -Infinity, 0, 1, 2, 2000]
|
||||
let comparisons = 0
|
||||
for (let trial = 0; trial < 4_000; trial += 1) {
|
||||
const sessions = new CodexSessionCollection()
|
||||
const admitted = []
|
||||
for (let batch = 0; batch < 10; batch += 1) {
|
||||
const count = random(8)
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const id = random(12)
|
||||
const row =
|
||||
admitted.length && random(5) === 0
|
||||
? admitted[random(admitted.length)]
|
||||
: session(id, {
|
||||
agent: random(3) ? 'codex' : 'claude',
|
||||
executionHostId: random(4) ? 'local' : 'ssh:dev',
|
||||
codexHome: random(2) ? null : '/custom',
|
||||
updatedAt: random(3)
|
||||
? new Date(random(5000) - 2500).toISOString()
|
||||
: dates[random(dates.length)],
|
||||
modifiedAt: random(4) ? new Date(random(5000)).toISOString() : 'invalid'
|
||||
})
|
||||
admitted.push(row)
|
||||
sessions.add(row)
|
||||
}
|
||||
const limit = random(3) ? 1 + random(40) : limits[random(limits.length)]
|
||||
const next = random(2) ? random(5000) - 2500 : nextTimes[random(nextTimes.length)]
|
||||
check(sessions, limit, next)
|
||||
comparisons += 1
|
||||
}
|
||||
}
|
||||
console.log(`${comparisons} differential batch cutoffs passed.`)
|
||||
|
||||
function median(values) {
|
||||
const sorted = values.toSorted((left, right) => left - right)
|
||||
return (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
|
||||
}
|
||||
function measure(name, run, repeats) {
|
||||
const expected = run(baseline)
|
||||
assert.deepEqual(run(current), expected)
|
||||
const sample = (cutoff) => {
|
||||
let result
|
||||
const start = performance.now()
|
||||
for (let index = 0; index < repeats; index += 1) {
|
||||
result = run(cutoff)
|
||||
}
|
||||
const elapsed = (performance.now() - start) / repeats
|
||||
assert.deepEqual(result, expected)
|
||||
return elapsed
|
||||
}
|
||||
sample(baseline)
|
||||
sample(current)
|
||||
const samples = { baseline: [], current: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'current')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(sample(arm === 'baseline' ? baseline : current))
|
||||
}
|
||||
}
|
||||
return { name, beforeMs: median(samples.baseline), afterMs: median(samples.current) }
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch })
|
||||
)
|
||||
const results = []
|
||||
for (const count of [8, 100, 1_000, 2_000, 10_000]) {
|
||||
for (const order of ['ordered', 'shuffled']) {
|
||||
const rows = Array.from({ length: count }, (_, index) => session(count - index))
|
||||
if (order === 'shuffled') {
|
||||
for (let index = count - 1; index > 0; index -= 1) {
|
||||
const other = random(index + 1)
|
||||
;[rows[index], rows[other]] = [rows[other], rows[index]]
|
||||
}
|
||||
}
|
||||
const sessions = collection(rows)
|
||||
for (const next of [0, count]) {
|
||||
results.push(
|
||||
measure(
|
||||
`${count} ${order} / ${next === 0 ? 'stop' : 'continue'}`,
|
||||
(cutoff) => cutoff(sessions, Math.ceil(count / 2), next),
|
||||
Math.max(20, Math.floor(30_000 / count))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const invalidIndex of [0, 999]) {
|
||||
const sessions = collection(
|
||||
Array.from({ length: 1_000 }, (_, index) =>
|
||||
session(index, invalidIndex === index ? { updatedAt: 'invalid' } : {})
|
||||
)
|
||||
)
|
||||
results.push(measure(`1000 invalid at ${invalidIndex}`, (cutoff) => cutoff(sessions, 500, 0), 50))
|
||||
}
|
||||
const rows = Array.from({ length: 2_000 }, () => session(random(2_000)))
|
||||
results.push(
|
||||
measure(
|
||||
'2000-candidate scan cutoff + admission / limit1000',
|
||||
(cutoff) => {
|
||||
const sessions = new CodexSessionCollection()
|
||||
let index = 0
|
||||
while (index < rows.length && !cutoff(sessions, 1_000, 10_000)) {
|
||||
const end = Math.min(rows.length, index + Math.min(8, Math.max(1, 1_000 - sessions.size)))
|
||||
while (index < end) {
|
||||
sessions.add(rows[index++])
|
||||
}
|
||||
}
|
||||
return { parsed: index, sessions: sessions.size }
|
||||
},
|
||||
2
|
||||
)
|
||||
)
|
||||
console.table(results)
|
||||
console.log('Synthetic cutoff/admission CPU; excludes discovery, parsing, I/O and final sorting.')
|
||||
@@ -0,0 +1,192 @@
|
||||
import { rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
createSessionParseStats,
|
||||
parseAgentSessionFileCached,
|
||||
resetSessionParseCacheForTests
|
||||
} from '../../src/main/ai-vault/session-scanner-parse-cache'
|
||||
import { resetTranscriptConsumersForTests } from '../../src/main/ai-vault/session-transcript-consumers'
|
||||
import { SessionSearchEngine } from '../../src/main/ai-vault-search/session-search-engine'
|
||||
import type {
|
||||
SessionSearchRequest,
|
||||
SessionSearchScope
|
||||
} from '../../src/main/ai-vault-search/session-search-engine-types'
|
||||
import { registerSessionSearchIndexConsumer } from '../../src/main/ai-vault-search/session-search-index-consumer'
|
||||
import { SessionSearchStore } from '../../src/main/ai-vault-search/session-search-store'
|
||||
import type SyncDatabase from '../../src/main/sqlite/sync-database'
|
||||
import {
|
||||
writeSyntheticTranscriptCorpus,
|
||||
type SyntheticCorpus,
|
||||
type SyntheticCorpusOptions
|
||||
} from '../../src/main/ai-vault-search/session-search-synthetic-corpus'
|
||||
import { sessionCandidate } from '../../src/main/ai-vault-search/session-search-transcript-fixtures'
|
||||
|
||||
// What a query costs, and what the session candidate limit buys. Everything
|
||||
// runs through the real store and the real engine over a synthetic corpus;
|
||||
// never point this at a real transcript tree.
|
||||
|
||||
const WARMUP = 5
|
||||
const SAMPLES = 25
|
||||
|
||||
// One query per rung the ladder can take, plus the two shapes that skip it.
|
||||
const QUERIES: { name: string; request: SessionSearchRequest }[] = [
|
||||
{ name: 'phrase', request: { query: '"terminal reattach"' } },
|
||||
{ name: 'identifier', request: { query: 'resolveTerminalPath' } },
|
||||
{ name: 'path', request: { query: 'src/main/ai-vault/session-transcript-reader.ts' } },
|
||||
{ name: 'prose', request: { query: 'why is the daemon snapshot stale' } },
|
||||
{ name: 'typo', request: { query: 'reattahc worktre' } },
|
||||
{ name: 'common-term', request: { query: 'index' } },
|
||||
{ name: 'operator-only', request: { query: 'repo:app-3' } },
|
||||
{ name: 'scoped', request: { query: 'worktree', filters: { scopePaths: ['/repo/app-3'] } } }
|
||||
]
|
||||
|
||||
type Timing = { p50: number; p95: number }
|
||||
|
||||
function percentile(sorted: readonly number[], fraction: number): number {
|
||||
const at = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))
|
||||
return Math.round((sorted[at] ?? 0) * 100) / 100
|
||||
}
|
||||
|
||||
function timing(samples: number[]): Timing {
|
||||
const sorted = [...samples].sort((left, right) => left - right)
|
||||
return { p50: percentile(sorted, 0.5), p95: percentile(sorted, 0.95) }
|
||||
}
|
||||
|
||||
function time(engine: SessionSearchEngine, request: SessionSearchRequest): number {
|
||||
const started = performance.now()
|
||||
engine.search(request)
|
||||
return performance.now() - started
|
||||
}
|
||||
|
||||
async function indexCorpus(
|
||||
options: SyntheticCorpusOptions
|
||||
): Promise<{ corpus: SyntheticCorpus; db: SyncDatabase; release: () => void }> {
|
||||
resetSessionParseCacheForTests()
|
||||
const corpus = await writeSyntheticTranscriptCorpus(options)
|
||||
const store = new SessionSearchStore(join(corpus.root, 'index.sqlite'), (error) => {
|
||||
throw error
|
||||
})
|
||||
const unregister = registerSessionSearchIndexConsumer(store)
|
||||
const stats = createSessionParseStats()
|
||||
for (const path of corpus.files) {
|
||||
await parseAgentSessionFileCached(
|
||||
await sessionCandidate('claude', path),
|
||||
process.platform,
|
||||
stats
|
||||
)
|
||||
}
|
||||
return {
|
||||
corpus,
|
||||
// The handle a composed reader gets. Every read here is one synchronous
|
||||
// statement, which is the contract that comes with it.
|
||||
db: store.connection,
|
||||
release: () => {
|
||||
unregister()
|
||||
resetTranscriptConsumersForTests()
|
||||
resetSessionParseCacheForTests()
|
||||
store.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-query and overall latency for one scope. */
|
||||
function scopeReport(db: SyncDatabase, scope: SessionSearchScope): Record<string, unknown> {
|
||||
const engine = new SessionSearchEngine(db)
|
||||
const everything: number[] = []
|
||||
const perQuery: Record<string, Timing & { hits: number; route: string }> = {}
|
||||
for (const { name, request } of QUERIES) {
|
||||
const scoped = { ...request, scope }
|
||||
for (let run = 0; run < WARMUP; run++) {
|
||||
engine.search(scoped)
|
||||
}
|
||||
const samples = Array.from({ length: SAMPLES }, () => time(engine, scoped))
|
||||
everything.push(...samples)
|
||||
const result = engine.search(scoped)
|
||||
perQuery[name] = { ...timing(samples), hits: result.hits.length, route: result.planner.route }
|
||||
}
|
||||
return { ...timing(everything), perQuery }
|
||||
}
|
||||
|
||||
/**
|
||||
* The candidate limit only costs anything once there are more matching sessions
|
||||
* than the limit, so this runs over many short sessions rather than the wide
|
||||
* corpus above. Limits are interleaved sample by sample: run back to back, the
|
||||
* first configuration pays for every page the OS cache had not seen yet and the
|
||||
* ordering alone moves p95 by more than the limit does.
|
||||
*/
|
||||
function candidateSweep(db: SyncDatabase, limits: readonly number[]): Record<string, unknown> {
|
||||
const request: SessionSearchRequest = { query: 'index', limit: 20 }
|
||||
const engines = new Map(
|
||||
limits.map((limit) => [limit, new SessionSearchEngine(db, { sessionCandidateLimit: limit })])
|
||||
)
|
||||
const samples = new Map(limits.map((limit) => [limit, [] as number[]]))
|
||||
for (let run = 0; run < WARMUP; run++) {
|
||||
for (const engine of engines.values()) {
|
||||
engine.search(request)
|
||||
}
|
||||
}
|
||||
for (let run = 0; run < SAMPLES; run++) {
|
||||
for (const limit of limits) {
|
||||
samples.get(limit)!.push(time(engines.get(limit)!, request))
|
||||
}
|
||||
}
|
||||
const report: Record<string, unknown> = {}
|
||||
for (const limit of limits) {
|
||||
const result = engines.get(limit)!.search(request)
|
||||
report[String(limit)] = {
|
||||
...timing(samples.get(limit)!),
|
||||
truncated: result.truncated.candidates,
|
||||
// Pages a caller could walk before the limit stops handing out sessions.
|
||||
reachablePages: Math.ceil(limit / (request.limit ?? 20))
|
||||
}
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
const wide = await indexCorpus({ sessions: Number(process.env.SESSIONS ?? 40) })
|
||||
let report: string
|
||||
try {
|
||||
const scope = {
|
||||
all: scopeReport(wide.db, 'all'),
|
||||
conversation: scopeReport(wide.db, 'conversation')
|
||||
}
|
||||
wide.release()
|
||||
await rm(wide.corpus.root, { recursive: true, force: true })
|
||||
|
||||
// Many short sessions: what makes the candidate limit binding is the session
|
||||
// count, not the byte count.
|
||||
const many = await indexCorpus({ sessions: 2500, turnsPerSession: 1, seed: 7 })
|
||||
try {
|
||||
report = JSON.stringify(
|
||||
{
|
||||
scopeCorpus: {
|
||||
sessions: wide.corpus.files.length,
|
||||
transcriptMb: Math.round((wide.corpus.transcriptBytes / 1024 / 1024) * 100) / 100,
|
||||
messages: wide.corpus.messageCount
|
||||
},
|
||||
scope,
|
||||
candidateCorpus: {
|
||||
sessions: many.corpus.files.length,
|
||||
transcriptMb: Math.round((many.corpus.transcriptBytes / 1024 / 1024) * 100) / 100
|
||||
},
|
||||
candidateSweep: candidateSweep(many.db, [200, 600, 1200, 2400])
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
} finally {
|
||||
many.release()
|
||||
await rm(many.corpus.root, { recursive: true, force: true })
|
||||
}
|
||||
} catch (error) {
|
||||
await rm(wide.corpus.root, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
|
||||
// Why a file as well as stdout: a runner that intercepts console output
|
||||
// (vitest does) would otherwise swallow the whole report.
|
||||
const out = process.env.BENCH_OUT
|
||||
if (out) {
|
||||
await writeFile(out, `${report}\n`)
|
||||
}
|
||||
console.log(report)
|
||||
@@ -0,0 +1,205 @@
|
||||
import { rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
createSessionParseStats,
|
||||
parseAgentSessionFileCached,
|
||||
resetSessionParseCacheForTests
|
||||
} from '../../src/main/ai-vault/session-scanner-parse-cache'
|
||||
import { resetTranscriptConsumersForTests } from '../../src/main/ai-vault/session-transcript-consumers'
|
||||
import { SessionSearchEngine } from '../../src/main/ai-vault-search/session-search-engine'
|
||||
import type {
|
||||
SessionSearchRequest,
|
||||
SessionSearchScope
|
||||
} from '../../src/main/ai-vault-search/session-search-engine-types'
|
||||
import { registerSessionSearchIndexConsumer } from '../../src/main/ai-vault-search/session-search-index-consumer'
|
||||
import { SessionSearchStore } from '../../src/main/ai-vault-search/session-search-store'
|
||||
import { sessionCandidate } from '../../src/main/ai-vault-search/session-search-transcript-fixtures'
|
||||
import type SyncDatabase from '../../src/main/sqlite/sync-database'
|
||||
import { writeToolHeavyCorpus, type ToolHeavyCorpus } from './session-search-tool-heavy-corpus'
|
||||
|
||||
// What each scope costs on an index the size of a real transcript tree.
|
||||
//
|
||||
// The 10.5 MB corpus in `session-search-query-benchmark.ts` sizes the route
|
||||
// ladder; this one sizes the corpus. `conversation` is a column filter over the
|
||||
// one FTS table rather than a second table of its own, and the whole cost of
|
||||
// that decision is how much of `messages_fts` a conversation query has to read
|
||||
// past — which is set by how much of a transcript is tool output.
|
||||
//
|
||||
// Synthetic, always: this must never be pointed at a real transcript.
|
||||
|
||||
const WARMUP = 5
|
||||
|
||||
/** Conversation-shaped queries; every term is one the prose actually uses. */
|
||||
const QUERIES = [
|
||||
'terminal reattach',
|
||||
'stale snapshot',
|
||||
'daemon cursor',
|
||||
'worktree index',
|
||||
'publish transaction',
|
||||
'relay daemon',
|
||||
'session cursor',
|
||||
'because stale',
|
||||
'terminal worktree',
|
||||
'index snapshot',
|
||||
'reattach cursor',
|
||||
'transaction relay',
|
||||
'snapshot session',
|
||||
'daemon publish',
|
||||
'worktree terminal',
|
||||
'cursor index',
|
||||
'stale relay',
|
||||
'session transaction',
|
||||
'publish snapshot',
|
||||
'reattach daemon'
|
||||
]
|
||||
|
||||
async function indexCorpus(
|
||||
corpus: ToolHeavyCorpus
|
||||
): Promise<{ db: SyncDatabase; release: () => void }> {
|
||||
resetSessionParseCacheForTests()
|
||||
const store = new SessionSearchStore(join(corpus.root, 'index.sqlite'), (error) => {
|
||||
throw error
|
||||
})
|
||||
const unregister = registerSessionSearchIndexConsumer(store)
|
||||
const stats = createSessionParseStats()
|
||||
for (const path of corpus.files) {
|
||||
await parseAgentSessionFileCached(
|
||||
await sessionCandidate('claude', path),
|
||||
process.platform,
|
||||
stats
|
||||
)
|
||||
}
|
||||
return {
|
||||
// The store's own handle, which is what a composed reader gets: every
|
||||
// retrieval is one synchronous statement, so nothing pins a WAL snapshot.
|
||||
db: store.connection,
|
||||
release: () => {
|
||||
unregister()
|
||||
resetTranscriptConsumersForTests()
|
||||
resetSessionParseCacheForTests()
|
||||
store.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Timing = { p50: number; p95: number }
|
||||
|
||||
function timing(samples: readonly number[]): Timing {
|
||||
const sorted = [...samples].sort((left, right) => left - right)
|
||||
const at = (fraction: number): number => {
|
||||
const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))
|
||||
return Math.round((sorted[index] ?? 0) * 100) / 100
|
||||
}
|
||||
return { p50: at(0.5), p95: at(0.95) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The query sets, one per rung of the ladder the engine may take.
|
||||
*
|
||||
* Which rung each one reaches is not forced, it is observed: samples are
|
||||
* bucketed by the route the engine reports, so the table says what was measured
|
||||
* rather than what was intended, and a query that lands on a different rung
|
||||
* than expected shows up as a bucket rather than as a wrong number.
|
||||
*/
|
||||
function queries(): string[] {
|
||||
const run = (index: number, length: number): string =>
|
||||
Array.from({ length }, (_unused, step) => QUERIES[(index + step) % QUERIES.length]).join(' ')
|
||||
return [
|
||||
// Two terms, unquoted: not literal, so straight to OR.
|
||||
...QUERIES,
|
||||
// Two terms, quoted: literal, and on this corpus any two of fourteen words
|
||||
// sit next to each other somewhere, so the phrase rung answers.
|
||||
...QUERIES.map((query) => `"${query}"`),
|
||||
// Eight terms, quoted: an ordered run that long does not occur in 105 MB of
|
||||
// draws from fourteen words, so the phrase rung misses and AND answers.
|
||||
...QUERIES.map((_query, index) => `"${run(index, 4)}"`)
|
||||
]
|
||||
}
|
||||
|
||||
type Bucket = { samples: number[]; hits: number }
|
||||
|
||||
/**
|
||||
* Both scopes over the same queries, interleaved scope by scope: run back to
|
||||
* back, the first one pays for every page the OS cache had not seen and the
|
||||
* ordering moves p95 more than the scope does.
|
||||
*/
|
||||
function scopeReport(db: SyncDatabase): Record<string, unknown> {
|
||||
const engine = new SessionSearchEngine(db)
|
||||
const scopes: SessionSearchScope[] = ['all', 'conversation']
|
||||
const requests: SessionSearchRequest[] = queries().map((query) => ({ query }))
|
||||
const buckets = new Map<string, Bucket>()
|
||||
for (let run = 0; run < WARMUP; run++) {
|
||||
for (const scope of scopes) {
|
||||
for (const request of requests) {
|
||||
engine.search({ ...request, scope })
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const request of requests) {
|
||||
for (const scope of scopes) {
|
||||
const started = performance.now()
|
||||
const result = engine.search({ ...request, scope })
|
||||
const elapsed = performance.now() - started
|
||||
const key = `${result.planner.route}/${scope}`
|
||||
const bucket = buckets.get(key) ?? { samples: [], hits: 0 }
|
||||
bucket.samples.push(elapsed)
|
||||
bucket.hits += result.hits.length
|
||||
buckets.set(key, bucket)
|
||||
}
|
||||
}
|
||||
const report: Record<string, unknown> = {}
|
||||
for (const [key, bucket] of [...buckets].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
report[key] = { ...timing(bucket.samples), samples: bucket.samples.length, hits: bucket.hits }
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
/** Bytes the FTS table occupies, which is the cost the deleted second table saved. */
|
||||
function indexBytes(db: SyncDatabase): Record<string, number> | { unavailable: string } {
|
||||
try {
|
||||
const sum = (where: string, ...values: string[]): number =>
|
||||
Number(
|
||||
(
|
||||
db
|
||||
.prepare(`SELECT COALESCE(SUM(pgsize),0) AS bytes FROM dbstat ${where}`)
|
||||
.get(...values) as { bytes: number }
|
||||
).bytes
|
||||
)
|
||||
return { total: sum(''), messagesFts: sum('WHERE name LIKE ?', 'messages_fts%') }
|
||||
} catch {
|
||||
// dbstat is a compile-time option; the latency numbers stand without it.
|
||||
return { unavailable: 'no dbstat' }
|
||||
}
|
||||
}
|
||||
|
||||
const corpus = await writeToolHeavyCorpus({
|
||||
targetBytes: Number(process.env.CORPUS_MB ?? 100) * 1024 * 1024,
|
||||
toolShare: Number(process.env.TOOL_SHARE ?? 0.9)
|
||||
})
|
||||
let report: string
|
||||
const indexed = await indexCorpus(corpus)
|
||||
try {
|
||||
report = JSON.stringify(
|
||||
{
|
||||
corpus: {
|
||||
sessions: corpus.files.length,
|
||||
transcriptMb: Math.round((corpus.transcriptBytes / 1024 / 1024) * 100) / 100,
|
||||
toolShareOfMessageText:
|
||||
Math.round((corpus.toolBytes / (corpus.toolBytes + corpus.proseBytes)) * 1000) / 1000
|
||||
},
|
||||
indexBytes: indexBytes(indexed.db),
|
||||
route: scopeReport(indexed.db)
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
} finally {
|
||||
indexed.release()
|
||||
await rm(corpus.root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const out = process.env.BENCH_OUT
|
||||
if (out) {
|
||||
await writeFile(out, `${report}\n`)
|
||||
}
|
||||
console.log(report)
|
||||
@@ -0,0 +1,152 @@
|
||||
import { mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// The corpus the scope benchmark runs over. Written here rather than by
|
||||
// `session-search-synthetic-corpus.ts` because what it costs to answer a
|
||||
// conversation query out of the one FTS table turns on the property that
|
||||
// generator fixes: how much of a transcript is tool output.
|
||||
//
|
||||
// Synthetic, always. This must never be pointed at a real transcript.
|
||||
|
||||
const PROSE = [
|
||||
'terminal',
|
||||
'reattach',
|
||||
'worktree',
|
||||
'the',
|
||||
'index',
|
||||
'cursor',
|
||||
'publish',
|
||||
'transaction',
|
||||
'relay',
|
||||
'daemon',
|
||||
'snapshot',
|
||||
'because',
|
||||
'stale',
|
||||
'session'
|
||||
]
|
||||
// Tool output is paths, hashes and log lines — and the same words the
|
||||
// conversation uses, because a `rg` over this repository prints them. That
|
||||
// overlap is what the benchmark turns on: it is what makes a conversation
|
||||
// term's posting list carry rows the column filter then has to discard. A tool
|
||||
// vocabulary disjoint from the prose would leave nothing to discard and measure
|
||||
// the wrong thing.
|
||||
const TOOL_ONLY = [
|
||||
'src/main/ai-vault/session-transcript-reader.ts',
|
||||
'node_modules/.pnpm/typescript@5.9.2',
|
||||
'0x00007ff8',
|
||||
'ENOENT',
|
||||
'drwxr-xr-x',
|
||||
'2026-09-10T00:00:00.000Z',
|
||||
'sha256:9f2c1a',
|
||||
'chunk-VHQ4NWQK.js',
|
||||
'warning:',
|
||||
'resolveTerminalPath',
|
||||
'byteOffset',
|
||||
'MAX_RETRIES'
|
||||
]
|
||||
// Half the tool tokens are conversation words. Deliberately pessimistic: the
|
||||
// more of a query term lives in `tool_text`, the more the column filter costs,
|
||||
// so a number measured here holds on a real transcript tree.
|
||||
const TOOL = [...PROSE, ...TOOL_ONLY]
|
||||
|
||||
function mulberry32(seed: number): () => number {
|
||||
let state = seed >>> 0
|
||||
return () => {
|
||||
state = (state + 0x6d2b79f5) >>> 0
|
||||
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
function words(random: () => number, vocabulary: readonly string[], count: number): string {
|
||||
const out: string[] = []
|
||||
for (let index = 0; index < count; index++) {
|
||||
out.push(vocabulary[Math.floor(random() * vocabulary.length)]!)
|
||||
}
|
||||
return out.join(' ')
|
||||
}
|
||||
|
||||
export type ToolHeavyCorpus = {
|
||||
root: string
|
||||
files: string[]
|
||||
transcriptBytes: number
|
||||
toolBytes: number
|
||||
proseBytes: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude JSONL transcripts whose tool output is `toolShare` of the message text.
|
||||
* One turn is a user question, an assistant answer, a tool call and its output;
|
||||
* only the last one grows with the share.
|
||||
*/
|
||||
export async function writeToolHeavyCorpus(args: {
|
||||
targetBytes: number
|
||||
toolShare: number
|
||||
seed?: number
|
||||
}): Promise<ToolHeavyCorpus> {
|
||||
const random = mulberry32(args.seed ?? 11)
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-search-convfts-'))
|
||||
const files: string[] = []
|
||||
const proseWordsPerTurn = 160
|
||||
// Tool and prose words are not the same length, so the share is over bytes.
|
||||
const proseBytesPerTurn = proseWordsPerTurn * 6
|
||||
const toolWordCount = Math.max(
|
||||
1,
|
||||
Math.round((proseBytesPerTurn * args.toolShare) / (1 - args.toolShare) / 22)
|
||||
)
|
||||
let transcriptBytes = 0
|
||||
let toolBytes = 0
|
||||
let proseBytes = 0
|
||||
for (let session = 0; transcriptBytes < args.targetBytes; session++) {
|
||||
const sessionId = `00000000-0000-4000-8000-${String(session).padStart(12, '0')}`
|
||||
const lines: string[] = []
|
||||
for (let turn = 0; turn < 40; turn++) {
|
||||
const at = new Date(1740000000000 + turn * 60_000).toISOString()
|
||||
const question = words(random, PROSE, 40)
|
||||
const answer = words(random, PROSE, proseWordsPerTurn - 40)
|
||||
const output = words(random, TOOL, toolWordCount)
|
||||
proseBytes += Buffer.byteLength(question) + Buffer.byteLength(answer)
|
||||
toolBytes += Buffer.byteLength(output)
|
||||
lines.push(
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
sessionId,
|
||||
timestamp: at,
|
||||
cwd: `/repo/app-${session % 7}`,
|
||||
gitBranch: 'main',
|
||||
message: { role: 'user', content: question }
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
sessionId,
|
||||
timestamp: at,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: 'claude-fable-5',
|
||||
content: [
|
||||
{ type: 'text', text: answer },
|
||||
{ type: 'tool_use', name: 'Bash', input: { command: 'rg needle' } }
|
||||
]
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
sessionId,
|
||||
timestamp: at,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: output }]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
const path = join(root, `${sessionId}.jsonl`)
|
||||
const body = `${lines.join('\n')}\n`
|
||||
await writeFile(path, body)
|
||||
transcriptBytes += Buffer.byteLength(body)
|
||||
files.push(path)
|
||||
}
|
||||
return { root, files, transcriptBytes, toolBytes, proseBytes }
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <ref>:src/main/ai-vault/session-scanner-accumulator.ts | node config/scripts/session-timeline-benchmark.mjs
|
||||
const target = resolve('src/main/ai-vault/session-scanner-accumulator.ts')
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: `
|
||||
export {createAccumulator, cloneSessionAccumulator, updateTimeline, finalizeSession}
|
||||
from './session-scanner-accumulator';
|
||||
export {createClaudeSessionParseState, consumeClaudeSessionLine}
|
||||
from './session-scanner-primary-parsers';`,
|
||||
loader: 'ts',
|
||||
resolveDir: dirname(target)
|
||||
},
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
plugins: [
|
||||
{
|
||||
name: 'timeline-baseline',
|
||||
setup(plugin) {
|
||||
plugin.onLoad({ filter: /session-scanner-accumulator\.ts$/ }, () => ({
|
||||
contents: source,
|
||||
loader: 'ts',
|
||||
resolveDir: dirname(target)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const implementations = {
|
||||
before: await load(readFileSync(0, 'utf8')),
|
||||
after: await load(readFileSync(target, 'utf8'))
|
||||
}
|
||||
const file = { path: 'timeline.jsonl', mtimeMs: 0, modifiedAt: '2026-01-01T00:00:00.000Z' }
|
||||
const create = (implementation) =>
|
||||
implementation.createAccumulator({ agent: 'claude', sessionId: 'timeline', file })
|
||||
const observable = ({ createdAt, updatedAt, latestTimestampMs }) => ({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
latestTimestampMs
|
||||
})
|
||||
|
||||
let seed = 42
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 2 ** 32) * max)
|
||||
}
|
||||
const tokens = [
|
||||
null,
|
||||
undefined,
|
||||
'',
|
||||
'bad',
|
||||
0,
|
||||
-1,
|
||||
Infinity,
|
||||
Number.NaN,
|
||||
8_640_000_000_000_001,
|
||||
'1969-12-31T23:59:59.999Z',
|
||||
'-000001-01-01T00:00:00.000Z',
|
||||
'+010000-01-01T00:00:00.000Z',
|
||||
'2026-01-01T01:00:00+01:00',
|
||||
1_700_000_000.0009,
|
||||
1_700_000_000_000.9,
|
||||
1_700_000_000_000.1,
|
||||
1_700_000_000_000 - 0.1
|
||||
]
|
||||
let differentialUpdates = 0
|
||||
for (let trial = 0; trial < 3000; trial += 1) {
|
||||
let before = create(implementations.before)
|
||||
let after = create(implementations.after)
|
||||
for (let index = 0; index < 32; index += 1) {
|
||||
const input = random(2) ? tokens[random(tokens.length)] : 1_700_000_000_000 + random(1000) / 10
|
||||
const update = (implementation, state) => {
|
||||
try {
|
||||
implementation.updateTimeline(state, input)
|
||||
} catch (error) {
|
||||
return String(error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
assert.equal(update(implementations.after, after), update(implementations.before, before))
|
||||
assert.deepEqual(observable(after), observable(before))
|
||||
if (index === 15) {
|
||||
before = implementations.before.cloneSessionAccumulator(before)
|
||||
after = implementations.after.cloneSessionAccumulator(after)
|
||||
}
|
||||
differentialUpdates += 1
|
||||
}
|
||||
assert.deepEqual(
|
||||
implementations.after.finalizeSession(after, 'linux'),
|
||||
implementations.before.finalizeSession(before, 'linux')
|
||||
)
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const records of [100, 10_000, 100_000]) {
|
||||
for (const workload of [
|
||||
'numeric-timeline',
|
||||
'iso-timeline',
|
||||
'out-of-order-timeline',
|
||||
'claude-record-fold'
|
||||
]) {
|
||||
const timestamps = Array.from({ length: records }, (_, index) => {
|
||||
const ms =
|
||||
1_700_000_000_000 + (workload === 'out-of-order-timeline' ? random(records) : index)
|
||||
return workload === 'numeric-timeline' ? ms : new Date(ms).toISOString()
|
||||
})
|
||||
const lines =
|
||||
workload === 'claude-record-fold'
|
||||
? timestamps.map((timestamp, index) =>
|
||||
JSON.stringify({
|
||||
type: index % 2 ? 'assistant' : 'user',
|
||||
sessionId: 'timeline',
|
||||
timestamp,
|
||||
message: {
|
||||
role: index % 2 ? 'assistant' : 'user',
|
||||
content: 'Example transcript message'
|
||||
}
|
||||
})
|
||||
)
|
||||
: []
|
||||
function run(arm) {
|
||||
const implementation = implementations[arm]
|
||||
const parser = implementation.createClaudeSessionParseState(file)
|
||||
const state = workload === 'claude-record-fold' ? parser.accumulator : create(implementation)
|
||||
const started = performance.now()
|
||||
if (workload === 'claude-record-fold') {
|
||||
for (const line of lines) {
|
||||
implementation.consumeClaudeSessionLine(parser, line)
|
||||
}
|
||||
} else {
|
||||
for (const timestamp of timestamps) {
|
||||
implementation.updateTimeline(state, timestamp)
|
||||
}
|
||||
}
|
||||
const ms = performance.now() - started
|
||||
return {
|
||||
ms,
|
||||
result: implementation.finalizeSession(state, 'linux'),
|
||||
timeline: observable(state)
|
||||
}
|
||||
}
|
||||
const expected = run('before')
|
||||
assert.deepEqual(run('after').result, expected.result)
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
const actual = run(arm)
|
||||
samples[arm].push(actual.ms)
|
||||
assert.deepEqual(actual.result, expected.result)
|
||||
assert.deepEqual(actual.timeline, expected.timeline)
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
records,
|
||||
workload,
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialUpdates, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,149 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const path = 'src/renderer/src/components/skills/skill-source-inventory.ts'
|
||||
const arms = {}
|
||||
for (const [name, source] of [
|
||||
['baseline', readFileSync(0, 'utf8')],
|
||||
['indexed', readFileSync(path, 'utf8')]
|
||||
]) {
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
const loaded = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
assert.equal(typeof loaded.summarizeSkillSources, 'function', 'Pipe the baseline module on stdin')
|
||||
arms[name] = loaded.summarizeSkillSources
|
||||
}
|
||||
|
||||
function verify(result) {
|
||||
const expected = arms.baseline(result)
|
||||
const actual = arms.indexed(result)
|
||||
assert.deepEqual(actual, expected)
|
||||
actual.forEach((entry, index) => assert.equal(entry.source, result.sources[index]))
|
||||
return expected
|
||||
}
|
||||
|
||||
let seed = 20260911
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 0x100000000) * max)
|
||||
}
|
||||
const paths = [
|
||||
'/home/ada/.agents/skills',
|
||||
'/repo/.agents/skills',
|
||||
'/REPO/.agents/skills',
|
||||
'/remote/folder/.claude/skills',
|
||||
'C:\\Users\\Ada\\.codex\\skills',
|
||||
'\\\\wsl$\\Ubuntu\\home\\ada\\.agents\\skills',
|
||||
'',
|
||||
'/not-listed'
|
||||
]
|
||||
verify(null)
|
||||
for (let trial = 0; trial < 5000; trial++) {
|
||||
const sources = Array.from({ length: random(25) }, (_, index) =>
|
||||
Object.freeze({
|
||||
id: `${index}`,
|
||||
path: paths[random(paths.length - 1)],
|
||||
exists: Boolean(random(2)),
|
||||
skippedReason: [undefined, 'missing', 'remote-repo', 'unavailable'][random(4)]
|
||||
})
|
||||
)
|
||||
const skills = []
|
||||
const count = random(100)
|
||||
for (let index = 0; index < count; index++) {
|
||||
skills.push(
|
||||
skills.length && random(4) === 0
|
||||
? skills[random(skills.length)]
|
||||
: Object.freeze({
|
||||
rootPath: paths[random(paths.length)],
|
||||
rootPaths: random(3)
|
||||
? Object.freeze(Array.from({ length: random(15) }, () => paths[random(paths.length)]))
|
||||
: undefined
|
||||
})
|
||||
)
|
||||
}
|
||||
verify(Object.freeze({ sources: Object.freeze(sources), skills: Object.freeze(skills) }))
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
differentialCases: 5001,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch
|
||||
})
|
||||
)
|
||||
|
||||
function workload(sourceCount, skillCount, rootsPerSkill) {
|
||||
const paths = Array.from(
|
||||
{ length: sourceCount || 1 },
|
||||
(_, index) => `/repo-${index}/.agents/skills`
|
||||
)
|
||||
return {
|
||||
sources: paths.slice(0, sourceCount).map((path, index) => ({
|
||||
id: `${index}`,
|
||||
path,
|
||||
exists: index % 3 !== 0,
|
||||
skippedReason: index % 5 ? 'missing' : 'unavailable'
|
||||
})),
|
||||
skills: Array.from({ length: skillCount }, (_, index) => ({
|
||||
rootPath: paths[index % paths.length],
|
||||
rootPaths: Array.from(
|
||||
{ length: rootsPerSkill },
|
||||
(_, rootIndex) => paths[(index + rootIndex) % paths.length]
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[3] + sorted[4]) / 2
|
||||
}
|
||||
|
||||
for (const [sourceCount, skillCount, rootsPerSkill] of [
|
||||
[0, 1000, 1],
|
||||
[1, 1000, 0],
|
||||
[1, 1000, 1],
|
||||
[17, 0, 0],
|
||||
[17, 20, 1],
|
||||
[17, 200, 1],
|
||||
[24, 1000, 3],
|
||||
[87, 1000, 3],
|
||||
[367, 5000, 3],
|
||||
[17, 200, 17]
|
||||
]) {
|
||||
const input = workload(sourceCount, skillCount, rootsPerSkill)
|
||||
const expected = verify(input)
|
||||
const samples = { baseline: [], indexed: [] }
|
||||
const repeats = Math.max(
|
||||
5,
|
||||
Math.floor(200000 / (Math.max(1, sourceCount) * Math.max(1, skillCount)))
|
||||
)
|
||||
for (const run of Object.values(arms)) {
|
||||
for (let warmup = 0; warmup < Math.min(100, repeats); warmup++) {
|
||||
run(input)
|
||||
}
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'indexed')) {
|
||||
for (const arm of pair) {
|
||||
const start = performance.now()
|
||||
let result
|
||||
for (let repeat = 0; repeat < repeats; repeat++) {
|
||||
result = arms[arm](input)
|
||||
}
|
||||
samples[arm].push((performance.now() - start) / repeats)
|
||||
assert.deepEqual(result, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
sourceCount,
|
||||
skillCount,
|
||||
rootsPerSkill,
|
||||
medianMs: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [arm, median(values)])
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe the baseline ssh-target-cleanup.ts source on stdin; no app or network is used.
|
||||
const entry = path.resolve('src/renderer/src/store/slices/ssh-target-cleanup.ts')
|
||||
const sources = [readFileSync(0, 'utf8'), readFileSync(entry, 'utf8')]
|
||||
assert(
|
||||
sources.every((source) => source.includes('export function buildRemovedSshTargetCleanupPatch'))
|
||||
)
|
||||
|
||||
async function load(source, instrument = false) {
|
||||
if (instrument) {
|
||||
const spread = '...nextTabsByWorktree'
|
||||
assert.equal(source.split(spread).length, 2)
|
||||
source = source.replace(spread, '...countTabMapCopy(nextTabsByWorktree)')
|
||||
source += `
|
||||
export const tabMapCopies = { count: 0, entries: 0 };
|
||||
function countTabMapCopy(map) {
|
||||
tabMapCopies.count++;
|
||||
tabMapCopies.entries += Reflect.ownKeys(map).length;
|
||||
return map;
|
||||
}
|
||||
`
|
||||
}
|
||||
source += "\nexport { toAppSshPtyId } from '../../../../shared/ssh-pty-id';"
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'cleanup-source',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /ssh-target-cleanup\.ts$/ }, () => ({
|
||||
contents: source,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(entry)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundled = `${result.outputFiles[0].text}\n//# sourceURL=ssh-cleanup-benchmark-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(bundled).toString('base64')}`)
|
||||
}
|
||||
|
||||
const modules = await Promise.all(sources.map((source) => load(source)))
|
||||
const arms = modules.map((module) => module.buildRemovedSshTargetCleanupPatch)
|
||||
const { toAppSshPtyId } = modules[0]
|
||||
|
||||
function emptyState() {
|
||||
return {
|
||||
repos: [],
|
||||
worktreesByRepo: {},
|
||||
detectedWorktreesByRepo: {},
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: {},
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
lastKnownRelayPtyIdByTabId: {},
|
||||
pendingCodexPaneRestartIds: {},
|
||||
codexRestartNoticeByPtyId: {},
|
||||
deferredSshSessionIdsByTabId: {},
|
||||
pendingReconnectPtyIdByTabId: {},
|
||||
directSshPaneRetryByTabId: {},
|
||||
directSshLivePtyBindingByTabId: {},
|
||||
directSshPaneRetryHistoryByTabId: {},
|
||||
deferredSshReconnectTargets: [],
|
||||
transientClearedAgentStatusConnectionIds: {},
|
||||
sshConnectionStates: new Map(),
|
||||
sshTargetLabels: new Map(),
|
||||
sshTargetGenerations: new Map(),
|
||||
remoteWorkspaceHydratedTargetIds: new Set(),
|
||||
remoteWorkspaceSyncStatusByTargetId: {},
|
||||
portForwardsByConnection: {},
|
||||
detectedPortsByConnection: {},
|
||||
sshCredentialQueue: []
|
||||
}
|
||||
}
|
||||
|
||||
function freezeState(value) {
|
||||
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
||||
return value
|
||||
}
|
||||
if (value instanceof Map || value instanceof Set) {
|
||||
for (const item of value.values()) {
|
||||
freezeState(item)
|
||||
}
|
||||
} else {
|
||||
for (const item of Object.values(value)) {
|
||||
freezeState(item)
|
||||
}
|
||||
}
|
||||
return Object.freeze(value)
|
||||
}
|
||||
|
||||
function tab(id, worktreeId, ptyId) {
|
||||
return {
|
||||
id,
|
||||
worktreeId,
|
||||
ptyId,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0,
|
||||
pendingActivationSpawn: true
|
||||
}
|
||||
}
|
||||
|
||||
function timedState(count, stride, catalog, tabsPerWorkspace = 1) {
|
||||
const state = emptyState()
|
||||
if (catalog) {
|
||||
state.repos.push({ id: 'repo', path: '/remote', connectionId: 'removed' })
|
||||
}
|
||||
state.worktreesByRepo.repo = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const selected = stride > 0 && i % stride === 0
|
||||
const worktreeId = catalog ? `repo::/remote/${i}` : `folder:${i}`
|
||||
const ptyId = toAppSshPtyId(selected ? 'removed' : 'other', `pty-${i}`)
|
||||
state.tabsByWorktree[worktreeId] = Array.from({ length: tabsPerWorkspace }, (_, j) =>
|
||||
tab(`tab-${i}-${j}`, worktreeId, ptyId)
|
||||
)
|
||||
if (catalog) {
|
||||
state.worktreesByRepo.repo.push({ id: worktreeId, repoId: 'repo', path: `/remote/${i}` })
|
||||
}
|
||||
}
|
||||
return freezeState(state)
|
||||
}
|
||||
|
||||
function compare(state, targetId) {
|
||||
const before = structuredClone(state)
|
||||
const results = arms.map((arm) => arm(state, targetId))
|
||||
assert.deepEqual(results[1], results[0])
|
||||
assert.deepEqual(state, before)
|
||||
for (const key of Object.keys(results[0] ?? {})) {
|
||||
assert.equal(results[1][key] === state[key], results[0][key] === state[key])
|
||||
}
|
||||
for (const [key, tabs] of Object.entries(state.tabsByWorktree)) {
|
||||
const next = results.map((result) => result?.tabsByWorktree?.[key] ?? tabs)
|
||||
assert.equal(next[1] === tabs, next[0] === tabs)
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
assert.equal(next[1][i] === tabs[i], next[0][i] === tabs[i])
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
let seed = 0x15c0ffee
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return (seed >>> 8) % max
|
||||
}
|
||||
|
||||
for (let iteration = 0; iteration < 3000; iteration++) {
|
||||
const state = emptyState()
|
||||
const targets = ['removed', 'other', 'space / @ Unicode 🐳']
|
||||
for (const targetId of targets) {
|
||||
state.repos.push({ id: 'collision', path: `/remote/${targetId}`, connectionId: targetId })
|
||||
if (random(2)) {
|
||||
state.sshConnectionStates.set(targetId, { targetId, status: 'disconnected' })
|
||||
}
|
||||
if (random(2)) {
|
||||
state.sshTargetLabels.set(targetId, targetId)
|
||||
}
|
||||
if (random(2)) {
|
||||
state.sshTargetGenerations.set(targetId, random(10))
|
||||
}
|
||||
if (random(2)) {
|
||||
state.remoteWorkspaceHydratedTargetIds.add(targetId)
|
||||
}
|
||||
if (random(2)) {
|
||||
state.deferredSshReconnectTargets.push(targetId)
|
||||
}
|
||||
if (random(2)) {
|
||||
state.transientClearedAgentStatusConnectionIds[targetId] = true
|
||||
}
|
||||
if (random(2)) {
|
||||
state.remoteWorkspaceSyncStatusByTargetId[targetId] = { phase: 'synced' }
|
||||
}
|
||||
if (random(2)) {
|
||||
state.portForwardsByConnection[targetId] = [{ localPort: 8000 }]
|
||||
}
|
||||
if (random(2)) {
|
||||
state.detectedPortsByConnection[targetId] = [{ port: 8001 }]
|
||||
}
|
||||
if (random(2)) {
|
||||
state.sshCredentialQueue.push({ targetId, requestId: targetId, kind: 'password' })
|
||||
}
|
||||
}
|
||||
const count = random(24)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const key = ['__proto__', 'constructor', 'toString'][i] ?? `folder:${i}`
|
||||
const owner = targets[random(targets.length)]
|
||||
const rows = Array.from({ length: random(5) }, (_, j) => {
|
||||
const id = `tab-${i}-${j}`
|
||||
const ptyId = [null, '', 'local-pty', 'ssh:bad', toAppSshPtyId(owner, `pty-${j}`)][random(5)]
|
||||
if (random(3) === 0) {
|
||||
state.ptyIdsByTabId[id] = [toAppSshPtyId(owner, 'split'), 'local-split']
|
||||
}
|
||||
if (random(3) === 0) {
|
||||
state.lastKnownRelayPtyIdByTabId[id] = toAppSshPtyId(owner, 'last')
|
||||
}
|
||||
if (random(2)) {
|
||||
state.deferredSshSessionIdsByTabId[id] = ptyId ?? 'local'
|
||||
}
|
||||
if (random(2)) {
|
||||
state.pendingReconnectPtyIdByTabId[id] = toAppSshPtyId(owner, 'reconnect')
|
||||
}
|
||||
if (ptyId) {
|
||||
state.pendingCodexPaneRestartIds[ptyId] = true
|
||||
state.codexRestartNoticeByPtyId[ptyId] = {
|
||||
previousAccountLabel: 'old',
|
||||
nextAccountLabel: 'new'
|
||||
}
|
||||
}
|
||||
const authority = {
|
||||
targetId: targets[random(3)],
|
||||
providerEpoch: 'epoch',
|
||||
connectionGeneration: 1
|
||||
}
|
||||
if (random(2)) {
|
||||
state.directSshPaneRetryByTabId[id] = { authority, attemptId: id, tabGeneration: 1 }
|
||||
}
|
||||
if (random(2)) {
|
||||
state.directSshLivePtyBindingByTabId[id] = { authority, ptyId, tabGeneration: 1 }
|
||||
}
|
||||
if (random(2)) {
|
||||
state.directSshPaneRetryHistoryByTabId[id] = { authority, attemptedAt: [10] }
|
||||
}
|
||||
return tab(id, key, ptyId)
|
||||
})
|
||||
Object.defineProperty(state.tabsByWorktree, key, { value: rows, enumerable: true })
|
||||
if (i >= 3 && random(2)) {
|
||||
const worktree = {
|
||||
id: key,
|
||||
repoId: 'collision',
|
||||
path: `/remote/${i}`,
|
||||
hostId: `ssh:${encodeURIComponent(owner)}`
|
||||
}
|
||||
;(state.worktreesByRepo.collision ??= []).push(worktree)
|
||||
if (random(2)) {
|
||||
state.detectedWorktreesByRepo.collision = { worktrees: [worktree] }
|
||||
}
|
||||
}
|
||||
}
|
||||
compare(freezeState(state), targets[random(3)])
|
||||
}
|
||||
console.log('3,000 frozen-state full-patch / identity differential cases passed')
|
||||
|
||||
// Instrument only the copy site for counts; timing arms above remain uninstrumented.
|
||||
const counted = await Promise.all(sources.map((source) => load(source, true)))
|
||||
for (const stride of [1, 10, 0]) {
|
||||
const state = timedState(100, stride, false)
|
||||
const expectedCopies = stride === 0 ? [0, 0] : [100 / stride, 1]
|
||||
counted.forEach((module, index) => {
|
||||
module.tabMapCopies.count = 0
|
||||
module.tabMapCopies.entries = 0
|
||||
assert.deepEqual(
|
||||
module.buildRemovedSshTargetCleanupPatch(state, 'removed'),
|
||||
arms[index](state, 'removed')
|
||||
)
|
||||
assert.deepEqual(module.tabMapCopies, {
|
||||
count: expectedCopies[index],
|
||||
entries: expectedCopies[index] * 100
|
||||
})
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({ stride, copiedEntries: counted.map((module) => module.tabMapCopies.entries) })
|
||||
)
|
||||
}
|
||||
|
||||
function sample(arm, state, repeats) {
|
||||
const start = performance.now()
|
||||
let changed = 0
|
||||
for (let i = 0; i < repeats; i++) {
|
||||
changed += arm(state, 'removed') !== null ? 1 : 0
|
||||
}
|
||||
assert(changed === 0 || changed === repeats)
|
||||
return (performance.now() - start) / repeats
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
unit: 'ms',
|
||||
pairs: 8
|
||||
})
|
||||
)
|
||||
for (const [count, stride, catalog, tabsPerWorkspace] of [
|
||||
[1, 1, false, 1],
|
||||
[10, 1, false, 1],
|
||||
[100, 1, false, 1],
|
||||
[500, 1, false, 1],
|
||||
[1000, 1, false, 1],
|
||||
[100, 10, false, 1],
|
||||
[1000, 10, false, 1],
|
||||
[1000, 0, false, 1],
|
||||
[100, 1, true, 4],
|
||||
[500, 1, true, 4]
|
||||
]) {
|
||||
const state = timedState(count, stride, catalog, tabsPerWorkspace)
|
||||
compare(state, 'removed')
|
||||
for (const arm of arms) {
|
||||
const until = performance.now() + 80
|
||||
while (performance.now() < until) {
|
||||
sample(arm, state, 1)
|
||||
}
|
||||
}
|
||||
const repeats = Math.max(1, Math.min(20000, Math.ceil(40 / sample(arms[0], state, 1))))
|
||||
/** @type {number[][]} */
|
||||
const samples = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
samples[index].push(sample(arms[index], state, repeats))
|
||||
}
|
||||
}
|
||||
const median = samples.map((values) => {
|
||||
values.sort((a, b) => a - b)
|
||||
return (values[3] + values[4]) / 2
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({ count, stride, catalog, tabsPerWorkspace, repeats, median, samples })
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env node
|
||||
// git show <base>:src/main/ipc/ssh-pty-source-obligation-state.ts | node config/scripts/ssh-source-frontier-benchmark.mjs
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const statePath = 'src/main/ipc/ssh-pty-source-obligation-state.ts'
|
||||
const baselineSource = readFileSync(0, 'utf8')
|
||||
assert(baselineSource.includes('export function advanceSourceTerminalEnd'))
|
||||
|
||||
async function loadLedger(source) {
|
||||
const result = await build({
|
||||
entryPoints: ['src/main/ipc/ssh-pty-source-obligation-ledger.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'source-obligation-state',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /ssh-pty-source-obligation-state\.ts$/ }, (args) => ({
|
||||
contents: source,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(args.path)
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const encoded = Buffer.from(result.outputFiles[0].text).toString('base64')
|
||||
return (await import(`data:text/javascript;base64,${encoded}`)).SshPtySourceObligationLedger
|
||||
}
|
||||
|
||||
const [Baseline, Current] = await Promise.all([
|
||||
loadLedger(baselineSource),
|
||||
loadLedger(readFileSync(statePath, 'utf8'))
|
||||
])
|
||||
|
||||
function identity(index = 0) {
|
||||
return Object.freeze({
|
||||
id: `pty-${index}`,
|
||||
providerGeneration: index + 1,
|
||||
clientGeneration: 2,
|
||||
ownerGeneration: 3,
|
||||
ptyIncarnation: `incarnation-${index}`,
|
||||
deliveryToken: `token-${index}`
|
||||
})
|
||||
}
|
||||
|
||||
function sourceSpan(owner, id, start, length) {
|
||||
return Object.freeze({
|
||||
...owner,
|
||||
spanId: id,
|
||||
sourceStartSu: start,
|
||||
sourceEndSu: start + length,
|
||||
displayStart: start,
|
||||
displayEnd: start + length,
|
||||
data: 'x'.repeat(length),
|
||||
splittable: true,
|
||||
transform: Object.freeze({ transformed: false, rawLengthSu: length, scalarSafe: true })
|
||||
})
|
||||
}
|
||||
|
||||
function trace(Ledger, seed) {
|
||||
let randomState = seed
|
||||
const random = (bound) => {
|
||||
randomState = (Math.imul(randomState, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((randomState / 2 ** 32) * bound)
|
||||
}
|
||||
const closed = []
|
||||
const ledger = new Ledger((owner) => closed.push(owner))
|
||||
const owners = [identity(), identity(1)]
|
||||
const starts = [seed % 3 === 0 ? Number.MAX_SAFE_INTEGER - 10_000 : 0, 100]
|
||||
const reservations = []
|
||||
const publications = []
|
||||
const spans = []
|
||||
const consumers = ['model', 'desktop', 'remote:viewer']
|
||||
owners.forEach((owner, index) => ledger.open(owner, starts[index]))
|
||||
const results = []
|
||||
const rememberAck = (publication) => {
|
||||
if (publication) {
|
||||
publications.push(publication)
|
||||
}
|
||||
return publication?.ack ?? null
|
||||
}
|
||||
for (let step = 0; step < 120; step += 1) {
|
||||
const owner = owners[random(owners.length)]
|
||||
const selectedSpan = spans[random(spans.length)]
|
||||
const spanId = selectedSpan?.spanId ?? 'missing'
|
||||
const consumer = consumers[random(consumers.length)]
|
||||
const operation = step < 20 ? random(2) : random(14)
|
||||
try {
|
||||
let value
|
||||
switch (operation) {
|
||||
case 0:
|
||||
case 1: {
|
||||
const start = ledger.snapshot(owner).receivedEndSu
|
||||
const span = sourceSpan(owner, `span-${step}`, start, random(5))
|
||||
spans.push(span)
|
||||
const reservation = ledger.reserve(owner, span, consumers)
|
||||
reservations.push(reservation)
|
||||
value = random(8) === 0 ? ledger.rollback(reservation) : ledger.commit(reservation)
|
||||
break
|
||||
}
|
||||
case 2:
|
||||
case 3:
|
||||
value = ledger.settle(spanId, consumer, 'accepted')
|
||||
break
|
||||
case 4:
|
||||
value = ledger.beginTransfer(spanId, consumer, 'model', 'hidden')
|
||||
break
|
||||
case 5:
|
||||
value = ledger.commitTransfer(spanId, consumer)
|
||||
break
|
||||
case 6:
|
||||
value = ledger.cancelTransfer(spanId, consumer, 'canceled')
|
||||
break
|
||||
case 7:
|
||||
value = ledger.rollbackTransfer(spanId, consumer)
|
||||
break
|
||||
case 8:
|
||||
value = rememberAck(ledger.queueAck(owner))
|
||||
break
|
||||
case 9:
|
||||
value = rememberAck(ledger.retryQueuedAck(owner))
|
||||
break
|
||||
case 10: {
|
||||
const publication = publications[random(publications.length)]
|
||||
const result = random(4) ? { ok: true } : { ok: false, error: new Error('write failed') }
|
||||
value = publication?.onSettled(result)
|
||||
break
|
||||
}
|
||||
case 11: {
|
||||
const reservation = reservations[random(reservations.length)]
|
||||
value = reservation ? ledger.rollbackCommitted(reservation) : false
|
||||
break
|
||||
}
|
||||
case 12:
|
||||
value = ledger.modelAcceptedEnd(owner)
|
||||
break
|
||||
case 13:
|
||||
value = step < 100 ? ledger.spanIdentity(spanId) : ledger.seal(owner)
|
||||
break
|
||||
}
|
||||
results.push({ operation, value })
|
||||
} catch (error) {
|
||||
results.push({ operation, error: [error.name, error.message] })
|
||||
}
|
||||
results.push(owners.map((entry) => ledger.snapshot(entry)))
|
||||
}
|
||||
for (const span of spans) {
|
||||
if (ledger.hasRetainedSpan(span.spanId)) {
|
||||
results.push(consumers.map((consumer) => ledger.obligation(span.spanId, consumer)))
|
||||
for (const consumer of consumers) {
|
||||
const obligation = ledger.obligation(span.spanId, consumer)
|
||||
if (obligation.state === 'open') {
|
||||
ledger.settle(span.spanId, consumer, 'drained')
|
||||
} else if (obligation.state === 'transferring') {
|
||||
ledger.commitTransfer(span.spanId, consumer)
|
||||
}
|
||||
}
|
||||
results.push(owners.map((owner) => ledger.snapshot(owner)))
|
||||
rememberAck(ledger.queueAck(span))
|
||||
if (random(3) === 0) {
|
||||
publications[random(publications.length)]?.onSettled({ ok: true })
|
||||
}
|
||||
} else {
|
||||
results.push(null)
|
||||
}
|
||||
}
|
||||
results.push(ledger.closeGeneration(1, 'connection closed'))
|
||||
for (const publication of publications) {
|
||||
publication.onSettled({ ok: true })
|
||||
}
|
||||
results.push(
|
||||
ledger.closeAll('disposed'),
|
||||
closed,
|
||||
owners.map((owner) => ledger.snapshot(owner))
|
||||
)
|
||||
return results
|
||||
}
|
||||
|
||||
for (let seed = 1; seed <= 2_000; seed += 1) {
|
||||
assert.deepEqual(trace(Current, seed), trace(Baseline, seed), `seed ${seed}`)
|
||||
}
|
||||
console.log('2,000 differential traces / 240,000 commands passed (plus snapshots and cleanup).')
|
||||
|
||||
function runBatch(Ledger, spans, mode) {
|
||||
const ledger = new Ledger()
|
||||
const owner = spans[0]
|
||||
ledger.open(owner)
|
||||
const settle = (span) => {
|
||||
ledger.settle(span.spanId, 'model', 'accepted')
|
||||
ledger.settle(span.spanId, 'desktop', 'parsed')
|
||||
}
|
||||
for (const span of spans) {
|
||||
ledger.commit(ledger.reserve(owner, span, ['model', 'desktop']))
|
||||
if (mode !== 'backlog') {
|
||||
settle(span)
|
||||
if (mode === 'immediate') {
|
||||
ledger.queueAck(owner)?.onSettled({ ok: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode === 'backlog') {
|
||||
for (const span of spans) {
|
||||
settle(span)
|
||||
}
|
||||
}
|
||||
ledger.queueAck(owner)?.onSettled({ ok: true })
|
||||
return ledger.snapshot(owner)
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
const ordered = values.toSorted((left, right) => left - right)
|
||||
return (ordered[ordered.length / 2 - 1] + ordered[ordered.length / 2]) / 2
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch })
|
||||
)
|
||||
const rows = []
|
||||
for (const mode of ['immediate', 'delayed', 'backlog']) {
|
||||
for (const count of [1, 16, 64, 256, 1_024]) {
|
||||
const spans = Array.from({ length: count }, (_, index) =>
|
||||
sourceSpan(identity(), `span-${index}`, index * 128, 128)
|
||||
)
|
||||
const expected = runBatch(Baseline, spans, mode)
|
||||
assert.deepEqual(runBatch(Current, spans, mode), expected)
|
||||
const iterations = Math.max(20, Math.floor(8_192 / count))
|
||||
const measure = (Ledger) => {
|
||||
let result
|
||||
const start = performance.now()
|
||||
for (let index = 0; index < iterations; index += 1) {
|
||||
result = runBatch(Ledger, spans, mode)
|
||||
}
|
||||
const elapsed = (performance.now() - start) / iterations
|
||||
assert.deepEqual(result, expected)
|
||||
return elapsed
|
||||
}
|
||||
measure(Baseline)
|
||||
measure(Current)
|
||||
const samples = { baseline: [], current: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'current')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(measure(arm === 'baseline' ? Baseline : Current))
|
||||
}
|
||||
}
|
||||
rows.push({
|
||||
mode,
|
||||
spans: count,
|
||||
beforeMs: median(samples.baseline),
|
||||
afterMs: median(samples.current)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.table(rows)
|
||||
console.log('Synthetic ledger CPU only; no network, renderer, or end-to-end latency measurement.')
|
||||
@@ -0,0 +1,271 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import ts from 'typescript-api'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
// Pipe the baseline capture module on stdin. No Electron or visible terminal is launched.
|
||||
const renderer = path.resolve('src/renderer/src')
|
||||
const entry = path.join(renderer, 'components/terminal-pane/terminal-shutdown-layout-capture.ts')
|
||||
const sources = [readFileSync(0, 'utf8'), readFileSync(entry, 'utf8')]
|
||||
assert(sources.every((source) => source.includes('export function captureTerminalShutdownLayout')))
|
||||
const layoutFile = path.join(renderer, 'components/terminal-pane/layout-serialization.ts')
|
||||
const layoutSource = readFileSync(layoutFile, 'utf8')
|
||||
const layoutAst = ts.createSourceFile(layoutFile, layoutSource, ts.ScriptTarget.Latest, true)
|
||||
const layoutNames = ['getLayoutChildNodes', 'serializePaneTree', 'serializeTerminalLayout']
|
||||
const layoutFunctions = layoutAst.statements.filter(
|
||||
(node) => ts.isFunctionDeclaration(node) && layoutNames.includes(node.name?.text)
|
||||
)
|
||||
assert.equal(layoutFunctions.length, layoutNames.length)
|
||||
const layoutSubset = `import { isTerminalLeafId } from '../../../../shared/stable-pane-id';
|
||||
${layoutFunctions.map((node) => node.getText(layoutAst)).join('\n')}`
|
||||
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
alias: { '@': renderer },
|
||||
plugins: [
|
||||
{
|
||||
name: 'capture-benchmark',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /terminal-shutdown-layout-capture\.ts$/ }, () => ({
|
||||
contents: `${source}\nexport { fitsSessionScrollbackByteLimit, MAX_BUFFER_BYTES };`,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(entry)
|
||||
}))
|
||||
// Select unchanged layout declarations to exclude unrelated renderer module side effects.
|
||||
builder.onLoad({ filter: /[/\\]layout-serialization\.ts$/ }, () => ({
|
||||
contents: layoutSubset,
|
||||
loader: 'ts',
|
||||
resolveDir: path.dirname(layoutFile)
|
||||
}))
|
||||
builder.onResolve({ filter: /pane-terminal-output-scheduler$/ }, () => ({
|
||||
path: 'capture-scheduler',
|
||||
namespace: 'fixture'
|
||||
}))
|
||||
builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({
|
||||
contents: 'export function flushTerminalOutput(terminal) { terminal.recordFlush?.(); }',
|
||||
loader: 'js'
|
||||
}))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundled = `${result.outputFiles[0].text}\n//# sourceURL=shutdown-byte-limit-benchmark-bundle.js`
|
||||
return import(`data:text/javascript;base64,${Buffer.from(bundled).toString('base64')}`)
|
||||
}
|
||||
const modules = await Promise.all(sources.map(load))
|
||||
const captures = modules.map((module) => module.captureTerminalShutdownLayout)
|
||||
const predicates = modules.map((module) => module.fitsSessionScrollbackByteLimit)
|
||||
|
||||
class FixtureElement {
|
||||
constructor({ classes = [], dataset = {}, children = [], firstElementChild = null } = {}) {
|
||||
this.classList = { contains: (name) => classes.includes(name) }
|
||||
this.dataset = dataset
|
||||
this.children = children
|
||||
this.firstElementChild = firstElementChild
|
||||
this.style = { flex: '1' }
|
||||
}
|
||||
}
|
||||
globalThis.HTMLElement = FixtureElement
|
||||
|
||||
function leafId(index) {
|
||||
return `${String(index + 1).padStart(8, '0')}-1111-4111-8111-111111111111`
|
||||
}
|
||||
|
||||
function captureArgs(panes, captureBuffers = true, cleared = false) {
|
||||
const leaves = panes.map(
|
||||
(pane) =>
|
||||
new FixtureElement({
|
||||
classes: ['pane'],
|
||||
dataset: { leafId: pane.leafId }
|
||||
})
|
||||
)
|
||||
let root = leaves[0] ?? null
|
||||
for (const leaf of leaves.slice(1)) {
|
||||
root = new FixtureElement({ classes: ['pane-split', 'is-horizontal'], children: [root, leaf] })
|
||||
}
|
||||
const prior = Object.fromEntries(panes.map((pane) => [pane.leafId, 'prior-buffer']))
|
||||
return {
|
||||
manager: { getPanes: () => panes, getActivePane: () => panes[0] ?? null },
|
||||
container: new FixtureElement({ firstElementChild: root }),
|
||||
expandedPaneId: panes[1]?.id ?? null,
|
||||
paneTransports: new Map(
|
||||
panes.map((pane, index) => [
|
||||
pane.id,
|
||||
{
|
||||
getPtyId: () => (index % 2 ? null : `ssh:target@@pty-${index}`)
|
||||
}
|
||||
])
|
||||
),
|
||||
paneTitlesByPaneId: Object.fromEntries(panes.map((pane) => [pane.id, `title-${pane.id}`])),
|
||||
existingLayout: Object.freeze({
|
||||
root: null,
|
||||
activeLeafId: null,
|
||||
expandedLeafId: null,
|
||||
buffersByLeafId: Object.freeze(prior),
|
||||
scrollbackRefsByLeafId: Object.freeze({ [leafId(0)]: 'v1-prior' }),
|
||||
ptyIdsByLeafId: Object.freeze({ [leafId(1)]: 'local-prior' })
|
||||
}),
|
||||
captureBuffers,
|
||||
clearedScrollbackLeafIds: new Set(cleared ? [leafId(0)] : [])
|
||||
}
|
||||
}
|
||||
|
||||
function syntheticFixture(config) {
|
||||
const events = []
|
||||
const panes = config.units.map((unit, index) =>
|
||||
Object.freeze({
|
||||
id: index + 1,
|
||||
leafId: leafId(index),
|
||||
terminal: Object.freeze({
|
||||
options: { scrollback: config.rows },
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
buffer: { active: { cursorX: index, cursorY: index } },
|
||||
recordFlush: () => events.push(['flush', index])
|
||||
}),
|
||||
serializeAddon: {
|
||||
serialize(options) {
|
||||
const rows = options?.scrollback ?? 0
|
||||
events.push(['serialize', index, rows])
|
||||
if (config.fail && index === 0) {
|
||||
throw new Error('fixture serializer failure')
|
||||
}
|
||||
return unit.repeat(Math.max(0, rows))
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
return { args: captureArgs(panes, config.capture, config.cleared), events }
|
||||
}
|
||||
|
||||
let seed = 0x17c0ffee
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return (seed >>> 8) % max
|
||||
}
|
||||
const limit = modules[0].MAX_BUFFER_BYTES
|
||||
assert.equal(modules[1].MAX_BUFFER_BYTES, limit)
|
||||
const units = ['x', 'é', '界', '😀', '\ud83d', '\udc00', '\x1b[31mtext\x1b[0m\r\n', '']
|
||||
let comparisons = 0
|
||||
function compareFixture(config) {
|
||||
const results = captures.map((capture) => {
|
||||
const fixture = syntheticFixture(config)
|
||||
const layout = capture(fixture.args)
|
||||
return { layout, events: fixture.events }
|
||||
})
|
||||
assert.deepEqual(results[1], results[0])
|
||||
comparisons++
|
||||
}
|
||||
for (const unit of units.filter(Boolean)) {
|
||||
for (let delta = -2; delta <= 2; delta++) {
|
||||
const rows = Math.floor((limit - 7) / Buffer.byteLength(unit)) + delta
|
||||
compareFixture({ units: [unit], rows, capture: true, cleared: false, fail: false })
|
||||
}
|
||||
}
|
||||
for (let iteration = 0; iteration < 1500; iteration++) {
|
||||
compareFixture({
|
||||
units: Array.from({ length: random(4) }, () => units[random(units.length)].repeat(random(50))),
|
||||
rows: random(2000),
|
||||
capture: random(5) !== 0,
|
||||
cleared: random(3) === 0,
|
||||
fail: random(9) === 0
|
||||
})
|
||||
}
|
||||
for (const unit of units) {
|
||||
for (const length of [0, 1, 64, limit - 1, limit, limit + 1]) {
|
||||
const input = unit.repeat(length)
|
||||
assert.equal(predicates[1](input), predicates[0](input))
|
||||
}
|
||||
}
|
||||
console.log(`${comparisons} full capture layouts/event traces and 48 byte-fit cases match`)
|
||||
|
||||
function sample(arm, input, repeats) {
|
||||
const start = performance.now()
|
||||
let output
|
||||
for (let i = 0; i < repeats; i++) {
|
||||
output = arm(input)
|
||||
}
|
||||
return { elapsed: (performance.now() - start) / repeats, output }
|
||||
}
|
||||
|
||||
function benchmark(name, input, arms, minimumRepeats = 1) {
|
||||
const expected = arms[0](input)
|
||||
assert.deepEqual(arms[1](input), expected)
|
||||
for (const arm of arms) {
|
||||
const until = performance.now() + 250
|
||||
while (performance.now() < until) {
|
||||
sample(arm, input, 1)
|
||||
}
|
||||
}
|
||||
const repeats = Math.max(
|
||||
minimumRepeats,
|
||||
Math.min(100000, Math.ceil(50 / sample(arms[0], input, 1).elapsed))
|
||||
)
|
||||
/** @type {number[][]} */
|
||||
const times = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
const result = sample(arms[index], input, repeats)
|
||||
times[index].push(result.elapsed)
|
||||
assert.deepEqual(result.output, expected)
|
||||
}
|
||||
}
|
||||
const median = times.map((values) => {
|
||||
values.sort((a, b) => a - b)
|
||||
return (values[3] + values[4]) / 2
|
||||
})
|
||||
console.log(JSON.stringify({ name, repeats, median, times }))
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
pairs: 8,
|
||||
unit: 'ms'
|
||||
})
|
||||
)
|
||||
for (const size of [64, 32768, limit, 2 * 1024 * 1024]) {
|
||||
benchmark(`fit ASCII ${size} code units`, 'x'.repeat(size), predicates)
|
||||
benchmark(`fit Unicode ${size} code units`, '界'.repeat(size), predicates)
|
||||
}
|
||||
|
||||
const xterm = await import('@xterm/headless')
|
||||
const addon = await import('@xterm/addon-serialize')
|
||||
const Terminal = xterm.Terminal ?? xterm.default.Terminal
|
||||
const SerializeAddon = addon.SerializeAddon ?? addon.default.SerializeAddon
|
||||
for (const [rows, unicode, paneCount] of [
|
||||
[50, false, 1],
|
||||
[500, false, 1],
|
||||
[5000, false, 1],
|
||||
[5000, true, 1],
|
||||
[500, false, 8]
|
||||
]) {
|
||||
const panes = []
|
||||
for (let index = 0; index < paneCount; index++) {
|
||||
const terminal = new Terminal({ cols: 120, rows: 40, scrollback: 5000, allowProposedApi: true })
|
||||
const serializeAddon = new SerializeAddon()
|
||||
terminal.loadAddon(serializeAddon)
|
||||
const row = unicode ? '界é😀'.repeat(15) : 'line x'.repeat(18)
|
||||
await new Promise((resolve) => terminal.write(`${row}\r\n`.repeat(rows), resolve))
|
||||
panes.push({ id: index + 1, leafId: leafId(index), terminal, serializeAddon })
|
||||
}
|
||||
try {
|
||||
benchmark(
|
||||
`full capture ${paneCount} panes / ${rows} ${unicode ? 'Unicode' : 'ASCII'} lines`,
|
||||
captureArgs(panes),
|
||||
captures,
|
||||
4
|
||||
)
|
||||
} finally {
|
||||
panes.forEach((pane) => pane.terminal.dispose())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import fs from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { build } from 'esbuild'
|
||||
const file = 'src/shared/native-chat-tool-summary.ts'
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const beforeSource = execFileSync('git', ['show', `${baseline}:${file}`], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true
|
||||
})
|
||||
const afterSource = fs.readFileSync(file, 'utf8')
|
||||
async function load(contents) {
|
||||
const { outputFiles } = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(beforeSource),
|
||||
after = await load(afterSource)
|
||||
const display = (m, input) => {
|
||||
const d = m.createToolInputDisplay(input)
|
||||
return { ...d, formatDetail: d.formatDetail() }
|
||||
}
|
||||
let seed = 8121
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 4294967296) * max)
|
||||
}
|
||||
const pieces = [
|
||||
'a',
|
||||
'b',
|
||||
'…',
|
||||
'😀',
|
||||
'\ud800',
|
||||
'\udc00',
|
||||
'\0',
|
||||
'\t',
|
||||
'\r',
|
||||
'\n',
|
||||
' ',
|
||||
'\v',
|
||||
'\f',
|
||||
'\u00a0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u200a',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202f',
|
||||
'\u205f',
|
||||
'\u3000',
|
||||
'\ufeff',
|
||||
'\u0085',
|
||||
'\u200b'
|
||||
]
|
||||
for (let i = 0; i < 3000; i++) {
|
||||
const input = Array.from({ length: random(500) }, () => pieces[random(pieces.length)]).join('')
|
||||
assert.equal(after.summarizeToolInput(input), before.summarizeToolInput(input))
|
||||
assert.deepEqual(display(after, input), display(before, input))
|
||||
}
|
||||
for (const input of [
|
||||
`${'a'.repeat(79)}…`,
|
||||
'a'.repeat(80) + ' '.repeat(500),
|
||||
' '.repeat(100000),
|
||||
`${'\n'.repeat(100000)}x`,
|
||||
{ command: 'a '.repeat(50000) },
|
||||
{ file_path: 'a '.repeat(500) },
|
||||
{ x: 'a '.repeat(500) },
|
||||
JSON.stringify({ command: 'a '.repeat(50000) })
|
||||
]) {
|
||||
assert.deepEqual(display(after, input), display(before, input))
|
||||
}
|
||||
for (const [shape, input] of [
|
||||
['tiny', 'ls -la'],
|
||||
['100KB', 'a b\n\t'.repeat(15000)],
|
||||
['1MB', 'a b\n\t'.repeat(150000)],
|
||||
['all-space', ' '.repeat(1000000)],
|
||||
['leading', `${' '.repeat(1000000)}x`],
|
||||
['trailing', `x${' '.repeat(1000000)}`],
|
||||
['long-word', 'x'.repeat(1000000)]
|
||||
]) {
|
||||
const samples = { before: [], after: [] }
|
||||
for (let i = 0; i < 20; i++) {
|
||||
before.createToolInputDisplay(input)
|
||||
after.createToolInputDisplay(input)
|
||||
}
|
||||
for (let r = 0; r < 8; r++) {
|
||||
for (const [label, m] of r % 2
|
||||
? [
|
||||
['after', after],
|
||||
['before', before]
|
||||
]
|
||||
: [
|
||||
['before', before],
|
||||
['after', after]
|
||||
]) {
|
||||
global.gc()
|
||||
const start = process.cpuUsage()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
m.createToolInputDisplay(input)
|
||||
}
|
||||
const cpu = process.cpuUsage(start)
|
||||
samples[label].push((cpu.user + cpu.system) / 20000)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ shape, samples }))
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
|
||||
const modulePath = 'src/main/native-chat/transcript-stream-lines.ts'
|
||||
const baselineSource = readFileSync(0, 'utf8')
|
||||
assert.ok(baselineSource.includes('decodeTranscriptStream'), 'Pipe the baseline module on stdin')
|
||||
const arms = {}
|
||||
for (const [name, source] of [
|
||||
['baseline', baselineSource],
|
||||
['fragmented', readFileSync(modulePath, 'utf8')]
|
||||
]) {
|
||||
const output = await build({
|
||||
stdin: {
|
||||
contents: `${source}\nexport { decodeClaudeTranscriptLine } from './transcript-line-decoders'`,
|
||||
loader: 'ts',
|
||||
resolveDir: dirname(resolve(modulePath))
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
arms[name] = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(output.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
|
||||
const path = '/fixture/transcript.jsonl'
|
||||
const decode = (line, id) => ({ id, line })
|
||||
let seed = 90211
|
||||
function random(max) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 0x100000000) * max)
|
||||
}
|
||||
|
||||
async function observe(arm, chunks, start, includeTrailing, throwAt) {
|
||||
const calls = []
|
||||
const stream = Readable.from(chunks)
|
||||
try {
|
||||
const result = await arm.decodeTranscriptStream(
|
||||
stream,
|
||||
path,
|
||||
start,
|
||||
(line, id) => {
|
||||
calls.push({ line, id })
|
||||
if (calls.length === throwAt) {
|
||||
throw new Error('fixture decode error')
|
||||
}
|
||||
return line.startsWith('skip') ? null : decode(line, id)
|
||||
},
|
||||
includeTrailing
|
||||
)
|
||||
return { calls, result, destroyed: stream.destroyed }
|
||||
} catch (error) {
|
||||
return { calls, error: error.message, destroyed: stream.destroyed }
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = ['a', '\r', '\n', '😀', 'é中', '\ud83d', '\ude00', 'skip', '\x00', '\u2028']
|
||||
for (let trial = 0; trial < 5000; trial++) {
|
||||
let chunks
|
||||
if (trial % 2) {
|
||||
const raw = Buffer.from(Array.from({ length: random(100) }, () => random(256)))
|
||||
chunks = []
|
||||
for (let offset = 0; offset < raw.length;) {
|
||||
const end = Math.min(raw.length, offset + 1 + random(8))
|
||||
chunks.push(raw.subarray(offset, end))
|
||||
offset = end
|
||||
}
|
||||
} else {
|
||||
chunks = Array.from({ length: random(30) }, () => {
|
||||
const text = Array.from({ length: random(15) }, () => tokens[random(tokens.length)]).join('')
|
||||
return random(3) ? text : Buffer.from(text)
|
||||
})
|
||||
}
|
||||
const start = [0, 123, -1, 0.5, Number.MAX_SAFE_INTEGER - 3, Infinity, Number.NaN][random(7)]
|
||||
const includeTrailing = Boolean(random(2))
|
||||
const throwAt = random(7) === 0 ? 1 + random(4) : Infinity
|
||||
const expected = await observe(arms.baseline, chunks, start, includeTrailing, throwAt)
|
||||
const actual = await observe(arms.fragmented, chunks, start, includeTrailing, throwAt)
|
||||
assert.deepEqual(actual, expected)
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
differentialCases: 5000,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch
|
||||
})
|
||||
)
|
||||
|
||||
function splitInput(input, chunkSize, kind) {
|
||||
const raw = kind === 'buffer' ? Buffer.from(input) : input
|
||||
const chunks = []
|
||||
for (let offset = 0; offset < raw.length; offset += chunkSize) {
|
||||
chunks.push(raw.slice(offset, offset + chunkSize))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
const workloads = []
|
||||
for (const [size, chunkSize, kind] of [
|
||||
[4096, 65536, 'string'],
|
||||
[2 * 1024 * 1024, 65536, 'string'],
|
||||
[8 * 1024 * 1024, 65536, 'string'],
|
||||
[2 * 1024 * 1024, 4096, 'buffer'],
|
||||
[8 * 1024 * 1024, 1024 * 1024, 'buffer']
|
||||
]) {
|
||||
const input = `${'x'.repeat(size)}\n`
|
||||
workloads.push({
|
||||
name: `${size}B record/${chunkSize}B ${kind}`,
|
||||
chunks: splitInput(input, chunkSize, kind),
|
||||
includeTrailing: true
|
||||
})
|
||||
}
|
||||
workloads.push({
|
||||
name: '10000 short records/64KiB strings',
|
||||
chunks: splitInput('ordinary record\r\n'.repeat(10000), 65536, 'string'),
|
||||
includeTrailing: true
|
||||
})
|
||||
for (const includeTrailing of [false, true]) {
|
||||
workloads.push({
|
||||
name: `2MiB partial trailing/include=${includeTrailing}`,
|
||||
chunks: splitInput(`complete\n${'x'.repeat(2 * 1024 * 1024)}`, 65536, 'string'),
|
||||
includeTrailing
|
||||
})
|
||||
}
|
||||
for (const size of [4096, 2 * 1024 * 1024, 8 * 1024 * 1024]) {
|
||||
const input = `${JSON.stringify({
|
||||
type: 'user',
|
||||
uuid: 'message-1',
|
||||
timestamp: '2026-09-11T10:00:00Z',
|
||||
message: { role: 'user', content: 'x'.repeat(size) }
|
||||
})}\n`
|
||||
workloads.push({
|
||||
name: `${size}B Claude record with real decoder`,
|
||||
chunks: splitInput(input, 65536, 'string'),
|
||||
includeTrailing: true,
|
||||
claude: true
|
||||
})
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
return (sorted[3] + sorted[4]) / 2
|
||||
}
|
||||
for (const workload of workloads) {
|
||||
const run = (arm) =>
|
||||
arms[arm].decodeTranscriptStream(
|
||||
Readable.from(workload.chunks),
|
||||
path,
|
||||
0,
|
||||
workload.claude ? arms[arm].decodeClaudeTranscriptLine : decode,
|
||||
workload.includeTrailing
|
||||
)
|
||||
const expected = await run('baseline')
|
||||
assert.deepEqual(await run('fragmented'), expected)
|
||||
if (workload.claude) {
|
||||
assert.equal(expected.messages.length, 1)
|
||||
}
|
||||
const samples = { baseline: [], fragmented: [] }
|
||||
const repeats = workload.chunks.length === 1 ? 100 : 5
|
||||
for (const arm of Object.keys(arms)) {
|
||||
for (let warmup = 0; warmup < 5; warmup++) {
|
||||
await run(arm)
|
||||
}
|
||||
}
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'fragmented')) {
|
||||
for (const arm of pair) {
|
||||
const start = performance.now()
|
||||
let actual
|
||||
for (let repeat = 0; repeat < repeats; repeat++) {
|
||||
actual = await run(arm)
|
||||
}
|
||||
samples[arm].push((performance.now() - start) / repeats)
|
||||
assert.deepEqual(actual, expected)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name: workload.name,
|
||||
medianMs: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [arm, median(values)])
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -20,8 +20,8 @@ const TILES = [
|
||||
{
|
||||
id: 'tile-01',
|
||||
sourceRoot: ROOT,
|
||||
gifRelativePath: 'docs/assets/feature-wall/parallel-worktrees.gif',
|
||||
posterRelativePath: 'docs/assets/feature-wall/parallel-worktrees.jpg'
|
||||
gifRelativePath: 'docs/site/public/docs/tab-split.gif',
|
||||
posterRelativePath: 'docs/site/public/docs/posters/tab-split.jpg'
|
||||
},
|
||||
{
|
||||
id: 'tile-02',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -15,13 +18,74 @@ const REF_MIRRORS = [
|
||||
]
|
||||
|
||||
describe('ref-mirroring vet steps', () => {
|
||||
it('keeps the full-history adhoc checkout on the same case-safe backend', () => {
|
||||
it.each(['daily', 'hourly', 'adhoc'])('%s builds only need the current commit', (channel) => {
|
||||
const job = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[
|
||||
`build-${channel}-mac`
|
||||
]
|
||||
const checkout = job.steps.find((step) => step.uses === 'actions/checkout@v6')
|
||||
expect(checkout.with['fetch-depth']).toBe(1)
|
||||
expect(job.steps.some((step) => step.run?.includes('gh release list'))).toBe(true)
|
||||
expect(
|
||||
job.steps.some((step) => step.run?.includes('ORCA_PUBLISHED_VERSIONS="$published"'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('retains release-cut history for version reservation and retry ancestry', () => {
|
||||
const checkout = readWorkflow('.github/workflows/release-cut.yml').jobs.cut.steps.find(
|
||||
(step) => step.uses === 'actions/checkout@v6'
|
||||
)
|
||||
expect(checkout.with['fetch-depth']).toBe(0)
|
||||
})
|
||||
|
||||
it('resolves identical dev identities in full and depth-one checkouts without local tags', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-checkout-identity-'))
|
||||
const source = join(directory, 'source')
|
||||
const shallow = join(directory, 'shallow')
|
||||
const run = (program, args, cwd) => {
|
||||
const result = runProcessSync({ program, args, cwd })
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
const git = (args, cwd = directory) => run('git', args, cwd)
|
||||
try {
|
||||
git(['init', source])
|
||||
git(['config', 'user.name', 'CI test'], source)
|
||||
git(['config', 'user.email', 'ci@example.invalid'], source)
|
||||
writeFileSync(join(source, 'package.json'), JSON.stringify({ version: '1.4.165-rc.0' }))
|
||||
git(['add', 'package.json'], source)
|
||||
git(['-c', 'commit.gpgsign=false', 'commit', '-m', 'initial'], source)
|
||||
git(['tag', 'v1.4.167'], source)
|
||||
git(['-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'head'], source)
|
||||
git(['clone', '--depth=1', '--no-tags', pathToFileURL(source).href, shallow])
|
||||
expect(git(['rev-list', '--count', 'HEAD'], shallow)).toBe('1')
|
||||
expect(git(['tag', '--list'], shallow)).toBe('')
|
||||
const script = `
|
||||
const result = [];
|
||||
for (const [channel, exported] of [['daily', 'Daily'], ['hourly', 'Hourly'], ['adhoc', 'Adhoc']]) {
|
||||
const module = await import(${JSON.stringify(pathToFileURL(join(projectDir, 'config/scripts/')).href)} + channel + '-build-version.mjs');
|
||||
const date = new Date('2026-09-12T00:00:00Z');
|
||||
result.push(channel === 'adhoc'
|
||||
? module.getAdhocBuildIdentity(date, 'branch', ['v1.4.167'])
|
||||
: module['get' + exported + 'BuildIdentity'](date, { publishedVersions: ['v1.4.167'], releaseNames: [] }));
|
||||
}
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
`
|
||||
const identities = (cwd) => run(process.execPath, ['--input-type=module', '-e', script], cwd)
|
||||
expect(identities(shallow)).toBe(identities(source))
|
||||
expect(
|
||||
JSON.parse(identities(shallow)).every((identity) => identity.version.startsWith('1.4.168-'))
|
||||
).toBe(true)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('checks out only the vetted commit without remirroring refs', () => {
|
||||
const steps = readWorkflow('.github/workflows/adhoc-mac-build.yml').jobs['build-adhoc-mac']
|
||||
.steps
|
||||
const checkout = steps.find((step) => step.name === 'Checkout the requested ref')
|
||||
expect(checkout.env.GIT_DEFAULT_REF_FORMAT).toBe('reftable')
|
||||
expect(checkout.with.ref).toBe('${{ steps.vetted.outputs.sha }}')
|
||||
expect(checkout.with['fetch-depth']).toBe(0)
|
||||
expect(checkout.with['fetch-depth']).toBe(1)
|
||||
expect(checkout.with['persist-credentials']).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('release ref trust with case-twin names', () => {
|
||||
expect(result.stdout).toContain('Refusing to build PR ref')
|
||||
})
|
||||
|
||||
it('preserves both case variants in the subsequent full-history checkout', async () => {
|
||||
it('checks out the vetted SHA shallowly without mirroring case-twin refs again', async () => {
|
||||
const checkout = join(directory, 'checkout')
|
||||
const env = { ...identity, ...macCheckout.env }
|
||||
await git(['init', checkout], env)
|
||||
@@ -105,21 +105,15 @@ describe('release ref trust with case-twin names', () => {
|
||||
checkout,
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
`--depth=${macCheckout.with['fetch-depth']}`,
|
||||
repository,
|
||||
'+refs/heads/*:refs/remotes/origin/*',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
upper
|
||||
],
|
||||
env
|
||||
)
|
||||
await git(['-C', checkout, 'checkout', '--detach', upper], env)
|
||||
for (const [ref, sha] of [
|
||||
['refs/remotes/origin/Fix', upper],
|
||||
['refs/remotes/origin/fix', lower],
|
||||
['refs/tags/Release', upper],
|
||||
['refs/tags/release', lower]
|
||||
]) {
|
||||
expect(await git(['-C', checkout, 'rev-parse', `${ref}^{commit}`], env)).toBe(sha)
|
||||
}
|
||||
expect(await git(['-C', checkout, 'rev-parse', 'HEAD'], env)).toBe(upper)
|
||||
expect(await git(['-C', checkout, 'rev-list', '--count', 'HEAD'], env)).toBe('1')
|
||||
expect(await git(['-C', checkout, 'for-each-ref', '--format=%(refname)'], env)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import TimingSequencer from './scripts/ci-unit-sequencer.mjs'
|
||||
|
||||
const windowsTestWorkerOptions = process.platform === 'win32' ? { maxWorkers: 4 } : {}
|
||||
|
||||
@@ -15,6 +16,9 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
...(process.env.ORCA_BALANCE_UNIT_SHARDS === '1'
|
||||
? { sequence: { sequencer: TimingSequencer } }
|
||||
: {}),
|
||||
// Why: Node 26's undefined Web Storage globals prevent Vitest from installing happy-dom's.
|
||||
// Why --expose-gc: retention tests need a deterministic collection point to measure what a queue really holds.
|
||||
execArgv: ['--no-experimental-webstorage', '--expose-gc'],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user