diff --git a/.github/actions/macos-runner-tuneup/action.yml b/.github/actions/macos-runner-tuneup/action.yml new file mode 100644 index 0000000000..f87ae65c02 --- /dev/null +++ b/.github/actions/macos-runner-tuneup/action.yml @@ -0,0 +1,84 @@ +name: 'macOS Runner Tuneup' +description: 'Optimizes macOS GitHub runners by disabling resource-heavy background processes' +author: 'Bitwarden' + +runs: + using: 'composite' + steps: + - name: Optimize macOS Runner + shell: bash + run: | + echo "🚀 Starting macOS Runner Tuneup..." + echo "==================================" + + echo "🔍 Disabling Spotlight..." + sudo mdutil -a -i off # disables indexing on all volumes + sudo mdutil -a -d # disables spotlight activity on all volumes + + echo "🔍 Disabling Spotlight Knowledge Daemon..." + sudo launchctl disable system/com.apple.spotlightknowledged || true + sudo pkill -SIGKILL spotlightknowledged || true + + echo "🛑 Disabling metadata services..." + sudo launchctl disable system/com.apple.metadata.mds || true + sudo launchctl disable system/com.apple.metadata.mds.index || true + sudo launchctl disable system/com.apple.metadata.mds.scan || true + + echo "📦 Stopping metadata services..." + sudo launchctl bootout system/com.apple.metadata.mds || true + sudo launchctl bootout system/com.apple.metadata.mds.index || true + sudo launchctl bootout system/com.apple.metadata.mds.scan || true + sudo launchctl bootout system/com.apple.metadata.mdwrite || true + + echo "⚡ Killing metadata processes..." + sudo pkill -SIGKILL -f "Metadata.framework/Versions/A/Support/mds" || true + sudo pkill -SIGKILL Spotlight || true + echo "✅ Spotlight disabled!" + + echo "💥 Disabling ReportCrash..." + sudo launchctl disable system/com.apple.ReportCrash || true + sudo launchctl disable system/com.apple.ReportCrash.Root || true + + echo "💥 Unloading ReportCrash..." + sudo launchctl bootout system/com.apple.ReportCrash || true + sudo launchctl bootout system/com.apple.ReportCrash.Root || true + + sudo defaults write com.apple.CrashReporter DialogType none || true + echo "✅ ReportCrash disabled!" + + echo "🌱 Disabling EcosystemD..." + sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.ecosystemd.plist || true + sudo pkill -SIGKILL -f "ecosystemd" || true + echo "✅ EcosystemD disabled!" + + echo "🌱 Disabling EcosystemAnalyticsD..." + sudo launchctl bootout system/com.apple.ecosystemanalyticsd || true + echo "✅ EcosystemAnalyticsD disabled!" + + echo "🌱 Disabling SubmitDiagInfo..." + sudo defaults write /Library/Preferences/com.apple.SubmitDiagInfo AutoSubmit -bool false || true + + echo "🌍 Disabling location services..." + sudo defaults write /Library/Preferences/com.apple.locationd.plist LocationServicesEnabled -int 0 || true + + echo "🔍 Disabling Siri..." + sudo defaults write com.apple.assistant.support Assistant\ Enabled -bool false || true + sudo defaults write com.apple.Siri StatusMenuVisible -bool false || true + + echo "🔒 Disabling iCloud analytics and usage tracking..." + sudo defaults write com.apple.UsageTracking CoreDonationsEnabled -bool false || true + sudo defaults write com.apple.UsageTracking UDCAutomationEnabled -bool false || true + + echo "🔍 Disabling Spotlight suggestions..." + sudo defaults write com.apple.lookup.shared LookupSuggestionsDisabled -bool true || true + + echo "📊 Process Information After Tuning" + echo "==================================" + echo "🧠 Sorted by memory usage:" + head -n20 < <(ps -emo pid,pcpu,pmem,comm) + echo "" + echo "🔥 Sorted by CPU usage:" + head -n20 < <(ps -ero pid,pcpu,pmem,comm) + echo "" + echo "🎉 macOS Runner Tuneup Complete!" + diff --git a/.github/workflows/_build-any.yml b/.github/workflows/_build-any.yml index 68930489c7..b527812a0a 100644 --- a/.github/workflows/_build-any.yml +++ b/.github/workflows/_build-any.yml @@ -55,6 +55,9 @@ jobs: - name: Check out repo uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Optimize macOS Runner + uses: ./.github/actions/macos-runner-tuneup + - name: Read Xcode version from file if not provided run: | if [ -z "$_XCODE_VERSION" ]; then diff --git a/.github/workflows/test-bwa.yml b/.github/workflows/test-bwa.yml index a402c28325..1f4ab40eea 100644 --- a/.github/workflows/test-bwa.yml +++ b/.github/workflows/test-bwa.yml @@ -60,7 +60,7 @@ jobs: test: name: Test runs-on: macos-26 - timeout-minutes: 30 + timeout-minutes: 50 permissions: contents: read @@ -73,6 +73,9 @@ jobs: - name: Check out repo uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Optimize macOS Runner + uses: ./.github/actions/macos-runner-tuneup + - name: Read Xcode version and simulator configuration from file if not provided run: | if [ -z "$_XCODE_VERSION" ]; then @@ -123,6 +126,7 @@ jobs: - name: Build and test run: | + python Scripts/pyeetd/main.py & PYEETD_PID=$! xcrun xcodebuild test \ -workspace Bitwarden.xcworkspace \ -scheme Authenticator \ @@ -132,7 +136,10 @@ jobs: -derivedDataPath build/DerivedData \ -test-timeouts-enabled yes \ -maximum-test-execution-time-allowance 1 \ + -retry-tests-on-failure \ + -test-repetition-relaunch-enabled YES \ -quiet + kill $PYEETD_PID - name: Print Logs Summary if: always() diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0979edadd3..a3f6e9dfe9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: test: name: Test runs-on: macos-26 - timeout-minutes: 30 + timeout-minutes: 50 permissions: contents: read @@ -68,6 +68,9 @@ jobs: - name: Check out repo uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Optimize macOS Runner + uses: ./.github/actions/macos-runner-tuneup + - name: Read Xcode version and simulator configuration from file if not provided run: | if [ -z "$_XCODE_VERSION" ]; then @@ -118,6 +121,7 @@ jobs: - name: Build and test run: | + python Scripts/pyeetd/main.py & PYEETD_PID=$! xcrun xcodebuild test \ -workspace Bitwarden.xcworkspace \ -scheme Bitwarden \ @@ -127,7 +131,18 @@ jobs: -derivedDataPath build/DerivedData \ -test-timeouts-enabled yes \ -maximum-test-execution-time-allowance 1 \ + -retry-tests-on-failure \ + -test-repetition-relaunch-enabled YES \ -quiet + kill $PYEETD_PID + + - name: Output processes + run: | + echo "Sorted by memory usage" + ps -em -o pid,pcpu,pmem,comm | head -n40 + echo "--------------------------------" + echo "Sorted by CPU usage" + ps -er -o pid,pcpu,pmem,comm | head -n40 - name: Print Logs Summary if: always() diff --git a/Scripts/pyeetd/.python-version b/Scripts/pyeetd/.python-version new file mode 100644 index 0000000000..e4fba21835 --- /dev/null +++ b/Scripts/pyeetd/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Scripts/pyeetd/main.py b/Scripts/pyeetd/main.py new file mode 100644 index 0000000000..7fba3d885b --- /dev/null +++ b/Scripts/pyeetd/main.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +pyeetd - based on https://github.com/biscuitehh/yeetd + +how to use: +python Scripts/pyeetd/main.py & PYEETD_PID=$! +... +kill $PYEETD_PID +""" + +import os +import signal +import time +import subprocess +import re +from dataclasses import dataclass +from enum import Enum + +OS_PROCESSES = { + "Spotlight", + "ReportCrash", + "ecosystemanalyticsd" + "com.apple.ecosystemd", + "com.apple.metadata.mds", +} + +SIMULATOR_PROCESSES = { + "AegirPoster", + "InfographPoster", + "CollectionsPoster", + "ExtragalacticPoster", + "KaleidoscopePoster", + "EmojiPosterExtension", + "AmbientPhotoFramePosterProvider", + "PhotosPosterProvider", + "AvatarPosterExtension", + "GradientPosterExtension", + "MonogramPosterExtension" +} + +SIMULATOR_PATH_SEARCH_KEY = "simruntime/Contents/Resources/RuntimeRoot" + +# How long to sleep between checks in seconds +SLEEP_DELAY = 5 + +# How often to print process info (in seconds) +PRINT_PROCESSES_INTERVAL = 60 + +@dataclass +class ProcessInfo: + pid: int + cpu_percent: float + memory_percent: float + name: str + is_simulator: bool + + @property + def environment(self) -> str: + return "Simulator" if self.is_simulator else "OS" + + @property + def output_string(self) -> str: + return f"{self.pid}\t{self.cpu_percent}%\t{self.memory_percent}%\t{self.name}\t{self.environment}" + +class ProcessSort(Enum): + CPU = "cpu" + MEMORY = "memory" + +def get_processes(sort_by=ProcessSort.CPU): + """Get all processes using ps command - equivalent to Swift's proc_listallpids""" + sorty_by = "-ero" if sort_by == ProcessSort.CPU else "-emo" + result = subprocess.run(['ps', sorty_by, 'pid,pcpu,pmem,comm'], + capture_output=True, text=True, check=True) + processes = [] + + for line in result.stdout.splitlines()[1:]: # Skip header + parts = line.strip().split(None, 3) + if len(parts) >= 3: + pid = int(parts[0]) + cpu_percent = float(parts[1]) + memory_percent = float(parts[2]) + name = parts[3] + is_simulator = SIMULATOR_PATH_SEARCH_KEY in name + processes.append(ProcessInfo(pid, cpu_percent, memory_percent, name, is_simulator)) + + return processes + +def print_processes(processes, limit=-1): + output = [] + output.append("================================") + output.append("⚡️ Processes sorted by CPU usage:") + output.append("PID\tCPU%\tMemory%\tName\tEnvironment") + limit = len(processes) if limit == -1 else limit + for p in processes[:limit]: + output.append(p.output_string) + + output.append("--------------------------------") + output.append("🧠 Processes sorted by memory usage:") + output.append("PID\tCPU%\tMemory%\tName\tEnvironment") + processes_sorted_by_memory = sorted(processes, key=lambda x: x.memory_percent, reverse=True) + for p in processes_sorted_by_memory[:limit]: + output.append(p.output_string) + + output.append("================================") + print("\n".join(output)) + +def find_unwanted(processes): + yeeting = [] + for p in processes: + process_target_list = SIMULATOR_PROCESSES if p.is_simulator else OS_PROCESSES + for k in process_target_list: + if k in p.name: + yeeting.append(p) + return yeeting + +def yeet(processes): + output = [] + for p in processes: + output.append(f"🤠 pyeetd: Stopping - {p.output_string}") + os.killpg(p.pid, signal.SIGKILL) + return output + +def main(): + print_cycles = PRINT_PROCESSES_INTERVAL // SLEEP_DELAY + i = 0 + while True: + output = [] + processes = get_processes(ProcessSort.CPU) + processes_to_yeet = find_unwanted(processes) + output.extend(yeet(processes_to_yeet)) + output.append(f"🤠 {time.strftime('%Y-%m-%d %H:%M:%S')} - pyeetd {len(processes_to_yeet)} processes.") + print("\n".join(output)) + if i % print_cycles == 0: + print_processes(processes, 10) + i += 1 + time.sleep(SLEEP_DELAY) + +if __name__ == '__main__': + main()