From 2c23d15eb7af2e3ef8bbf5c18da64543881c6e5f Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 26 Dec 2025 18:25:38 +0800 Subject: [PATCH] Clean performance speed optimization --- bin/clean.sh | 19 +--------- lib/clean/brew.sh | 71 ++++++++++++++++++++++++++--------- lib/clean/caches.sh | 56 +++++++++++++++++++++++++++ lib/clean/user.sh | 45 +++++++++++++++++----- tests/system_maintenance.bats | 8 ++++ 5 files changed, 154 insertions(+), 45 deletions(-) diff --git a/bin/clean.sh b/bin/clean.sh index 43506b4..5a810f4 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -238,7 +238,7 @@ safe_clean() { # Show progress indicator for potentially slow operations if [[ ${#existing_paths[@]} -gt 3 ]]; then local total_paths=${#existing_paths[@]} - if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $total_paths items..."; fi + if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning items..."; fi local temp_dir # create_temp_dir uses mktemp -d for secure temporary directory creation temp_dir=$(create_temp_dir) @@ -269,11 +269,6 @@ safe_clean() { wait "${pids[0]}" 2> /dev/null || true pids=("${pids[@]:1}") ((completed++)) - # Update progress less frequently to reduce overhead - if [[ -t 1 ]] && ((completed % 20 == 0)); then - stop_inline_spinner - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning items ($completed/$total_paths)..." - fi fi done @@ -290,11 +285,6 @@ safe_clean() { read -r size count < "$result_file" 2> /dev/null || true if [[ "$count" -gt 0 && "$size" -gt 0 ]]; then if [[ "$DRY_RUN" != "true" ]]; then - # Update spinner to show cleaning progress - if [[ -t 1 ]] && ((idx % 5 == 0)); then - stop_inline_spinner - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Cleaning items ($idx/$total_paths)..." - fi # Handle symbolic links separately (only remove the link, not the target) if [[ -L "$path" ]]; then rm "$path" 2> /dev/null || true @@ -314,7 +304,7 @@ safe_clean() { else # Show progress for small batches too (simpler jobs) local total_paths=${#existing_paths[@]} - if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $total_paths items..."; fi + if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning items..."; fi local idx=0 for path in "${existing_paths[@]}"; do @@ -324,11 +314,6 @@ safe_clean() { # Optimization: Skip expensive file counting if [[ "$size_bytes" -gt 0 ]]; then if [[ "$DRY_RUN" != "true" ]]; then - # Update spinner to show cleaning progress for slow operations - if [[ -t 1 ]]; then - stop_inline_spinner - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Cleaning $description..." - fi # Handle symbolic links separately (only remove the link, not the target) if [[ -L "$path" ]]; then rm "$path" 2> /dev/null || true diff --git a/lib/clean/brew.sh b/lib/clean/brew.sh index 130b76d..b84f240 100644 --- a/lib/clean/brew.sh +++ b/lib/clean/brew.sh @@ -1,7 +1,7 @@ #!/bin/bash # Clean Homebrew caches and remove orphaned dependencies -# Skips if run within 2 days, runs cleanup/autoremove in parallel with 120s timeout +# Skips if run within 7 days, runs cleanup/autoremove in parallel with 120s timeout # Env: MO_BREW_TIMEOUT, DRY_RUN clean_homebrew() { command -v brew > /dev/null 2>&1 || return 0 @@ -12,9 +12,10 @@ clean_homebrew() { return 0 fi - # Smart caching: check if brew cleanup was run recently (within 2 days) + # Smart caching: check if brew cleanup was run recently (within 7 days) + # Extended from 2 days to 7 days to reduce cleanup frequency local brew_cache_file="${HOME}/.cache/mole/brew_last_cleanup" - local cache_valid_days=2 + local cache_valid_days=7 local should_skip=false if [[ -f "$brew_cache_file" ]]; then @@ -33,35 +34,62 @@ clean_homebrew() { [[ "$should_skip" == "true" ]] && return 0 + # Quick pre-check: determine if cleanup is needed based on cache size (<50MB) + # Use timeout to prevent slow du on very large caches + # If timeout occurs, assume cache is large and run cleanup + local skip_cleanup=false + local brew_cache_size=0 + if [[ -d ~/Library/Caches/Homebrew ]]; then + brew_cache_size=$(run_with_timeout 3 du -sk ~/Library/Caches/Homebrew 2>/dev/null | awk '{print $1}') + local du_exit=$? + + # Skip cleanup (but still run autoremove) if cache is small + if [[ $du_exit -eq 0 && -n "$brew_cache_size" && "$brew_cache_size" -lt 51200 ]]; then + skip_cleanup=true + fi + fi + + # Display appropriate spinner message if [[ -t 1 ]]; then - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew cleanup and autoremove..." + if [[ "$skip_cleanup" == "true" ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew autoremove (cleanup skipped)..." + else + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew cleanup and autoremove..." + fi fi local timeout_seconds=${MO_BREW_TIMEOUT:-120} - # Run brew cleanup and autoremove in parallel for performance + # Run brew cleanup and/or autoremove based on cache size local brew_tmp_file autoremove_tmp_file - brew_tmp_file=$(create_temp_file) + local brew_pid autoremove_pid + + if [[ "$skip_cleanup" == "false" ]]; then + brew_tmp_file=$(create_temp_file) + (brew cleanup > "$brew_tmp_file" 2>&1) & + brew_pid=$! + fi + autoremove_tmp_file=$(create_temp_file) - - (brew cleanup > "$brew_tmp_file" 2>&1) & - local brew_pid=$! - (brew autoremove > "$autoremove_tmp_file" 2>&1) & - local autoremove_pid=$! + autoremove_pid=$! local elapsed=0 local brew_done=false local autoremove_done=false + # Mark cleanup as done if it was skipped + [[ "$skip_cleanup" == "true" ]] && brew_done=true + # Wait for both to complete or timeout while [[ "$brew_done" == "false" ]] || [[ "$autoremove_done" == "false" ]]; do if [[ $elapsed -ge $timeout_seconds ]]; then - kill -TERM $brew_pid $autoremove_pid 2> /dev/null || true + [[ -n "$brew_pid" ]] && kill -TERM $brew_pid 2> /dev/null || true + kill -TERM $autoremove_pid 2> /dev/null || true break fi - kill -0 $brew_pid 2> /dev/null || brew_done=true + [[ -n "$brew_pid" ]] && { kill -0 $brew_pid 2> /dev/null || brew_done=true; } kill -0 $autoremove_pid 2> /dev/null || autoremove_done=true sleep 1 @@ -70,8 +98,10 @@ clean_homebrew() { # Wait for processes to finish local brew_success=false - if wait $brew_pid 2> /dev/null; then - brew_success=true + if [[ "$skip_cleanup" == "false" && -n "$brew_pid" ]]; then + if wait $brew_pid 2> /dev/null; then + brew_success=true + fi fi local autoremove_success=false @@ -82,7 +112,11 @@ clean_homebrew() { if [[ -t 1 ]]; then stop_inline_spinner; fi # Process cleanup output and extract metrics - if [[ "$brew_success" == "true" && -f "$brew_tmp_file" ]]; then + if [[ "$skip_cleanup" == "true" ]]; then + # Cleanup was skipped due to small cache size + local size_mb=$((brew_cache_size / 1024)) + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew cleanup (cache ${size_mb}MB, skipped)" + elif [[ "$brew_success" == "true" && -f "$brew_tmp_file" ]]; then local brew_output brew_output=$(cat "$brew_tmp_file" 2> /dev/null || echo "") local removed_count freed_space @@ -114,8 +148,9 @@ clean_homebrew() { echo -e " ${YELLOW}${ICON_WARNING}${NC} Autoremove timed out (run ${GRAY}brew autoremove${NC} manually)" fi - # Update cache timestamp on successful completion - if [[ "$brew_success" == "true" || "$autoremove_success" == "true" ]]; then + # Update cache timestamp on successful completion or when cleanup was intelligently skipped + # This prevents repeated cache size checks within the 7-day window + if [[ "$skip_cleanup" == "true" ]] || [[ "$brew_success" == "true" ]] || [[ "$autoremove_success" == "true" ]]; then ensure_user_file "$brew_cache_file" date +%s > "$brew_cache_file" fi diff --git a/lib/clean/caches.sh b/lib/clean/caches.sh index b6755df..51c1468 100644 --- a/lib/clean/caches.sh +++ b/lib/clean/caches.sh @@ -125,6 +125,62 @@ clean_service_worker_cache() { # Clean Next.js (.next/cache) and Python (__pycache__) build caches # Uses maxdepth 3, excludes Library/.Trash/node_modules, 10s timeout per scan clean_project_caches() { + # Quick check: skip if user likely doesn't have development projects + local has_dev_projects=false + local -a common_dev_dirs=( + "$HOME/Code" + "$HOME/Projects" + "$HOME/workspace" + "$HOME/github" + "$HOME/dev" + "$HOME/work" + "$HOME/src" + "$HOME/repos" + "$HOME/Development" + "$HOME/www" + "$HOME/golang" + "$HOME/go" + "$HOME/rust" + "$HOME/python" + "$HOME/ruby" + "$HOME/java" + "$HOME/dotnet" + "$HOME/node" + ) + + for dir in "${common_dev_dirs[@]}"; do + if [[ -d "$dir" ]]; then + has_dev_projects=true + break + fi + done + + # If no common dev directories found, perform feature-based detection + # Check for project markers in $HOME (node_modules, .git, target, etc.) + if [[ "$has_dev_projects" == "false" ]]; then + local -a project_markers=( + "node_modules" + ".git" + "target" + "go.mod" + "Cargo.toml" + "package.json" + "pom.xml" + "build.gradle" + ) + + for marker in "${project_markers[@]}"; do + # Quick check with maxdepth 2 and 3s timeout to avoid slow scans + if run_with_timeout 3 sh -c "find '$HOME' -maxdepth 2 -name '$marker' -not -path '*/Library/*' -not -path '*/.Trash/*' 2>/dev/null | head -1" | grep -q .; then + has_dev_projects=true + break + fi + done + + # If still no dev projects found, skip scanning + [[ "$has_dev_projects" == "false" ]] && return 0 + fi + if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Searching project caches..." diff --git a/lib/clean/user.sh b/lib/clean/user.sh index f4d4545..3971df6 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -14,8 +14,9 @@ clean_user_essentials() { scan_external_volumes() { [[ -d "/Volumes" ]] || return 0 - # Fast pre-check: count non-system external volumes without expensive operations + # Fast pre-check: collect non-system external volumes and detect network volumes local -a candidate_volumes=() + local -a network_volumes=() for volume in /Volumes/*; do # Basic checks (directory, writable, not a symlink) [[ -d "$volume" && -w "$volume" && ! -L "$volume" ]] || continue @@ -23,26 +24,50 @@ scan_external_volumes() { # Skip system root if it appears in /Volumes [[ "$volume" == "/" || "$volume" == "/Volumes/Macintosh HD" ]] && continue + # Use diskutil to intelligently detect network volumes (SMB/NFS/AFP) + # Timeout protection: 1s per volume to avoid slow network responses + local protocol="" + protocol=$(run_with_timeout 1 command diskutil info "$volume" 2>/dev/null | grep -i "Protocol:" | awk '{print $2}' || echo "") + + case "$protocol" in + SMB | NFS | AFP | CIFS | WebDAV) + network_volumes+=("$volume") + continue + ;; + esac + + # Fallback: Check filesystem type via df if diskutil didn't identify protocol + local fs_type="" + fs_type=$(run_with_timeout 1 command df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}' || echo "") + case "$fs_type" in + nfs | smbfs | afpfs | cifs | webdav) + network_volumes+=("$volume") + continue + ;; + esac + candidate_volumes+=("$volume") done # If no external volumes found, return immediately (zero overhead) local volume_count=${#candidate_volumes[@]} - [[ $volume_count -eq 0 ]] && return 0 + local network_count=${#network_volumes[@]} - # We have external volumes, now perform full scan + if [[ $volume_count -eq 0 ]]; then + # Show info if network volumes were skipped + if [[ $network_count -gt 0 ]]; then + echo -e " ${GRAY}→${NC} External volumes (${network_count} network volume(s) skipped)" + note_activity + fi + return 0 + fi + + # We have local external volumes, now perform full scan if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $volume_count external volume(s)..." fi for volume in "${candidate_volumes[@]}"; do - # Skip network volumes with short timeout (reduced from 2s to 1s) - local fs_type="" - fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "unknown") - case "$fs_type" in - nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;; - esac - # Verify volume is actually mounted (reduced timeout from 2s to 1s) run_with_timeout 1 mount | grep -q "on $volume " || continue diff --git a/tests/system_maintenance.bats b/tests/system_maintenance.bats index 8728e26..f7d7315 100644 --- a/tests/system_maintenance.bats +++ b/tests/system_maintenance.bats @@ -128,10 +128,15 @@ source "$PROJECT_ROOT/lib/clean/brew.sh" mkdir -p "$HOME/.cache/mole" rm -f "$HOME/.cache/mole/brew_last_cleanup" +# Create a large enough Homebrew cache to pass pre-check (>50MB) +mkdir -p "$HOME/Library/Caches/Homebrew" +dd if=/dev/zero of="$HOME/Library/Caches/Homebrew/test.tar.gz" bs=1024 count=51200 2>/dev/null + MO_BREW_TIMEOUT=2 start_inline_spinner(){ :; } stop_inline_spinner(){ :; } +note_activity(){ :; } brew() { case "$1" in @@ -150,6 +155,9 @@ brew() { } clean_homebrew + +# Cleanup test files +rm -rf "$HOME/Library/Caches/Homebrew" EOF [ "$status" -eq 0 ]