#!/bin/bash # 🤖 wedge-watcher — samples a ClickLab-instrumented Mac build while its main # thread is wedged. ClickLab (ios/LChatMac/ClickLab.swift) rewrites a # heartbeat file from the MAIN thread every ~1s; when the file goes stale # while the process is alive, main is blocked (or the app is napping — idle # stacks are discarded at analysis time), and `sample` captures the live # stack. The in-app watchdog can only report a stall AFTER it drains — this # is how you catch the blocked stack DURING one. # # Usage: tools/wedge-watcher.sh [bundle-id-suffix] [out-dir] [hours] # e.g. tools/wedge-watcher.sh alt-clicklab ~/Desktop/wedges 8 # The build must be sample-able: Debug builds are (get-task-allow); Release # rig builds need CODE_SIGN_INJECT_BASE_ENTITLEMENTS=YES at build time. # Analysis: read the sample's main-thread block — an idle run loop # (__CFRunLoopRun → mach_msg) means App Nap, anything else names the wedge. SUFFIX="${1:-alt-clicklab}" HB="$HOME/Library/Containers/com.elevenmessenger.messenger.mac.$SUFFIX/Data/Library/Logs/ClickLab.heartbeat" OUT="${2:-./wedges}" HOURS="${3:-8}" mkdir -p "$OUT" END=$((SECONDS + HOURS * 3600)) LAST=0 echo "watching $HB → $OUT for ${HOURS}h" while [ $SECONDS -lt $END ]; do sleep 2 PID=$(pgrep -f "$SUFFIX" | head -1) [ -z "$PID" ] && continue [ -f "$HB" ] || continue AGE=$(( $(date +%s) - $(stat -f %m "$HB") )) if [ "$AGE" -ge 4 ] && [ $((SECONDS - LAST)) -ge 30 ]; then TS=$(date +%H%M%S) sample "$PID" 3 -file "$OUT/wedge-$TS.txt" >/dev/null 2>&1 echo "$(date +%H:%M:%S) heartbeat ${AGE}s stale — sampled pid $PID → wedge-$TS.txt" LAST=$SECONDS fi done echo "wedge-watcher: ${HOURS}h window elapsed, exiting"