* Rename cmuxterm to cmux across entire codebase - Rename GitHub repos: manaflow-ai/cmuxterm -> manaflow-ai/cmux, manaflow-ai/homebrew-cmuxterm -> manaflow-ai/homebrew-cmux - Rename bundle IDs: com.cmuxterm.app -> com.cmux.app - Rename CLI: CLI/cmuxterm.swift -> CLI/cmux.swift - Rename homebrew submodule: homebrew-cmuxterm -> homebrew-cmux - Update all socket paths: /tmp/cmuxterm*.sock -> /tmp/cmux*.sock - Update all GitHub URLs, DMG names, Sparkle URLs - Update all source files, scripts, tests, docs, CI workflows * Bump version to 1.23.0
211 lines
6.1 KiB
Python
211 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CPU usage test for cmux.
|
|
|
|
This test monitors cmux's CPU usage during idle periods to catch
|
|
performance regressions like runaway animations or continuous view updates.
|
|
|
|
Run this test after launching cmux:
|
|
python3 tests/test_cpu_usage.py
|
|
|
|
The test will fail if:
|
|
- CPU usage exceeds 15% during idle (no user interaction)
|
|
- The sample shows suspicious patterns (continuous body.getter calls, animations)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import re
|
|
import os
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
# Allow importing tests/cmux.py when running from repo root.
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from cmux import cmux
|
|
|
|
|
|
# Maximum acceptable CPU usage during idle (percentage)
|
|
MAX_IDLE_CPU_PERCENT = 15.0
|
|
|
|
# How long to wait for app to settle before measuring (seconds)
|
|
SETTLE_TIME = 2.0
|
|
|
|
# Duration to monitor CPU usage (seconds)
|
|
MONITOR_DURATION = 3.0
|
|
|
|
# Sampling interval for CPU checks (seconds)
|
|
SAMPLE_INTERVAL = 0.5
|
|
|
|
# Patterns that indicate performance issues in sample output
|
|
SUSPICIOUS_PATTERNS = [
|
|
r"body\.getter.*\d{3,}", # View body getter called 100+ times
|
|
r"repeatForever", # Runaway animations
|
|
r"TimelineView.*animation.*\d{3,}", # Unpaused timeline views
|
|
]
|
|
|
|
|
|
def get_cmux_pid() -> Optional[int]:
|
|
"""Get the PID of the running cmux process."""
|
|
socket_path = os.environ.get("CMUX_SOCKET_PATH")
|
|
if not socket_path:
|
|
try:
|
|
socket_path = cmux().socket_path
|
|
except Exception:
|
|
socket_path = None
|
|
|
|
if socket_path and os.path.exists(socket_path):
|
|
result = subprocess.run(
|
|
["lsof", "-t", socket_path],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
for line in result.stdout.strip().split("\n"):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
pid = int(line)
|
|
except ValueError:
|
|
continue
|
|
if pid != os.getpid():
|
|
return pid
|
|
|
|
result = subprocess.run(
|
|
["pgrep", "-f", r"cmux\.app/Contents/MacOS/cmux$"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
# Try DEV build
|
|
result = subprocess.run(
|
|
["pgrep", "-f", r"cmux DEV\.app/Contents/MacOS/cmux"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
return None
|
|
pids = result.stdout.strip().split("\n")
|
|
return int(pids[0]) if pids and pids[0] else None
|
|
|
|
|
|
def get_cpu_usage(pid: int) -> float:
|
|
"""Get current CPU usage percentage for a process."""
|
|
result = subprocess.run(
|
|
["ps", "-p", str(pid), "-o", "%cpu="],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
return 0.0
|
|
try:
|
|
return float(result.stdout.strip())
|
|
except ValueError:
|
|
return 0.0
|
|
|
|
|
|
def sample_process(pid: int, duration: int = 2) -> str:
|
|
"""Sample a process and return the output."""
|
|
result = subprocess.run(
|
|
["sample", str(pid), str(duration)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return result.stdout + result.stderr
|
|
|
|
|
|
def check_sample_for_issues(sample_output: str) -> List[str]:
|
|
"""Check sample output for suspicious patterns."""
|
|
issues = []
|
|
for pattern in SUSPICIOUS_PATTERNS:
|
|
if re.search(pattern, sample_output):
|
|
issues.append(f"Found suspicious pattern: {pattern}")
|
|
return issues
|
|
|
|
|
|
def monitor_cpu_usage(pid: int, duration: float, interval: float) -> List[float]:
|
|
"""Monitor CPU usage over a period and return all readings."""
|
|
readings = []
|
|
start = time.time()
|
|
while time.time() - start < duration:
|
|
cpu = get_cpu_usage(pid)
|
|
readings.append(cpu)
|
|
time.sleep(interval)
|
|
return readings
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print("cmux CPU Usage Test")
|
|
print("=" * 60)
|
|
|
|
# Find cmux process
|
|
pid = get_cmux_pid()
|
|
if pid is None:
|
|
print("\n❌ SKIP: cmux is not running")
|
|
print("Start cmux and run this test again.")
|
|
return 0 # Not a failure, just skip
|
|
|
|
print(f"\nFound cmux process: PID {pid}")
|
|
|
|
# Wait for app to settle
|
|
print(f"Waiting {SETTLE_TIME}s for app to settle...")
|
|
time.sleep(SETTLE_TIME)
|
|
|
|
# Monitor CPU usage
|
|
print(f"Monitoring CPU usage for {MONITOR_DURATION}s...")
|
|
readings = monitor_cpu_usage(pid, MONITOR_DURATION, SAMPLE_INTERVAL)
|
|
|
|
avg_cpu = sum(readings) / len(readings) if readings else 0
|
|
max_cpu = max(readings) if readings else 0
|
|
min_cpu = min(readings) if readings else 0
|
|
|
|
print(f"\nCPU Usage Results:")
|
|
print(f" Average: {avg_cpu:.1f}%")
|
|
print(f" Max: {max_cpu:.1f}%")
|
|
print(f" Min: {min_cpu:.1f}%")
|
|
print(f" Samples: {len(readings)}")
|
|
|
|
# Check if CPU is too high
|
|
if avg_cpu > MAX_IDLE_CPU_PERCENT:
|
|
print(f"\n❌ FAIL: Average CPU ({avg_cpu:.1f}%) exceeds threshold ({MAX_IDLE_CPU_PERCENT}%)")
|
|
|
|
# Take a sample to diagnose
|
|
print("\nTaking process sample for diagnosis...")
|
|
sample_output = sample_process(pid, 2)
|
|
|
|
# Check for known issues
|
|
issues = check_sample_for_issues(sample_output)
|
|
if issues:
|
|
print("\nDiagnostic findings:")
|
|
for issue in issues:
|
|
print(f" - {issue}")
|
|
|
|
# Save sample for debugging
|
|
sample_file = Path(f"/tmp/cmux_cpu_test_sample_{pid}.txt")
|
|
sample_file.write_text(sample_output)
|
|
print(f"\nFull sample saved to: {sample_file}")
|
|
|
|
# Show top functions from sample
|
|
print("\nTop functions in sample (look for .body.getter or Animation):")
|
|
lines = sample_output.split("\n")
|
|
relevant_lines = [
|
|
l for l in lines
|
|
if "cmux" in l and ("body" in l or "Animation" in l or "Timer" in l)
|
|
][:10]
|
|
for line in relevant_lines:
|
|
print(f" {line.strip()[:100]}")
|
|
|
|
return 1
|
|
|
|
print(f"\n✅ PASS: CPU usage is within acceptable range")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|