testsAndMisc/scripts/check_polling_antipatterns.sh
Krzysztof kuhy Rudnicki 59e863f2a5 feat: Add shell script quality enforcement and polling optimization guidelines
- Add pre-commit hook (check_polling_antipatterns.sh) to detect fork-storm anti-patterns
- Update .pre-commit-config.yaml with no-polling-antipatterns hook registration
- Add comprehensive documentation (6 guides, 1000+ lines total)

Detects and blocks:
  * while true + sleep patterns (suggests event-driven I/O)
  * $(date +...) subprocess forks (suggests /proc/uptime or bash printf)
  * pgrep/xdotool in polling functions (expensive fork overhead)
  * aggressive polling (sleep < 1s causing fork storms)
  * heavy piped commands (| awk | grep | tr with multiple forks)

Documentation included:
  - SHELL_SCRIPT_QUALITY_GUIDELINES.md: 3-layer guide for developers/reviewers
  - SHELL_QUALITY_IMPLEMENTATION_SUMMARY.md: Technical implementation reference
  - COMPLETE_IMPLEMENTATION_SUMMARY.md: Full overview and integration guide
  - QUICK_REFERENCE_SHELL_QUALITY.md: Visual checklist and quick lookup
  - DELIVERABLES_INDEX.md: Index of all deliverables and next steps
  - shell.instructions.md: R1-R8 polling optimization rules (in ~/.copilot/instructions/)

System impact:
  - Prevents new scripts from introducing fork-storm regressions
  - Already active optimizations: network_monitor.sh zero-fork, battery 1s->5s, music adaptive sleep
  - Expected daily savings: ~1-2 CPU-hours from eliminated fork overhead

Related: Resolves previous fork-storm issue identified on May 3 causing 728k CPU-seconds
2026-05-03 21:42:49 +02:00

168 lines
5.5 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Pre-commit hook to detect polling script anti-patterns.
# Blocks commits of shell scripts that violate efficient polling best practices.
#
# Usage: check_polling_antipatterns.sh [files...]
# Exit codes: 0 = no issues, 1 = issues found
set -euo pipefail
errors=0
files_checked=0
# Patterns to detect (these are red flags for fork storms)
# shellcheck disable=SC2034
declare -A patterns=(
['while_true_with_sleep']='while\s+(true|:).*sleep'
['date_in_loop']='while.*date|for.*date'
['pgrep_in_loop']='while.*pgrep|for.*pgrep'
['xdotool_in_loop']='while.*xdotool|for.*xdotool'
['pipe_fork_chain']='|.*|.*|.*|'
['subshell_date']='$\(date\s+\+'
['backtick_date']='`date\s+\+'
['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)'
['sleep_less_than_one']='sleep\s+0\.[0-9]'
)
usage() {
echo "Usage: $(basename "$0") [files...]"
echo ""
echo "Checks shell scripts for polling anti-patterns that cause fork storms."
echo "Exit code 0 = no issues, 1 = issues found"
echo ""
echo "Patterns detected:"
echo " - while true + sleep (should use event-driven I/O)"
echo " - \$(date +...) in loops (forks process)"
echo " - pgrep/xdotool in loops (forks process)"
echo " - Multiple piped commands (each | forks)"
echo " - Aggressive polling (sleep < 1s)"
exit 0
}
check_file() {
local file="$1"
local file_errors=0
local line_num=0
local in_polling_function=0
# shellcheck disable=SC2034
local function_name=""
# Skip non-shell files
if ! grep -q "#!/bin/bash\|#!/bin/sh\|#!/usr/bin/env bash" "$file" 2>/dev/null; then
return 0
fi
files_checked=$((files_checked + 1))
# Line-by-line check
while IFS= read -r line; do
line_num=$((line_num + 1))
# Track if we're in a polling-related function
if [[ $line =~ ^[[:space:]]*(.*_loop|.*_daemon|poll.*)\(\) ]]; then
in_polling_function=1
# shellcheck disable=SC2034
function_name="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[[:space:]]*\} ]]; then
in_polling_function=0
fi
# Only check within potential polling contexts
if [[ $in_polling_function -eq 1 ]]; then
# Check for while true + sleep (classic polling anti-pattern)
if [[ $line =~ while[[:space:]]+(true|:) ]] && [[ $line =~ sleep ]]; then
echo " Line $line_num: ❌ while true/: + sleep detected (use event-driven I/O)"
file_errors=$((file_errors + 1))
fi
# Check for $(date ...) or `date ...` in loops
if [[ $line =~ \$\(date[[:space:]] ]] || [[ $line =~ \`date[[:space:]] ]]; then
echo " Line $line_num: ❌ date fork in polling function (optimize with single invocation)"
file_errors=$((file_errors + 1))
fi
# Check for pgrep in loops
if [[ $line =~ \bpgrep\b ]]; then
echo " Line $line_num: ❌ pgrep in polling context (consider alternatives or cache PID)"
file_errors=$((file_errors + 1))
fi
# Check for xdotool in loops
if [[ $line =~ \bxdotool\b ]]; then
echo " Line $line_num: ❌ xdotool in polling context (high fork overhead)"
file_errors=$((file_errors + 1))
fi
# Check for aggressive polling (sleep < 1s)
if [[ $line =~ sleep[[:space:]]+0\.[0-9] ]]; then
echo " Line $line_num: ⚠️ Aggressive polling (sleep < 1s)"
file_errors=$((file_errors + 1))
fi
fi
# Check for excessive pipe chains (each | is a fork)
# Skip lines that are variable assignments or comments
if [[ ! $line =~ = ]] && [[ ! $line =~ ^[[:space:]]*# ]]; then
local pipe_count
pipe_count=$(echo "$line" | tr -cd '|' | wc -c)
if [[ $pipe_count -gt 3 ]]; then
echo " Line $line_num: ⚠️ Excessive pipes ($pipe_count pipes = many forks)"
file_errors=$((file_errors + 1))
fi
fi
done < "$file"
if [[ $file_errors -gt 0 ]]; then
errors=$((errors + file_errors))
return 1
fi
return 0
}
provide_suggestions() {
echo ""
echo "📋 Optimization Tips:"
echo ""
echo "Replace polling loops with:"
echo " • inotifywait/fanotify for file system events"
echo " • timerfd for interval-based tasks"
echo " • select/poll for I/O multiplexing"
echo " • systemd timers for scheduled tasks"
echo " • dbus signals for system events"
echo ""
echo "Reduce fork overhead:"
echo " • Cache \$(date +%s) in variable, update periodically"
echo " • Use /proc filesystem instead of pgrep"
echo " • Consolidate commands with && instead of separate invocations"
echo ""
}
main() {
# Show usage if requested
[[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage
# Check all provided files
for file in "$@"; do
check_file "$file" || true
done
# Report results
if [[ $files_checked -eq 0 ]]; then
echo " No shell scripts to check"
exit 0
fi
if [[ $errors -gt 0 ]]; then
echo ""
echo "❌ Found $errors issue(s) in shell scripts"
provide_suggestions
exit 1
else
echo "✓ No polling anti-patterns detected in $files_checked file(s)"
exit 0
fi
}
main "$@"