From e57d43b1803aa251c355257db2a4b17d56c77012 Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 01:00:44 +0800 Subject: [PATCH 1/6] Parallelize the Test workflow into three jobs with caching Split the single sequential test job into parallel package, macos, and ipad jobs so wall-clock drops from sum(passes) to max(pass). Add SwiftPM and DerivedData build caches, HOMEBREW_NO_AUTO_UPDATE for faster xcodegen install, and xcodebuild speed flags (-quiet, -derivedDataPath, -skipPackagePluginValidation, -skipMacroValidation, COMPILER_INDEX_STORE_ENABLE=NO). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 68 +++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2aa4723..945d4b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,27 @@ permissions: contents: read jobs: - test: - name: Unit Tests + package: + name: Package Tests (KVMCore) + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Cache SwiftPM build + uses: actions/cache@v4 + with: + path: KVMCore/.build + key: spm-${{ runner.os }}-${{ hashFiles('KVMCore/Package.swift', 'KVMCore/Sources/**', 'KVMCore/Tests/**') }} + restore-keys: | + spm-${{ runner.os }}- + + - name: Run package tests + run: swift test --package-path KVMCore + + macos: + name: macOS Tests (KVMConsole) runs-on: macos-latest steps: @@ -21,12 +40,19 @@ jobs: - name: Install XcodeGen run: brew install xcodegen + env: + HOMEBREW_NO_AUTO_UPDATE: 1 - name: Generate Xcode project run: xcodegen generate - - name: Run package tests - run: swift test --package-path KVMCore + - name: Cache DerivedData + uses: actions/cache@v4 + with: + path: DerivedData + key: dd-macos-${{ runner.os }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsole/**') }} + restore-keys: | + dd-macos-${{ runner.os }}- - name: Run macOS tests run: | @@ -34,17 +60,51 @@ jobs: -project KVMConsole.xcodeproj \ -scheme KVMConsole \ -destination 'platform=macOS' \ + -derivedDataPath ./DerivedData \ + -quiet \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + COMPILER_INDEX_STORE_ENABLE=NO \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= \ DEVELOPMENT_TEAM= + ipad: + name: iPadOS Tests (KVMConsoleiPad) + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install XcodeGen + run: brew install xcodegen + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + + - name: Generate Xcode project + run: xcodegen generate + + - name: Cache DerivedData + uses: actions/cache@v4 + with: + path: DerivedData + key: dd-ipad-${{ runner.os }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsoleiPad/**') }} + restore-keys: | + dd-ipad-${{ runner.os }}- + - name: Run iPadOS tests run: | xcodebuild test \ -project KVMConsole.xcodeproj \ -scheme KVMConsoleiPad \ -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M5)' \ + -derivedDataPath ./DerivedData \ + -quiet \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + COMPILER_INDEX_STORE_ENABLE=NO \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= \ From 601eb51dc5f0c2cda50248caf319d48b2a05cc0c Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 01:20:35 +0800 Subject: [PATCH 2/6] Pre-boot iPad simulator and key caches on Xcode toolchain version The iPad job's wall-clock is dominated by building the app for the simulator plus the cold simulator boot. Boot the simulator at job start so it warms up in parallel with the XcodeGen install and the build, moving boot off the critical path. Also fold a hash of `xcodebuild -version` into every cache key so a runner-image Xcode roll invalidates the SwiftPM/DerivedData caches instead of restoring stale module artifacts that fail with "module compiled with a different version of Swift". Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 945d4b3..7f8ed14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,13 +19,16 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Toolchain version + run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" + - name: Cache SwiftPM build uses: actions/cache@v4 with: path: KVMCore/.build - key: spm-${{ runner.os }}-${{ hashFiles('KVMCore/Package.swift', 'KVMCore/Sources/**', 'KVMCore/Tests/**') }} + key: spm-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}-${{ hashFiles('KVMCore/Package.swift', 'KVMCore/Sources/**', 'KVMCore/Tests/**') }} restore-keys: | - spm-${{ runner.os }}- + spm-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}- - name: Run package tests run: swift test --package-path KVMCore @@ -38,6 +41,9 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Toolchain version + run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" + - name: Install XcodeGen run: brew install xcodegen env: @@ -50,9 +56,9 @@ jobs: uses: actions/cache@v4 with: path: DerivedData - key: dd-macos-${{ runner.os }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsole/**') }} + key: dd-macos-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsole/**') }} restore-keys: | - dd-macos-${{ runner.os }}- + dd-macos-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}- - name: Run macOS tests run: | @@ -78,6 +84,15 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Boot simulator + # Kick off the simulator boot now so it warms up in parallel with the + # XcodeGen install and the app build below; simctl returns immediately + # and the device finishes booting off the critical path. + run: xcrun simctl boot 'iPad Pro 11-inch (M5)' || true + + - name: Toolchain version + run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" + - name: Install XcodeGen run: brew install xcodegen env: @@ -90,9 +105,9 @@ jobs: uses: actions/cache@v4 with: path: DerivedData - key: dd-ipad-${{ runner.os }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsoleiPad/**') }} + key: dd-ipad-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsoleiPad/**') }} restore-keys: | - dd-ipad-${{ runner.os }}- + dd-ipad-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}- - name: Run iPadOS tests run: | From fa83a284f2a1bf165153eb5c4c94f0bc3981d87a Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 01:38:12 +0800 Subject: [PATCH 3/6] Install XcodeGen from prebuilt release instead of Homebrew `brew install xcodegen` spends ~2 min per job in Homebrew overhead even with HOMEBREW_NO_AUTO_UPDATE and a tiny 7.4 MB bottle. Download the pinned prebuilt XcodeGen release artifact and add it to PATH instead, cutting the install step from ~2 min to a few seconds on both the macos and ipad jobs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f8ed14..23a0d2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,11 @@ on: permissions: contents: read +# Pinned XcodeGen release. We download the prebuilt binary instead of +# `brew install xcodegen`, which spends ~2 min in Homebrew overhead per job. +env: + XCODEGEN_VERSION: "2.45.4" + jobs: package: name: Package Tests (KVMCore) @@ -45,9 +50,10 @@ jobs: run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" - name: Install XcodeGen - run: brew install xcodegen - env: - HOMEBREW_NO_AUTO_UPDATE: 1 + run: | + curl -fsSL "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" -o /tmp/xcodegen.zip + unzip -q /tmp/xcodegen.zip -d /tmp/xcodegen-dist + echo "/tmp/xcodegen-dist/xcodegen/bin" >> "$GITHUB_PATH" - name: Generate Xcode project run: xcodegen generate @@ -94,9 +100,10 @@ jobs: run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" - name: Install XcodeGen - run: brew install xcodegen - env: - HOMEBREW_NO_AUTO_UPDATE: 1 + run: | + curl -fsSL "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" -o /tmp/xcodegen.zip + unzip -q /tmp/xcodegen.zip -d /tmp/xcodegen-dist + echo "/tmp/xcodegen-dist/xcodegen/bin" >> "$GITHUB_PATH" - name: Generate Xcode project run: xcodegen generate From d05d86153d625f684049939d06280b0b9bca6201 Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 02:23:21 +0800 Subject: [PATCH 4/6] Boot the iPad simulator asynchronously off the critical path `simctl boot` was blocking ~40-80s waiting for the device to finish booting. The boot is actually carried out by the CoreSimulator daemon, so run it via nohup in the background and let it complete while XcodeGen installs and the app builds. If the device isn't ready when the test step runs, xcodebuild boots it itself, so correctness is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23a0d2a..2e23e8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,10 +91,12 @@ jobs: uses: actions/checkout@v6 - name: Boot simulator - # Kick off the simulator boot now so it warms up in parallel with the - # XcodeGen install and the app build below; simctl returns immediately - # and the device finishes booting off the critical path. - run: xcrun simctl boot 'iPad Pro 11-inch (M5)' || true + # Kick off the simulator boot in the background so it warms up in + # parallel with the XcodeGen install and the app build below. The boot + # is carried out by the CoreSimulator daemon, so this step returns + # immediately and the device finishes booting off the critical path; + # if it isn't ready in time, the test step's xcodebuild boots it itself. + run: nohup xcrun simctl boot 'iPad Pro 11-inch (M5)' >/dev/null 2>&1 & - name: Toolchain version run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" From 8871518b4a5c1fb52dff7a2ad16031b661b09fff Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 02:26:01 +0800 Subject: [PATCH 5/6] Drop DerivedData caching from the xcodebuild jobs The DerivedData cache grew to ~96 MB and GitHub's cache service throttled the restore to ~0.1 MB/s, costing up to 2.5 min just to download it. Xcode also rebuilds anyway because actions/checkout resets file mtimes, so the cache saved little compile time while adding large, unpredictable restore/save overhead. Remove it (and the now-unused -derivedDataPath and toolchain-key steps) so the macOS and iPad jobs do a clean, predictable build each run. The package job keeps its SwiftPM .build cache, which is content-hashed, smaller, and reliably incremental. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e23e8f..6cf18d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,9 +46,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Toolchain version - run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" - - name: Install XcodeGen run: | curl -fsSL "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" -o /tmp/xcodegen.zip @@ -58,21 +55,12 @@ jobs: - name: Generate Xcode project run: xcodegen generate - - name: Cache DerivedData - uses: actions/cache@v4 - with: - path: DerivedData - key: dd-macos-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsole/**') }} - restore-keys: | - dd-macos-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}- - - name: Run macOS tests run: | xcodebuild test \ -project KVMConsole.xcodeproj \ -scheme KVMConsole \ -destination 'platform=macOS' \ - -derivedDataPath ./DerivedData \ -quiet \ -skipPackagePluginValidation \ -skipMacroValidation \ @@ -98,9 +86,6 @@ jobs: # if it isn't ready in time, the test step's xcodebuild boots it itself. run: nohup xcrun simctl boot 'iPad Pro 11-inch (M5)' >/dev/null 2>&1 & - - name: Toolchain version - run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" - - name: Install XcodeGen run: | curl -fsSL "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" -o /tmp/xcodegen.zip @@ -110,21 +95,12 @@ jobs: - name: Generate Xcode project run: xcodegen generate - - name: Cache DerivedData - uses: actions/cache@v4 - with: - path: DerivedData - key: dd-ipad-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}-${{ hashFiles('project.yml', 'KVMCore/Sources/**', 'KVMConsoleiPad/**') }} - restore-keys: | - dd-ipad-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }}- - - name: Run iPadOS tests run: | xcodebuild test \ -project KVMConsole.xcodeproj \ -scheme KVMConsoleiPad \ -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M5)' \ - -derivedDataPath ./DerivedData \ -quiet \ -skipPackagePluginValidation \ -skipMacroValidation \ From eef22b7fa62ce96608b86f7c11d77c4607d2e00d Mon Sep 17 00:00:00 2001 From: Yu-Xi Lim Date: Fri, 26 Jun 2026 10:07:00 +0800 Subject: [PATCH 6/6] Cache only the iOS SDK module cache for the iPad job Profiling showed the iPad job's extra CI time is not compilation (locally the iOS build is as fast as macOS) but building the cold iOS-SDK Clang/ Swift module cache on a fresh runner. Cache just DerivedData/ ModuleCache.noindex (the 368 precompiled system-framework PCMs), keyed on the Xcode toolchain version alone since it is source-independent, so it restores on every run after the first. This skips the cold module build without caching the rest of DerivedData, whose project objects Xcode rebuilds anyway because checkout resets file mtimes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cf18d7..c6e23a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -95,12 +95,29 @@ jobs: - name: Generate Xcode project run: xcodegen generate + - name: Toolchain version + run: echo "TOOLCHAIN_KEY=$(xcodebuild -version | shasum | cut -c1-12)" >> "$GITHUB_ENV" + + - name: Cache iOS module cache + # The Clang/Swift module cache holds the precompiled iOS-SDK system + # modules (SwiftUI, UIKit, AVFoundation, ...). Building it from cold is + # the bulk of the iPad job's extra time on a fresh runner. It depends + # only on the SDK/compiler, not on our sources, so the key is just the + # toolchain version — it hits on every run after the first. We cache + # only this, not the rest of DerivedData (Xcode rebuilds the project + # objects anyway because checkout resets mtimes). + uses: actions/cache@v4 + with: + path: DerivedData/ModuleCache.noindex + key: modulecache-ipad-${{ runner.os }}-${{ env.TOOLCHAIN_KEY }} + - name: Run iPadOS tests run: | xcodebuild test \ -project KVMConsole.xcodeproj \ -scheme KVMConsoleiPad \ -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M5)' \ + -derivedDataPath ./DerivedData \ -quiet \ -skipPackagePluginValidation \ -skipMacroValidation \