-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
perf(tests): stop the slowest app-host unit tests waiting out real timeouts #14210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
196d7b9
258b283
b30b030
13fad29
626112b
b3755b8
dbdcd23
0d6df3a
b602efb
18e3c96
70ecc1a
9f258dd
77bfa38
1e62556
cbc73f7
366492b
5510126
a956dae
2c55871
d5cedc9
e6926fb
4c22264
84e70db
c875d8d
3dc91b2
4d1f51e
999b359
17381ac
70eaf44
42ae036
e167c15
de81ebd
22a62e0
9d37670
7b985be
bafd6c3
a91a26c
f7132b3
4df563e
49e5dda
30590ad
41d8f87
2f6adcc
ecc8044
8ca302a
b102403
5a92d7f
87a7016
a682c55
e28ef70
e160e82
36f988a
21cca2a
0af1a5b
cf7eacc
86c850b
74cbe2c
572f94f
ed3ab7a
745430d
28d2f72
c7e9c55
a6fd4bf
ca1d74d
63a6e26
60c4e7b
dd779b8
6e387b0
b820391
29683cb
78c6f09
7b3b4b3
874d9b0
572dc2a
49829bc
a29b56c
e1816a5
e63f6e7
eeda3f9
5e929bc
c9b892b
f9b2c4c
c6f9f7f
d460008
2889bb8
c0a7557
5fffa37
93b7b77
0bacf0b
763f4da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,152 @@ | ||||||
| name: command palette search benchmarks | ||||||
|
|
||||||
| # The wall-clock command-palette search benchmarks are gated out of the sharded | ||||||
| # app-host unit suite by CMUX_COMMAND_PALETTE_SEARCH_BENCHMARKS (see | ||||||
| # skipUnlessCommandPaletteSearchBenchmarksAreEnabled in | ||||||
| # cmuxTests/CommandPaletteNucleoFixtures.swift). A gate with no caller at all is | ||||||
| # deletion with extra steps: the benchmarks keep compiling and stop running. | ||||||
| # This workflow is that caller -- dispatch-only while automatic CI is paused, so | ||||||
| # running the benchmarks is a deliberate act rather than a daily cost. It runs | ||||||
| # the one script that owns the gate, | ||||||
| # scripts/test-command-palette-nucleo-ffi.sh, which enables the env var and | ||||||
| # names both benchmark classes; the script's own BENCH assertions fail the job | ||||||
| # if any gated benchmark silently skipped. | ||||||
| on: | ||||||
| # Temporarily manual-only beginning 2026-07-13 to pause automatic CI. | ||||||
| # tmux-corpus.yml and perf-activation.yml -- the two closest macOS | ||||||
| # benchmark nightlies -- both carry this and have their crons removed. | ||||||
| # A new cron here would quietly reverse that reduction, so the gate's | ||||||
| # caller is dispatch-only until the pause lifts. | ||||||
| workflow_dispatch: | ||||||
| inputs: | ||||||
| runner: | ||||||
| description: macOS runner (auto follows MACOS_RUNNER_15) | ||||||
| required: false | ||||||
| default: auto | ||||||
| type: choice | ||||||
| options: | ||||||
| - auto | ||||||
| - blacksmith-6vcpu-macos-15 | ||||||
| - blacksmith-6vcpu-macos-26 | ||||||
| - blacksmith-6vcpu-macos-latest | ||||||
|
|
||||||
| permissions: | ||||||
| contents: read | ||||||
|
|
||||||
| concurrency: | ||||||
| group: ${{ github.workflow }}-${{ github.ref }} | ||||||
| cancel-in-progress: false | ||||||
|
|
||||||
| jobs: | ||||||
| command-palette-search-benchmarks: | ||||||
| runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'macos-26' || ((!inputs.runner || inputs.runner == 'auto') && (vars.CI_PAID_MACOS_OVERFLOW == '1' && vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15') || inputs.runner) }} | ||||||
| timeout-minutes: 60 | ||||||
| env: | ||||||
| # XCTest app-host crashes can leave xcodebuild waiting in Swift's crash | ||||||
| # backtracer until the job timeout. Keep crash handling non-interactive | ||||||
| # and cheap so xcodebuild can restart/finish the suite. | ||||||
| SWIFT_BACKTRACE: "interactive=no,timeout=0s,symbolicate=off,color=no" | ||||||
| # Declared here, not in a step, so the always() summary and upload steps | ||||||
| # still have a path when an earlier step fails. | ||||||
| CMUX_NUCLEO_FFI_LOG: ${{ github.workspace }}/command-palette-search-benchmarks.log | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,160p' .github/workflows/command-palette-search-benchmarks.yml
sed -n '1,105p' scripts/test-command-palette-nucleo-ffi.shRepository: manaflow-ai/cmux Length of output: 10157 🌐 Web query:
💡 Result: Use a run-specific benchmark log path. A self-hosted runner can retain files in Proposed change- CMUX_NUCLEO_FFI_LOG: ${{ github.workspace }}/command-palette-search-benchmarks.log
+ CMUX_NUCLEO_FFI_LOG: ${{ runner.temp }}/command-palette-search-benchmarks-${{ github.run_id }}-${{ github.run_attempt }}.log📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| steps: | ||||||
| - name: Clear stale git locks (self-hosted reused workspace) | ||||||
| shell: bash | ||||||
| run: | | ||||||
| # Self-hosted macOS runners reuse the workspace. A job cancelled or | ||||||
| # killed mid-checkout can leave a stale .git/modules/*/index.lock that | ||||||
| # fails every later submodule checkout (e.g. ghostty). Clear them first. | ||||||
| ws="${GITHUB_WORKSPACE:-$PWD}" | ||||||
| rm -f "$ws/.git/index.lock" 2>/dev/null || true | ||||||
| if [ -d "$ws/.git/modules" ]; then | ||||||
| find "$ws/.git/modules" -type f -name "*.lock" -delete 2>/dev/null || true | ||||||
| fi | ||||||
|
|
||||||
| - name: Checkout | ||||||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||||||
| with: | ||||||
| persist-credentials: false | ||||||
| submodules: recursive | ||||||
|
|
||||||
| - name: Select Xcode | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
| ./scripts/select-ci-xcode.sh | ||||||
|
|
||||||
| - name: Setup Bun | ||||||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | ||||||
| with: | ||||||
| bun-version: "1.3.14" | ||||||
|
|
||||||
| - name: Cache GhosttyKit.xcframework | ||||||
| id: cache-ghosttykit | ||||||
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | ||||||
| with: | ||||||
| path: GhosttyKit.xcframework | ||||||
| key: ghosttykit-sentry-off-v1-${{ hashFiles('.gitmodules', 'ghostty/**') }} | ||||||
|
|
||||||
| - name: Download pre-built GhosttyKit.xcframework | ||||||
| if: steps.cache-ghosttykit.outputs.cache-hit != 'true' | ||||||
| run: ./scripts/download-prebuilt-ghosttykit.sh | ||||||
|
|
||||||
| - name: Install zig | ||||||
| run: ./scripts/install-zig-ci.sh | ||||||
|
|
||||||
| - name: Install Rust | ||||||
| run: ./scripts/install-rust-ci.sh | ||||||
|
|
||||||
| - name: Cache Swift packages | ||||||
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | ||||||
| with: | ||||||
| path: .ci-source-packages | ||||||
| key: command-palette-bench-spm-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} | ||||||
| restore-keys: command-palette-bench-spm- | ||||||
|
|
||||||
| - name: Sanitize Swift package cache | ||||||
| run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .ci-source-packages | ||||||
|
|
||||||
| - name: Prepare isolated DerivedData | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
| DERIVED_DATA_PATH="${RUNNER_TEMP}/cmux-derived-data-command-palette-bench-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | ||||||
| rm -rf "$DERIVED_DATA_PATH" | ||||||
| mkdir -p "$DERIVED_DATA_PATH" | ||||||
| echo "CMUX_NUCLEO_FFI_DERIVED_DATA=$DERIVED_DATA_PATH" >> "$GITHUB_ENV" | ||||||
|
|
||||||
| - name: Resolve Swift packages | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | ||||||
| mkdir -p "$SOURCE_PACKAGES_DIR" | ||||||
| xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | ||||||
| -derivedDataPath "$CMUX_NUCLEO_FFI_DERIVED_DATA" \ | ||||||
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | ||||||
| -resolvePackageDependencies | ||||||
|
|
||||||
| - name: Run command palette search benchmarks | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
| CMUX_NUCLEO_FFI_SOURCE_PACKAGES="$PWD/.ci-source-packages" \ | ||||||
| ./scripts/test-command-palette-nucleo-ffi.sh | ||||||
|
|
||||||
| - name: Write benchmark summary | ||||||
| if: always() | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
| { | ||||||
| echo "## Command palette search benchmarks" | ||||||
| echo "" | ||||||
| if [ -f "$CMUX_NUCLEO_FFI_LOG" ]; then | ||||||
| grep 'BENCH cmd+' "$CMUX_NUCLEO_FFI_LOG" | sed 's/^/- `/; s/$/`/' || echo "No BENCH lines were emitted." | ||||||
| else | ||||||
| echo "The benchmark invocation produced no log." | ||||||
| fi | ||||||
| } >> "$GITHUB_STEP_SUMMARY" | ||||||
|
|
||||||
| - name: Upload benchmark log | ||||||
| if: always() | ||||||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | ||||||
| with: | ||||||
| name: command-palette-search-benchmarks | ||||||
| path: ${{ env.CMUX_NUCLEO_FFI_LOG }} | ||||||
| if-no-files-found: ignore | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| public import Foundation | ||
|
|
||
| /// Shared timeout window for the `browser.download.wait` control call. | ||
| /// | ||
| /// The app-side handler and the command-line client have to agree: the client | ||
| /// has to outwait the window the handler is allowed to spend, or a download | ||
| /// that the app reports on time still fails in the terminal. Both sides read | ||
| /// ``standard`` so the two windows cannot drift apart. | ||
| /// | ||
| /// The windows are values rather than fixed constants so a test can hold the | ||
| /// same agreement at a fraction of the wall-clock cost instead of waiting the | ||
| /// shipped window out. | ||
| public struct BrowserDownloadWaitTimeout: Equatable, Sendable { | ||
| /// The windows the app and the CLI ship with. | ||
| public static let standard = BrowserDownloadWaitTimeout() | ||
|
|
||
| /// Window the handler waits when the caller sends no `timeout_ms`. | ||
| public let defaultTimeoutMilliseconds: Int | ||
|
|
||
| /// Ceiling the handler applies to a caller-supplied `timeout_ms`. | ||
| public let maximumTimeoutMilliseconds: Int | ||
|
|
||
| /// Slack the client adds on top of the handler's window to cover request | ||
| /// dispatch, the handler's own bookkeeping, and the reply hop. | ||
| public let clientResponseSlackSeconds: TimeInterval | ||
|
|
||
| /// Creates a window pair the handler and the client both read. | ||
| /// | ||
| /// - Parameters: | ||
| /// - defaultTimeoutMilliseconds: Window for a caller that sends no | ||
| /// `timeout_ms`. | ||
| /// - maximumTimeoutMilliseconds: Ceiling for a caller-supplied | ||
| /// `timeout_ms`. | ||
| /// - clientResponseSlackSeconds: Slack the client adds to the handler's | ||
| /// window. | ||
| public init( | ||
| defaultTimeoutMilliseconds: Int = 10_000, | ||
| maximumTimeoutMilliseconds: Int = 120_000, | ||
| clientResponseSlackSeconds: TimeInterval = 5 | ||
| ) { | ||
| self.defaultTimeoutMilliseconds = defaultTimeoutMilliseconds | ||
| self.maximumTimeoutMilliseconds = maximumTimeoutMilliseconds | ||
| self.clientResponseSlackSeconds = clientResponseSlackSeconds | ||
| } | ||
|
|
||
| /// Window the handler spends for `requestedMilliseconds`. | ||
| /// | ||
| /// - Parameter requestedMilliseconds: The caller's `timeout_ms`, or `nil` | ||
| /// when the caller sent none. | ||
| /// - Returns: The clamped handler window in milliseconds. | ||
| public func handlerTimeoutMilliseconds( | ||
| requestedMilliseconds: Int? | ||
| ) -> Int { | ||
| let requested = max(1, requestedMilliseconds ?? defaultTimeoutMilliseconds) | ||
| return min(requested, maximumTimeoutMilliseconds) | ||
| } | ||
|
|
||
| /// Socket response timeout the client uses for `requestedMilliseconds`. | ||
| /// | ||
| /// - Parameter requestedMilliseconds: The caller's `--timeout-ms`, or `nil` | ||
| /// when the caller passed none. | ||
| /// - Returns: The handler window plus ``clientResponseSlackSeconds``. | ||
| public func clientResponseTimeoutSeconds( | ||
| requestedMilliseconds: Int? | ||
| ) -> TimeInterval { | ||
| let handlerWindow = handlerTimeoutMilliseconds( | ||
| requestedMilliseconds: requestedMilliseconds | ||
| ) | ||
| return TimeInterval(handlerWindow) / 1000.0 + clientResponseSlackSeconds | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 26707
Set up Node for every notification-test path.
The notification step runs on the focused shard and on matching
unit_strict_steps, but Node detection and installation run only on the CLI shard. Because the runner image may not include Node,command -v nodecan fail before the suites run.Suggested fix
🤖 Prompt for AI Agents