diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7b0076bf..b86ee537 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -146,10 +146,10 @@ jobs: node-version: '24.10.0' cache: 'pnpm' - - name: Setup Xcode 26.4.1 + - name: Setup Xcode 26.6 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: - xcode-version: '26.4.1' + xcode-version: '26.6' - name: Install Watchman run: brew install watchman @@ -200,6 +200,25 @@ jobs: path: ./apps/playground/ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app key: ios-app-playground + # Two backgrounded samplers (persist across later steps via nohup): + # 1. mem-pressure.log - system swap/compressor/free every 2s (timeline) + # 2. mem-proc.log - `top -l1 -o mem` every 10s: honest per-process + # physical footprint (phys_footprint, no RSS shared-page double-count) + # + a PhysMem total + process count. Ranks the real consumers. + # Goal: attribute the 7 GB / 3 vCPU pressure to specific processes and + # bring them down so Harness fits on the standard macos runner. + - name: Start memory-pressure sampler + shell: bash + run: | + SAMPLE_LOG="$RUNNER_TEMP/mem-pressure.log" + PROC_LOG="$RUNNER_TEMP/mem-proc.log" + nohup bash -c 'while true; do echo "=== $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="; sysctl -n vm.swapusage; vm_stat; sleep 2; done' > "$SAMPLE_LOG" 2>&1 & + echo $! > "$RUNNER_TEMP/mem-sampler.pid" + nohup bash -c 'while true; do echo "=== $(date -u +%Y-%m-%dT%H:%M:%SZ) nprocs=$(ps -A | wc -l | tr -d " ") ==="; top -l 1 -o mem -n 45 -stats pid,mem,command; sleep 10; done' > "$PROC_LOG" 2>&1 & + echo $! > "$RUNNER_TEMP/mem-proc.pid" + echo "pressure sampler PID $(cat "$RUNNER_TEMP/mem-sampler.pid") -> $SAMPLE_LOG" + echo "process sampler PID $(cat "$RUNNER_TEMP/mem-proc.pid") -> $PROC_LOG" + - name: Run React Native Harness uses: ./ with: @@ -213,6 +232,65 @@ jobs: echo "HARNESS_RUNNER=$HARNESS_RUNNER" echo "HARNESS_EXIT_CODE=$HARNESS_EXIT_CODE" + - name: Stop sampler and summarize memory pressure + if: always() + shell: bash + run: | + PID="$(cat "$RUNNER_TEMP/mem-sampler.pid" 2>/dev/null || true)" + if [ -n "$PID" ]; then kill "$PID" 2>/dev/null || true; fi + PPID_="$(cat "$RUNNER_TEMP/mem-proc.pid" 2>/dev/null || true)" + if [ -n "$PPID_" ]; then kill "$PPID_" 2>/dev/null || true; fi + LOG="$RUNNER_TEMP/mem-pressure.log" + if [ ! -s "$LOG" ]; then echo "no sampler log found"; exit 0; fi + + PAGE=16384 + PEAK_SWAP_M="$(grep -o 'used = [0-9.]*M' "$LOG" | sed 's/used = //; s/M//' | sort -n | tail -1)" + PEAK_COMP_PAGES="$(grep 'occupied by compressor' "$LOG" | grep -oE '[0-9]+' | sort -n | tail -1)" + MIN_FREE_PAGES="$(grep 'Pages free' "$LOG" | grep -oE '[0-9]+' | sort -n | head -1)" + FIRST_SWAPOUT="$(grep 'Swapouts' "$LOG" | head -1 | grep -oE '[0-9]+' | head -1)" + LAST_SWAPOUT="$(grep 'Swapouts' "$LOG" | tail -1 | grep -oE '[0-9]+' | head -1)" + PEAK_SWAP_M="${PEAK_SWAP_M:-0}"; PEAK_COMP_PAGES="${PEAK_COMP_PAGES:-0}" + MIN_FREE_PAGES="${MIN_FREE_PAGES:-0}"; FIRST_SWAPOUT="${FIRST_SWAPOUT:-0}"; LAST_SWAPOUT="${LAST_SWAPOUT:-0}" + PEAK_COMP_MB=$(( PEAK_COMP_PAGES * PAGE / 1048576 )) + MIN_FREE_MB=$(( MIN_FREE_PAGES * PAGE / 1048576 )) + SWAPOUT_DELTA=$(( LAST_SWAPOUT - FIRST_SWAPOUT )) + + { + echo "### iOS runner memory pressure" + echo "" + echo "- Peak swap used: **${PEAK_SWAP_M} MB**" + echo "- Peak compressed memory: **${PEAK_COMP_MB} MB** (${PEAK_COMP_PAGES} pages)" + echo "- Min free memory: **${MIN_FREE_MB} MB** (${MIN_FREE_PAGES} pages)" + echo "- Swapouts during run (delta): **${SWAPOUT_DELTA}**" + echo "- Runner: macos-latest (3 vCPU / 7 GB), page size ${PAGE} B" + } >> "$GITHUB_STEP_SUMMARY" + + echo "peak_swap_mb=${PEAK_SWAP_M} peak_compressed_mb=${PEAK_COMP_MB} min_free_mb=${MIN_FREE_MB} swapout_delta=${SWAPOUT_DELTA}" + + # Honest physical footprint: top's PhysMem line (no RSS shared-page + # double-count). Per-process MEM (phys_footprint) is in the artifact. + PROC_LOG="$RUNNER_TEMP/mem-proc.log" + if [ -s "$PROC_LOG" ]; then + PEAK_PHYS="$(grep -o 'PhysMem: [0-9]*[MG] used' "$PROC_LOG" | grep -oE '[0-9]+[MG]' | awk '/G$/{sub(/G/,"");print $1*1024;next}{sub(/M/,"");print}' | sort -n | tail -1)" + PEAK_NPROCS="$(grep -oE 'nprocs=[0-9]+' "$PROC_LOG" | grep -oE '[0-9]+' | sort -n | tail -1)" + { + echo "- Peak physical memory used (top PhysMem): **${PEAK_PHYS:-?} MB**" + echo "- Peak process count: **${PEAK_NPROCS:-?}**" + echo "- Per-process physical footprint (top MEM) in artifact \`mem-proc.log\`" + } >> "$GITHUB_STEP_SUMMARY" + echo "peak_phys_mb=${PEAK_PHYS:-0} peak_nprocs=${PEAK_NPROCS:-0}" + fi + + - name: Upload memory samples + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mem-pressure-e2e-ios + path: | + ${{ runner.temp }}/mem-pressure.log + ${{ runner.temp }}/mem-proc.log + if-no-files-found: warn + - name: Upload Harness logs if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/.github/workflows/ios-simulator-startup-timings.yml b/.github/workflows/ios-simulator-startup-timings.yml new file mode 100644 index 00000000..a41f767a --- /dev/null +++ b/.github/workflows/ios-simulator-startup-timings.yml @@ -0,0 +1,170 @@ +name: Simulator boot benchmark + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + simulator-boot: + strategy: + fail-fast: false + matrix: + runtime: + - iOS-26-2 + - iOS-26-4 + - iOS-26-5 + + runs-on: macos-26 + timeout-minutes: 20 + + steps: + - name: Select Xcode 26.6 + shell: bash + run: | + set -euo pipefail + + sudo xcode-select \ + --switch /Applications/Xcode_26.6.app/Contents/Developer + + xcodebuild -version + + echo "SIMULATOR_CACHE_KEY=core-simulator-device-ios-26-5-xcode-26-6-${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV" + + - name: Restore iOS 26.5 simulator cache + id: cache-ios-26-5-restore + if: matrix.runtime == 'iOS-26-5' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/Library/Developer/CoreSimulator/Devices/FD9EC0F9-63AA-4C2F-B0F5-224AAE68866E + key: ${{ env.SIMULATOR_CACHE_KEY }} + + - name: Inspect available simulators + shell: bash + run: | + set -euo pipefail + + xcrun simctl list runtimes + xcrun simctl list devices available + + - name: Measure existing simulator boot + shell: bash + env: + RUNTIME_SUFFIX: ${{ matrix.runtime }} + DEVICE_NAME: iPhone 17 Pro + run: | + set -euo pipefail + + RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.${RUNTIME_SUFFIX}" + BOOT_LOG="$RUNNER_TEMP/bootstatus-${RUNTIME_SUFFIX}.log" + + echo "Looking for:" + echo " Runtime: $RUNTIME_ID" + echo " Device: $DEVICE_NAME" + + # Verify that the requested runtime is installed. + if ! xcrun simctl list runtimes -j | + jq -e --arg runtime "$RUNTIME_ID" ' + .runtimes[] + | select( + .identifier == $runtime + and .isAvailable == true + ) + ' >/dev/null; then + echo "::error::Runtime $RUNTIME_ID is not available" + exit 1 + fi + + # Select an existing available device only. + UDID="$( + xcrun simctl list devices available -j | + jq -er \ + --arg runtime "$RUNTIME_ID" \ + --arg name "$DEVICE_NAME" ' + .devices[$runtime] + | map( + select( + .name == $name + and .isAvailable == true + ) + ) + | .[0].udid + ' + )" || { + echo "::error::No existing $DEVICE_NAME found for $RUNTIME_ID" + echo "Available devices:" + xcrun simctl list devices available + exit 1 + } + + echo "Selected existing simulator:" + echo " $DEVICE_NAME" + echo " $RUNTIME_ID" + echo " $UDID" + + # Do not erase, recreate, or delete the selected device. + xcrun simctl shutdown "$UDID" 2>/dev/null || true + + START_SECONDS="$(date +%s)" + + echo "=== Boot request ===" + time xcrun simctl boot "$UDID" + + echo "=== Boot completion ===" + set +e + xcrun simctl bootstatus "$UDID" -b 2>&1 | + tee "$BOOT_LOG" + BOOT_STATUS="${PIPESTATUS[0]}" + set -e + + END_SECONDS="$(date +%s)" + ELAPSED_SECONDS="$((END_SECONDS - START_SECONDS))" + + MIGRATION_SAMPLES="$( + grep -c "Waiting on Data Migration" "$BOOT_LOG" || true + )" + + { + echo "### Existing simulator boot result" + echo + echo "- Runtime: \`$RUNTIME_ID\`" + echo "- Device: \`$DEVICE_NAME\`" + echo "- UDID: \`$UDID\`" + echo "- Duration: **${ELAPSED_SECONDS}s**" + echo "- Migration samples: **${MIGRATION_SAMPLES}**" + echo "- Runner image: \`${ImageOS:-unknown}\`" + echo "- Image version: \`${ImageVersion:-unknown}\`" + } >> "$GITHUB_STEP_SUMMARY" + + if [[ "$BOOT_STATUS" -ne 0 ]]; then + echo "::error::Simulator failed to finish booting" + exit "$BOOT_STATUS" + fi + + - name: Shut down iOS 26.5 simulator before caching + id: shutdown-ios-26-5 + if: always() && matrix.runtime == 'iOS-26-5' + shell: bash + run: | + set -euo pipefail + + UDID="FD9EC0F9-63AA-4C2F-B0F5-224AAE68866E" + xcrun simctl shutdown "$UDID" || true + + for _ in {1..30}; do + if xcrun simctl list devices | grep -F "$UDID" | grep -q '(Shutdown)'; then + exit 0 + fi + sleep 1 + done + + echo "::error::Simulator $UDID did not shut down before caching" + exit 1 + + - name: Save iOS 26.5 simulator cache + if: always() && matrix.runtime == 'iOS-26-5' && steps.shutdown-ios-26-5.outcome == 'success' && steps.cache-ios-26-5-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/Library/Developer/CoreSimulator/Devices/FD9EC0F9-63AA-4C2F-B0F5-224AAE68866E + key: ${{ steps.cache-ios-26-5-restore.outputs.cache-primary-key }}