boot-perf.sh 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. #!/usr/bin/env bash
  2. # mac-ops :: boot-perf.sh
  3. # Measure boot duration and identify slow startup components.
  4. # macOS records boot events in the unified log; we extract the markers.
  5. set -u
  6. DAYS=7
  7. SHOW_N=10
  8. while [[ $# -gt 0 ]]; do
  9. case "$1" in
  10. --days) DAYS="$2"; shift 2 ;;
  11. --show) SHOW_N="$2"; shift 2 ;;
  12. --help|-h)
  13. cat <<EOF
  14. Usage: $0 [options]
  15. --days N How many days of boot history to scan (default: 7)
  16. --show N How many recent boots to show (default: 10)
  17. --json, --redact, --quiet, --verbose
  18. Healthy:
  19. Apple Silicon Mac to login: 10-20s
  20. Intel Mac (SSD): 20-35s
  21. Intel Mac (HDD, vintage): 45-90s
  22. Failing storage: 60s+ with stalls
  23. EOF
  24. exit 0 ;;
  25. *) shift ;;
  26. esac
  27. done
  28. source "$(dirname "$0")/_lib/common.sh"
  29. parse_common_flags "$@"
  30. maybe_filter_self "$@"
  31. # ----------------------------------------------------------------------------
  32. section "1. RECENT BOOT TIMES"
  33. # ----------------------------------------------------------------------------
  34. # Approach: find each "kernel boot" marker, then compute time until loginwindow
  35. # completes its initial setup. The unified log has BOOT_TIME / "kernel boot"
  36. # markers as well as loginwindow setup messages.
  37. note " Scanning unified log for last ${DAYS}d of boot events..."
  38. # macOS marks the kernel boot start with "boot complete" + "boot session" + "first user event"
  39. # We grep for kernel-version + UUID lines that mark a fresh boot.
  40. boots_raw=$(log show --last "${DAYS}d" --style compact --predicate \
  41. 'process == "kernel" AND (eventMessage CONTAINS "Darwin Kernel Version" OR eventMessage CONTAINS "boot args")' \
  42. 2>/dev/null | head -100)
  43. if [[ -z "$boots_raw" ]]; then
  44. log_info "Boot events" "no boot markers found in window — try a wider --days N"
  45. else
  46. # Each fresh boot logs "Darwin Kernel Version" once; count them
  47. boot_count=$(echo "$boots_raw" | grep -c "Darwin Kernel Version" || echo 0)
  48. log_info "Boots in last ${DAYS}d" "${boot_count:-0}"
  49. note " Recent boot markers (most recent ${SHOW_N}):"
  50. echo "$boots_raw" | grep "Darwin Kernel Version" | tail -"$SHOW_N" | awk '{print $1, $2}' | sed 's/^/ /'
  51. fi
  52. # ----------------------------------------------------------------------------
  53. section "2. CURRENT BOOT DURATION ESTIMATE"
  54. # ----------------------------------------------------------------------------
  55. # Find the most recent boot marker (Darwin Kernel Version line)
  56. boot_start_line=$(log show --last "${DAYS}d" --style compact --predicate \
  57. 'process == "kernel" AND eventMessage CONTAINS "Darwin Kernel Version"' \
  58. 2>/dev/null | tail -1)
  59. boot_start_ts=$(echo "$boot_start_line" | awk '{print $1, $2}')
  60. if [[ -z "$boot_start_ts" ]]; then
  61. log_warn "Boot start timestamp" "could not extract"
  62. else
  63. note " Boot start: $boot_start_ts"
  64. # Find first WindowServer / loginwindow ready event AFTER boot
  65. if loginwindow_evt=$(log show --start "$boot_start_ts" --style compact 2>/dev/null \
  66. | grep -E "(loginwindow.*started|WindowServer.*started|opendirectoryd started)" \
  67. | head -3); then
  68. note " Earliest user-space events after boot:"
  69. echo "$loginwindow_evt" | sed 's/^/ /'
  70. fi
  71. # Attempt to compute seconds from boot to loginwindow
  72. first_user_event=$(echo "$loginwindow_evt" | head -1 | awk '{print $1, $2}')
  73. if [[ -n "$first_user_event" ]] && command -v gdate >/dev/null 2>&1; then
  74. b=$(gdate -d "$boot_start_ts" +%s 2>/dev/null)
  75. f=$(gdate -d "$first_user_event" +%s 2>/dev/null)
  76. if [[ -n "$b" ]] && [[ -n "$f" ]]; then
  77. diff=$((f - b))
  78. log_info "Boot duration (boot → user-space)" "${diff}s"
  79. fi
  80. else
  81. log_info "Boot duration calc" "install coreutils (brew install coreutils) for gdate-based timing"
  82. fi
  83. fi
  84. # ----------------------------------------------------------------------------
  85. section "3. SLOW LAUNCH AGENTS"
  86. # ----------------------------------------------------------------------------
  87. # Find agents that took long to start. Narrow to launchd messages specifically;
  88. # avoid matching Wi-Fi/airportd "throttled=0" noise.
  89. slow_events=$(log show --last "${DAYS}d" --style compact --predicate \
  90. 'process == "launchd" AND (eventMessage CONTAINS "took longer than" OR eventMessage CONTAINS "throttled by" OR eventMessage CONTAINS "exited with abnormal code")' \
  91. 2>/dev/null | head -20)
  92. if [[ -z "$slow_events" ]]; then
  93. log_pass "Slow launchd events" "none found"
  94. else
  95. n=$(echo "$slow_events" | wc -l | tr -d ' ')
  96. log_warn "Slow launchd events" "$n events — see below"
  97. echo "$slow_events" | head -10 | sed 's/^/ /'
  98. fi
  99. # ----------------------------------------------------------------------------
  100. section "4. LOGINWINDOW DELAYS"
  101. # ----------------------------------------------------------------------------
  102. # loginwindow logs assertions about slow login items
  103. loginwindow_delays=$(log show --last "${DAYS}d" --style compact \
  104. --predicate 'process == "loginwindow"' 2>/dev/null \
  105. | grep -iE "(delay|slow|timed out|waited)" \
  106. | head -10)
  107. if [[ -n "$loginwindow_delays" ]]; then
  108. log_warn "loginwindow delay messages" "see below"
  109. echo "$loginwindow_delays" | sed 's/^/ /'
  110. else
  111. log_pass "loginwindow delays" "none reported"
  112. fi
  113. # ----------------------------------------------------------------------------
  114. section "5. SAFE-BOOT / VERBOSE-BOOT INDICATORS"
  115. # ----------------------------------------------------------------------------
  116. # nvram for boot args
  117. boot_args=$(nvram boot-args 2>/dev/null | awk '{print $2}')
  118. if [[ "$boot_args" == *"-v"* ]] || [[ "$boot_args" == *"-x"* ]]; then
  119. log_warn "NVRAM boot-args" "$boot_args — non-default boot mode"
  120. else
  121. log_pass "NVRAM boot-args" "default (${boot_args:-empty})"
  122. fi
  123. # ----------------------------------------------------------------------------
  124. emit_summary