Skip to content

Commit 0b9b9d8

Browse files
authored
[PM-26455] Improve CI runtimes by disabling cpu intensive processes (#1995)
1 parent 11f7358 commit 0b9b9d8

File tree

6 files changed

+251
-2
lines changed

6 files changed

+251
-2
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: 'macOS Runner Tuneup'
2+
description: 'Optimizes macOS GitHub runners by disabling resource-heavy background processes'
3+
author: 'Bitwarden'
4+
5+
runs:
6+
using: 'composite'
7+
steps:
8+
- name: Optimize macOS Runner
9+
shell: bash
10+
run: |
11+
echo "🚀 Starting macOS Runner Tuneup..."
12+
echo "=================================="
13+
14+
echo "🔍 Disabling Spotlight..."
15+
sudo mdutil -a -i off # disables indexing on all volumes
16+
sudo mdutil -a -d # disables spotlight activity on all volumes
17+
18+
echo "🔍 Disabling Spotlight Knowledge Daemon..."
19+
sudo launchctl disable system/com.apple.spotlightknowledged || true
20+
sudo pkill -SIGKILL spotlightknowledged || true
21+
22+
echo "🛑 Disabling metadata services..."
23+
sudo launchctl disable system/com.apple.metadata.mds || true
24+
sudo launchctl disable system/com.apple.metadata.mds.index || true
25+
sudo launchctl disable system/com.apple.metadata.mds.scan || true
26+
27+
echo "📦 Stopping metadata services..."
28+
sudo launchctl bootout system/com.apple.metadata.mds || true
29+
sudo launchctl bootout system/com.apple.metadata.mds.index || true
30+
sudo launchctl bootout system/com.apple.metadata.mds.scan || true
31+
sudo launchctl bootout system/com.apple.metadata.mdwrite || true
32+
33+
echo "⚡ Killing metadata processes..."
34+
sudo pkill -SIGKILL -f "Metadata.framework/Versions/A/Support/mds" || true
35+
sudo pkill -SIGKILL Spotlight || true
36+
echo "✅ Spotlight disabled!"
37+
38+
echo "💥 Disabling ReportCrash..."
39+
sudo launchctl disable system/com.apple.ReportCrash || true
40+
sudo launchctl disable system/com.apple.ReportCrash.Root || true
41+
42+
echo "💥 Unloading ReportCrash..."
43+
sudo launchctl bootout system/com.apple.ReportCrash || true
44+
sudo launchctl bootout system/com.apple.ReportCrash.Root || true
45+
46+
sudo defaults write com.apple.CrashReporter DialogType none || true
47+
echo "✅ ReportCrash disabled!"
48+
49+
echo "🌱 Disabling EcosystemD..."
50+
sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.ecosystemd.plist || true
51+
sudo pkill -SIGKILL -f "ecosystemd" || true
52+
echo "✅ EcosystemD disabled!"
53+
54+
echo "🌱 Disabling EcosystemAnalyticsD..."
55+
sudo launchctl bootout system/com.apple.ecosystemanalyticsd || true
56+
echo "✅ EcosystemAnalyticsD disabled!"
57+
58+
echo "🌱 Disabling SubmitDiagInfo..."
59+
sudo defaults write /Library/Preferences/com.apple.SubmitDiagInfo AutoSubmit -bool false || true
60+
61+
echo "🌍 Disabling location services..."
62+
sudo defaults write /Library/Preferences/com.apple.locationd.plist LocationServicesEnabled -int 0 || true
63+
64+
echo "🔍 Disabling Siri..."
65+
sudo defaults write com.apple.assistant.support Assistant\ Enabled -bool false || true
66+
sudo defaults write com.apple.Siri StatusMenuVisible -bool false || true
67+
68+
echo "🔒 Disabling iCloud analytics and usage tracking..."
69+
sudo defaults write com.apple.UsageTracking CoreDonationsEnabled -bool false || true
70+
sudo defaults write com.apple.UsageTracking UDCAutomationEnabled -bool false || true
71+
72+
echo "🔍 Disabling Spotlight suggestions..."
73+
sudo defaults write com.apple.lookup.shared LookupSuggestionsDisabled -bool true || true
74+
75+
echo "📊 Process Information After Tuning"
76+
echo "=================================="
77+
echo "🧠 Sorted by memory usage:"
78+
head -n20 < <(ps -emo pid,pcpu,pmem,comm)
79+
echo ""
80+
echo "🔥 Sorted by CPU usage:"
81+
head -n20 < <(ps -ero pid,pcpu,pmem,comm)
82+
echo ""
83+
echo "🎉 macOS Runner Tuneup Complete!"
84+

.github/workflows/_build-any.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
- name: Check out repo
5656
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
5757

58+
- name: Optimize macOS Runner
59+
uses: ./.github/actions/macos-runner-tuneup
60+
5861
- name: Read Xcode version from file if not provided
5962
run: |
6063
if [ -z "$_XCODE_VERSION" ]; then

.github/workflows/test-bwa.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
test:
6161
name: Test
6262
runs-on: macos-26
63-
timeout-minutes: 30
63+
timeout-minutes: 50
6464
permissions:
6565
contents: read
6666

@@ -73,6 +73,9 @@ jobs:
7373
- name: Check out repo
7474
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
7575

76+
- name: Optimize macOS Runner
77+
uses: ./.github/actions/macos-runner-tuneup
78+
7679
- name: Read Xcode version and simulator configuration from file if not provided
7780
run: |
7881
if [ -z "$_XCODE_VERSION" ]; then
@@ -123,6 +126,7 @@ jobs:
123126
124127
- name: Build and test
125128
run: |
129+
python Scripts/pyeetd/main.py & PYEETD_PID=$!
126130
xcrun xcodebuild test \
127131
-workspace Bitwarden.xcworkspace \
128132
-scheme Authenticator \
@@ -132,7 +136,10 @@ jobs:
132136
-derivedDataPath build/DerivedData \
133137
-test-timeouts-enabled yes \
134138
-maximum-test-execution-time-allowance 1 \
139+
-retry-tests-on-failure \
140+
-test-repetition-relaunch-enabled YES \
135141
-quiet
142+
kill $PYEETD_PID
136143
137144
- name: Print Logs Summary
138145
if: always()

.github/workflows/test.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
test:
5656
name: Test
5757
runs-on: macos-26
58-
timeout-minutes: 30
58+
timeout-minutes: 50
5959
permissions:
6060
contents: read
6161

@@ -68,6 +68,9 @@ jobs:
6868
- name: Check out repo
6969
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
7070

71+
- name: Optimize macOS Runner
72+
uses: ./.github/actions/macos-runner-tuneup
73+
7174
- name: Read Xcode version and simulator configuration from file if not provided
7275
run: |
7376
if [ -z "$_XCODE_VERSION" ]; then
@@ -118,6 +121,7 @@ jobs:
118121
119122
- name: Build and test
120123
run: |
124+
python Scripts/pyeetd/main.py & PYEETD_PID=$!
121125
xcrun xcodebuild test \
122126
-workspace Bitwarden.xcworkspace \
123127
-scheme Bitwarden \
@@ -127,7 +131,18 @@ jobs:
127131
-derivedDataPath build/DerivedData \
128132
-test-timeouts-enabled yes \
129133
-maximum-test-execution-time-allowance 1 \
134+
-retry-tests-on-failure \
135+
-test-repetition-relaunch-enabled YES \
130136
-quiet
137+
kill $PYEETD_PID
138+
139+
- name: Output processes
140+
run: |
141+
echo "Sorted by memory usage"
142+
ps -em -o pid,pcpu,pmem,comm | head -n40
143+
echo "--------------------------------"
144+
echo "Sorted by CPU usage"
145+
ps -er -o pid,pcpu,pmem,comm | head -n40
131146
132147
- name: Print Logs Summary
133148
if: always()

Scripts/pyeetd/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

Scripts/pyeetd/main.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""
3+
pyeetd - based on https://github.com/biscuitehh/yeetd
4+
5+
how to use:
6+
python Scripts/pyeetd/main.py & PYEETD_PID=$!
7+
...
8+
kill $PYEETD_PID
9+
"""
10+
11+
import os
12+
import signal
13+
import time
14+
import subprocess
15+
import re
16+
from dataclasses import dataclass
17+
from enum import Enum
18+
19+
OS_PROCESSES = {
20+
"Spotlight",
21+
"ReportCrash",
22+
"ecosystemanalyticsd"
23+
"com.apple.ecosystemd",
24+
"com.apple.metadata.mds",
25+
}
26+
27+
SIMULATOR_PROCESSES = {
28+
"AegirPoster",
29+
"InfographPoster",
30+
"CollectionsPoster",
31+
"ExtragalacticPoster",
32+
"KaleidoscopePoster",
33+
"EmojiPosterExtension",
34+
"AmbientPhotoFramePosterProvider",
35+
"PhotosPosterProvider",
36+
"AvatarPosterExtension",
37+
"GradientPosterExtension",
38+
"MonogramPosterExtension"
39+
}
40+
41+
SIMULATOR_PATH_SEARCH_KEY = "simruntime/Contents/Resources/RuntimeRoot"
42+
43+
# How long to sleep between checks in seconds
44+
SLEEP_DELAY = 5
45+
46+
# How often to print process info (in seconds)
47+
PRINT_PROCESSES_INTERVAL = 60
48+
49+
@dataclass
50+
class ProcessInfo:
51+
pid: int
52+
cpu_percent: float
53+
memory_percent: float
54+
name: str
55+
is_simulator: bool
56+
57+
@property
58+
def environment(self) -> str:
59+
return "Simulator" if self.is_simulator else "OS"
60+
61+
@property
62+
def output_string(self) -> str:
63+
return f"{self.pid}\t{self.cpu_percent}%\t{self.memory_percent}%\t{self.name}\t{self.environment}"
64+
65+
class ProcessSort(Enum):
66+
CPU = "cpu"
67+
MEMORY = "memory"
68+
69+
def get_processes(sort_by=ProcessSort.CPU):
70+
"""Get all processes using ps command - equivalent to Swift's proc_listallpids"""
71+
sorty_by = "-ero" if sort_by == ProcessSort.CPU else "-emo"
72+
result = subprocess.run(['ps', sorty_by, 'pid,pcpu,pmem,comm'],
73+
capture_output=True, text=True, check=True)
74+
processes = []
75+
76+
for line in result.stdout.splitlines()[1:]: # Skip header
77+
parts = line.strip().split(None, 3)
78+
if len(parts) >= 3:
79+
pid = int(parts[0])
80+
cpu_percent = float(parts[1])
81+
memory_percent = float(parts[2])
82+
name = parts[3]
83+
is_simulator = SIMULATOR_PATH_SEARCH_KEY in name
84+
processes.append(ProcessInfo(pid, cpu_percent, memory_percent, name, is_simulator))
85+
86+
return processes
87+
88+
def print_processes(processes, limit=-1):
89+
output = []
90+
output.append("================================")
91+
output.append("⚡️ Processes sorted by CPU usage:")
92+
output.append("PID\tCPU%\tMemory%\tName\tEnvironment")
93+
limit = len(processes) if limit == -1 else limit
94+
for p in processes[:limit]:
95+
output.append(p.output_string)
96+
97+
output.append("--------------------------------")
98+
output.append("🧠 Processes sorted by memory usage:")
99+
output.append("PID\tCPU%\tMemory%\tName\tEnvironment")
100+
processes_sorted_by_memory = sorted(processes, key=lambda x: x.memory_percent, reverse=True)
101+
for p in processes_sorted_by_memory[:limit]:
102+
output.append(p.output_string)
103+
104+
output.append("================================")
105+
print("\n".join(output))
106+
107+
def find_unwanted(processes):
108+
yeeting = []
109+
for p in processes:
110+
process_target_list = SIMULATOR_PROCESSES if p.is_simulator else OS_PROCESSES
111+
for k in process_target_list:
112+
if k in p.name:
113+
yeeting.append(p)
114+
return yeeting
115+
116+
def yeet(processes):
117+
output = []
118+
for p in processes:
119+
output.append(f"🤠 pyeetd: Stopping - {p.output_string}")
120+
os.killpg(p.pid, signal.SIGKILL)
121+
return output
122+
123+
def main():
124+
print_cycles = PRINT_PROCESSES_INTERVAL // SLEEP_DELAY
125+
i = 0
126+
while True:
127+
output = []
128+
processes = get_processes(ProcessSort.CPU)
129+
processes_to_yeet = find_unwanted(processes)
130+
output.extend(yeet(processes_to_yeet))
131+
output.append(f"🤠 {time.strftime('%Y-%m-%d %H:%M:%S')} - pyeetd {len(processes_to_yeet)} processes.")
132+
print("\n".join(output))
133+
if i % print_cycles == 0:
134+
print_processes(processes, 10)
135+
i += 1
136+
time.sleep(SLEEP_DELAY)
137+
138+
if __name__ == '__main__':
139+
main()

0 commit comments

Comments
 (0)