Skip to content

Instantly share code, notes, and snippets.

@nicholasg-dev
Created April 23, 2025 16:26
Show Gist options
  • Select an option

  • Save nicholasg-dev/c30c80e7abf24a1b164af5e15499da28 to your computer and use it in GitHub Desktop.

Select an option

Save nicholasg-dev/c30c80e7abf24a1b164af5e15499da28 to your computer and use it in GitHub Desktop.
cat periodic_cleanup.sh
#!/bin/bash
# periodic_cleanup.sh
#
# Description:
# This script performs periodic memory optimization tasks on macOS, including
# monitoring and cleaning browser caches, alerting on high memory pressure,
# and logging memory usage statistics.
#
# Setup:
# 1. Make script executable: chmod +x periodic_cleanup.sh
# 2. Create launchd plist: ./periodic_cleanup.sh --create-launchd
# 3. Load the launchd job: launchctl load ~/Library/LaunchAgents/com.nicholas.periodic-cleanup.plist
#
# Author: Nicholas
# Date: $(date +%Y-%m-%d)
# Configuration
USER_HOME="$HOME"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
# Paths
LOG_DIR="$USER_HOME/Library/Logs/MemoryOptimization"
LOG_FILE="$LOG_DIR/memory_cleanup.log"
STATS_FILE="$LOG_DIR/memory_stats.csv"
BROWSER_CACHE_DIR="$USER_HOME/Library/Caches"
CONFIG_FILE="$USER_HOME/.memory_cleanup_config"
# Thresholds
FREE_MEMORY_THRESHOLD=1024 # MB - Alert if free memory falls below this
SWAP_USAGE_THRESHOLD=80 # % - Alert if swap usage exceeds this percentage
BROWSER_CACHE_THRESHOLD=500 # MB - Clean browser caches if they exceed this size
# Protected process list - these processes will never be suspended or killed
EXCLUDED_PROCESSES=(
# Development IDEs and editors
"VSCode"
"Visual Studio Code"
"Code"
"Code Helper"
"code-oss"
"Windsurf"
"Cursor"
"Atom"
"Sublime Text"
"IntelliJ"
"PyCharm"
"WebStorm"
"Eclipse"
"Xcode"
"Android Studio"
# IDE services and language servers
"language_server"
"tsserver"
"node" # When used by IDEs
"java" # When used by IDEs
"gopls"
"rust-analyzer"
"jdt.ls"
"pyright"
"pylsp"
"clangd"
# Development tools
"git"
"npm"
"yarn"
"docker"
"iTermx2"
"Terminal"
"iTerm2"
"Warp"
"postgresql"
"mysql"
"mongod"
"redis-server"
)
# Memory optimization script paths
MEMORY_MONITOR="$SCRIPT_DIR/memory_monitor.sh"
OPTIMIZE_MEMORY="$SCRIPT_DIR/optimize_memory.sh"
OPTIMIZE_BROWSERS="$SCRIPT_DIR/optimize_browsers.sh"
# Default configuration
AUTO_CLEAN_BROWSER_CACHE=true
AUTO_PURGE_INACTIVE_MEMORY=true
NOTIFY_ON_HIGH_MEMORY_PRESSURE=true
RUN_INTERVAL=3600 # 1 hour in seconds
# Ensure log directory exists
mkdir -p "$LOG_DIR"
# Load configuration if exists
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# -------------------------------------------
# Utility Functions
# -------------------------------------------
# Get current timestamp
get_timestamp() {
date +"%Y-%m-%d %H:%M:%S"
}
# Log message with timestamp
log_message() {
local timestamp=$(get_timestamp)
echo "[$timestamp] $1" >> "$LOG_FILE"
# Also print to console if running in terminal
if [ -t 1 ]; then
echo "[$timestamp] $1"
fi
}
# Send notification
send_notification() {
local title="$1"
local message="$2"
# Use osascript for notifications
osascript -e "display notification \"$message\" with title \"$title\""
# Log the notification
log_message "NOTIFICATION: $title - $message"
}
# Get free memory in MB
get_free_memory() {
local vm_stats=$(vm_stat)
local page_size=$(vm_stat | grep "page size of" | grep -o '[0-9]\+')
local pages_free=$(echo "$vm_stats" | grep "Pages free" | awk '{print $3}' | sed 's/\.//')
local pages_speculative=$(echo "$vm_stats" | grep "Pages speculative" | awk '{print $3}' | sed 's/\.//')
# Calculate free memory in MB
echo $(( (pages_free + pages_speculative) * page_size / 1024 / 1024 ))
}
# Get swap usage percentage
get_swap_usage_percent() {
local swap_info=$(sysctl vm.swapusage | awk '{print $2 $3 $4 $5 $6 $7}')
local total_swap=$(echo "$swap_info" | grep -o 'total=[0-9]*\.[0-9]*M' | grep -o '[0-9]*\.[0-9]*')
local used_swap=$(echo "$swap_info" | grep -o 'used=[0-9]*\.[0-9]*M' | grep -o '[0-9]*\.[0-9]*')
# Calculate percentage
if [ -n "$total_swap" ] && [ -n "$used_swap" ] && [ $(echo "$total_swap > 0" | bc) -eq 1 ]; then
echo "scale=0; ($used_swap * 100) / $total_swap" | bc
else
echo "0"
fi
}
# Get total browser cache size in MB
get_browser_cache_size() {
local total_size=0
# Chrome cache
if [ -d "$BROWSER_CACHE_DIR/Google/Chrome" ]; then
local chrome_size=$(du -sm "$BROWSER_CACHE_DIR/Google/Chrome" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + chrome_size))
fi
# Safari cache
if [ -d "$BROWSER_CACHE_DIR/com.apple.Safari" ]; then
local safari_size=$(du -sm "$BROWSER_CACHE_DIR/com.apple.Safari" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + safari_size))
fi
# Firefox cache
if [ -d "$BROWSER_CACHE_DIR/Firefox" ]; then
local firefox_size=$(du -sm "$BROWSER_CACHE_DIR/Firefox" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + firefox_size))
fi
# Brave cache
if [ -d "$BROWSER_CACHE_DIR/com.brave.Browser" ]; then
local brave_size=$(du -sm "$BROWSER_CACHE_DIR/com.brave.Browser" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + brave_size))
fi
# Edge cache
if [ -d "$BROWSER_CACHE_DIR/com.microsoft.Edge" ]; then
local edge_size=$(du -sm "$BROWSER_CACHE_DIR/com.microsoft.Edge" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + edge_size))
fi
echo "$total_size"
}
# Check if a process is running
is_process_running() {
pgrep -q "$1"
return $?
}
# Log memory statistics to CSV
log_memory_stats() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local free_mem=$(get_free_memory)
local swap_percent=$(get_swap_usage_percent)
local browser_cache_size=$(get_browser_cache_size)
# Get VM stats for pageins/pageouts
local vm_stats=$(vm_stat)
local pageins=$(echo "$vm_stats" | grep "Pageins" | awk '{print $2}' | sed 's/\.//')
local pageouts=$(echo "$vm_stats" | grep "Pageouts" | awk '{print $2}' | sed 's/\.//')
# Get total system memory
local total_memory=$(sysctl hw.memsize | awk '{print $2}' | awk '{printf "%.0f", $1/1024/1024}')
# Create header if file doesn't exist
if [ ! -f "$STATS_FILE" ]; then
echo "Timestamp,FreeMemory_MB,TotalMemory_MB,SwapUsage_Percent,BrowserCache_MB,Pageins,Pageouts" > "$STATS_FILE"
fi
# Log stats
echo "$timestamp,$free_mem,$total_memory,$swap_percent,$browser_cache_size,$pageins,$pageouts" >> "$STATS_FILE"
}
# -------------------------------------------
# Cleanup Functions
# -------------------------------------------
# Check memory pressure and take action if needed
check_memory_pressure() {
log_message "Checking memory pressure..."
local free_mem=$(get_free_memory)
local swap_percent=$(get_swap_usage_percent)
log_message "Free memory: ${free_mem}MB, Swap usage: ${swap_percent}%"
# Check if memory pressure is high
if [ "$free_mem" -lt "$FREE_MEMORY_THRESHOLD" ] || [ "$swap_percent" -gt "$SWAP_USAGE_THRESHOLD" ]; then
log_message "High memory pressure detected!"
# Send notification if enabled
if [ "$NOTIFY_ON_HIGH_MEMORY_PRESSURE" = true ]; then
if [ "$free_mem" -lt "$FREE_MEMORY_THRESHOLD" ]; then
send_notification "Low Memory Warning" "Free memory is only ${free_mem}MB"
fi
if [ "$swap_percent" -gt "$SWAP_USAGE_THRESHOLD" ]; then
send_notification "High Swap Usage" "Swap usage is at ${swap_percent}%"
fi
fi
# Take action to reduce memory pressure
if [ "$AUTO_PURGE_INACTIVE_MEMORY" = true ]; then
purge_inactive_memory
fi
else
log_message "Memory pressure is normal."
fi
}
# Clean browser caches if they exceed threshold
clean_browser_caches() {
if [ "$AUTO_CLEAN_BROWSER_CACHE" != true ]; then
return
fi
log_message "Checking browser cache sizes..."
local cache_size=$(get_browser_cache_size)
if [ "$cache_size" -gt "$BROWSER_CACHE_THRESHOLD" ]; then
log_message "Browser caches exceed threshold (${cache_size}MB > ${BROWSER_CACHE_THRESHOLD}MB). Cleaning..."
# Use optimize_browsers.sh if available
if [ -f "$OPTIMIZE_BROWSERS" ]; then
"$OPTIMIZE_BROWSERS" clean
log_message "Browser caches cleaned using optimize_browsers.sh"
else
# Fall back to basic cache cleaning
log_message "optimize_browsers.sh not found, using basic cleaning..."
# Chrome cache
if [ -d "$BROWSER_CACHE_DIR/Google/Chrome/Default/Cache" ]; then
rm -rf "$BROWSER_CACHE_DIR/Google/Chrome/Default/Cache"/* 2>/dev/null
log_message "Cleared Chrome cache"
fi
# Safari cache
if [ -d "$BROWSER_CACHE_DIR/com.apple.Safari/Cache" ]; then
rm -rf "$BROWSER_CACHE_DIR/com.apple.Safari/Cache"/* 2>/dev/null
log_message "Cleared Safari cache"
fi
# Firefox cache
if [ -d "$BROWSER_CACHE_DIR/Firefox" ]; then
find "$BROWSER_CACHE_DIR/Firefox" -name "Cache" -exec rm -rf {}/* 2>/dev/null \;
log_message "Cleared Firefox cache"
fi
# Brave cache
if [ -d "$BROWSER_CACHE_DIR/com.brave.Browser/Default/Cache" ]; then
rm -rf "$BROWSER_CACHE_DIR/com.brave.Browser/Default/Cache"/* 2>/dev/null
log_message "Cleared Brave cache"
fi
fi
else
log_message "Browser cache size (${cache_size}MB) is below threshold."
fi
}
# Purge inactive memory
purge_inactive_memory() {
log_message "Purging inactive memory..."
# Use optimize_memory.sh if available
if [ -f "$OPTIMIZE_MEMORY" ]; then
"$OPTIMIZE_MEMORY" -y
log_message "Memory optimized using optimize_memory.sh"
else
# Fall back to basic purge
log_message "optimize_memory.sh not found, using basic purge..."
sudo purge
log_message "Executed purge command"
fi
}
# Run periodic cleanup
run_cleanup() {
log_message "Starting periodic cleanup..."
# Log memory stats
log_memory_stats
# Check memory pressure
check_memory_pressure
# Clean browser caches if needed
clean_browser_caches
log_message "Periodic cleanup completed."
}
# Create launchd plist file
create_launchd_plist() {
local plist_path="$USER_HOME/Library/LaunchAgents/com.nicholas.periodic-cleanup.plist"
log_message "Creating launchd plist at $plist_path..."
cat > "$plist_path" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.nicholas.periodic-cleanup</string>
<key>ProgramArguments</key>
<array>
<string>$SCRIPT_PATH</string>
<string>--run</string>
</array>
<key>StartInterval</key>
<integer>$RUN_INTERVAL</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>$LOG_DIR/periodic_cleanup_error.log</string>
<key>StandardOutPath</key>
<string>$LOG_DIR/periodic_cleanup_output.log</string>
</dict>
</plist>
EOF
log_message "Launchd plist created. To load it, run:"
log_message "launchctl load $plist_path"
}
# Create a temporary directory for operations
setup_temp_dir() {
local tmp_dir=$(mktemp -d "${TMPDIR:-/tmp/}memory_cleanup.XXXXXX")
echo "$tmp_dir"
}
# Clean up temporary files
cleanup_temp_files() {
local tmp_dir="$1"
if [ -d "$tmp_dir" ]; then
rm -rf "$tmp_dir"
fi
}
# Function to check if a process should be protected from cleanup
is_protected_process() {
local process_name="$1"
local process_cmd="$2"
# Check if process name matches any excluded pattern
for pattern in "${EXCLUDED_PROCESSES[@]}"; do
if [[ "$process_name" == *"$pattern"* ]] || [[ "$process_cmd" == *"$pattern"* ]]; then
return 0 # Process should be protected
fi
done
# Additional check for IDE-related node processes
if [[ "$process_name" == "node" || "$process_cmd" == *"node"* ]]; then
# Check if this node process is part of an IDE
if [[ "$process_cmd" == *"vscode"* ]] ||
[[ "$process_cmd" == *"windsurf"* ]] ||
[[ "$process_cmd" == *"cursor"* ]] ||
[[ "$process_cmd" == *"extension"* ]] ||
[[ "$process_cmd" == *"language_server"* ]]; then
return 0 # It's an IDE-related node process
fi
fi
return 1 # Not a protected process
}
# Interactive configuration
configure() {
echo "Memory Cleanup Configuration Utility"
echo "===================================="
echo
show_config
echo
read -p "Enter new Free Memory Threshold in MB [$FREE_MEMORY_THRESHOLD]: " input
[ -n "$input" ] && FREE_MEMORY_THRESHOLD=$input
read -p "Enter new Swap Usage Threshold in % [$SWAP_USAGE_THRESHOLD]: " input
[ -n "$input" ] && SWAP_USAGE_THRESHOLD=$input
read -p "Enter new Browser Cache Threshold in MB [$BROWSER_CACHE_THRESHOLD]: " input
[ -n "$input" ] && BROWSER_CACHE_THRESHOLD=$input
read -p "Auto Clean Browser Cache? (true/false) [$AUTO_CLEAN_BROWSER_CACHE]: " input
[ -n "$input" ] && AUTO_CLEAN_BROWSER_CACHE=$input
read -p "Auto Purge Inactive Memory? (true/false) [$AUTO_PURGE_INACTIVE_MEMORY]: " input
[ -n "$input" ] && AUTO_PURGE_INACTIVE_MEMORY=$input
read -p "Notify on High Memory Pressure? (true/false) [$NOTIFY_ON_HIGH_MEMORY_PRESSURE]: " input
[ -n "$input" ] && NOTIFY_ON_HIGH_MEMORY_PRESSURE=$input
read -p "Run Interval in seconds [$RUN_INTERVAL]: " input
[ -n "$input" ] && RUN_INTERVAL=$input
save_config
echo "Configuration updated."
}
# Print help message
print_help() {
echo "Usage: $0 [OPTION]"
echo "Periodic memory cleanup and optimization utility."
echo
echo "Options:"
echo " --run Run cleanup once"
echo " --config Configure settings interactively"
echo " --create-launchd Create launchd plist for automatic runs"
echo " --install Make script executable and install launchd job"
echo " --uninstall Remove launchd job"
echo " --show-config Show current configuration"
echo " --show-excluded Show list of protected processes"
echo " --stats Show memory statistics"
echo " --help Display this help and exit"
echo
echo "Examples:"
echo " $0 --run Run cleanup once immediately"
echo " $0 --install Install script for automatic cleanup"
echo " $0 --config Configure cleanup thresholds and settings"
echo " $0 --show-excluded Show list of protected development processes"
}
# Show configuration
show_config() {
echo "Current Configuration:"
echo "----------------------"
echo "Free Memory Threshold: ${FREE_MEMORY_THRESHOLD}MB"
echo "Swap Usage Threshold: ${SWAP_USAGE_THRESHOLD}%"
echo "Browser Cache Threshold: ${BROWSER_CACHE_THRESHOLD}MB"
echo "Auto Clean Browser Cache: $AUTO_CLEAN_BROWSER_CACHE"
echo "Auto Purge Inactive Memory: $AUTO_PURGE_INACTIVE_MEMORY"
echo "Notify on High Memory Pressure: $NOTIFY_ON_HIGH_MEMORY_PRESSURE"
echo "Run Interval: ${RUN_INTERVAL} seconds"
echo "Log Directory: $LOG_DIR"
echo "Protection: Development IDEs and tools are protected from optimization"
}
# Save configuration to file
save_config() {
cat > "$CONFIG_FILE" << EOF
# Memory Cleanup Configuration
FREE_MEMORY_THRESHOLD=$FREE_MEMORY_THRESHOLD
SWAP_USAGE_THRESHOLD=$SWAP_USAGE_THRESHOLD
BROWSER_CACHE_THRESHOLD=$BROWSER_CACHE_THRESHOLD
AUTO_CLEAN_BROWSER_CACHE=$AUTO_CLEAN_BROWSER_CACHE
AUTO_PURGE_INACTIVE_MEMORY=$AUTO_PURGE_INACTIVE_MEMORY
NOTIFY_ON_HIGH_MEMORY_PRESSURE=$NOTIFY_ON_HIGH_MEMORY_PRESSURE
RUN_INTERVAL=$RUN_INTERVAL
EOF
log_message "Configuration saved to $CONFIG_FILE"
}
# Handle cleanup on script termination
cleanup_and_exit() {
local exit_code=${1:-0}
# Clean up any temporary files here
if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
cleanup_temp_files "$TEMP_DIR"
fi
log_message "Script exiting with code $exit_code"
exit $exit_code
}
# Handle signals
handle_signal() {
local signal=$1
log_message "Received signal: $signal"
cleanup_and_exit 1
}
# Install the script
install_script() {
# Make script executable
chmod +x "$SCRIPT_PATH"
# Create launchd plist
create_launchd_plist
# Load launchd job
local plist_path="$USER_HOME/Library/LaunchAgents/com.nicholas.periodic-cleanup.plist"
if [ -f "$plist_path" ]; then
launchctl unload "$plist_path" 2>/dev/null
launchctl load "$plist_path"
log_message "Launchd job installed and loaded."
echo "Memory cleanup service installed and will run every $(($RUN_INTERVAL/60)) minutes."
else
log_message "Error: Failed to create launchd plist"
echo "Installation failed. Please check logs at $LOG_FILE"
return 1
fi
return 0
}
# Uninstall the script
uninstall_script() {
local plist_path="$USER_HOME/Library/LaunchAgents/com.nicholas.periodic-cleanup.plist"
if [ -f "$plist_path" ]; then
launchctl unload "$plist_path" 2>/dev/null
rm -f "$plist_path"
log_message "Launchd job uninstalled."
echo "Memory cleanup service has been uninstalled."
else
log_message "No launchd plist found at $plist_path"
echo "No installation found to uninstall."
fi
}
# Show memory statistics
show_statistics() {
if [ -f "$STATS_FILE" ]; then
echo "Memory Statistics (last 10 entries):"
echo "--------------------------------------"
# If csvkit is available, use it for better formatting
if command -v csvlook > /dev/null; then
tail -n 10 "$STATS_FILE" | csvlook
else
# Simple alternative
head -n 1 "$STATS_FILE"
tail -n 10 "$STATS_FILE" | sort -r
fi
echo
echo "Full statistics available in: $STATS_FILE"
else
echo "No statistics available yet. Run the script first to collect data."
fi
}
# -------------------------------------------
# Main Execution
# -------------------------------------------
# Set up trap for proper cleanup
trap 'handle_signal INT' INT
trap 'handle_signal TERM' TERM
trap 'handle_signal EXIT' EXIT
# Create a temporary directory for operations
TEMP_DIR=$(setup_temp_dir)
# Process command-line arguments
if [ $# -eq 0 ]; then
# No arguments, show help
print_help
else
case "$1" in
--run)
run_cleanup
;;
--config)
configure
;;
--create-launchd)
create_launchd_plist
;;
--install)
install_script
;;
--uninstall)
uninstall_script
;;
--show-config)
show_config
;;
--show-excluded)
echo "Protected processes that will never be suspended or killed:"
printf "%s\n" "${EXCLUDED_PROCESSES[@]}" | sort
;;
--stats)
show_statistics
;;
--help)
print_help
;;
*)
echo "Unknown option: $1"
print_help
cleanup_and_exit 1
;;
esac
fi
# Successful completion
cleanup_and_exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment