#!/usr/bin/env bash # Profile a Lean binary with demangled names. # # Usage: # script/lean_profile.sh ./my_lean_binary [args...] # # Records a profile with samply, symbolicates via samply's API, # demangles Lean symbol names, and opens the result in Firefox Profiler. # # Requirements: samply (cargo install samply), python3 # # Options (via environment variables): # SAMPLY_RATE — sampling rate in Hz (default: 1000) # SAMPLY_PORT — port for samply symbolication server (default: 3756) # SERVE_PORT — port for serving the demangled profile (default: 3757) # PROFILE_KEEP — set to 1 to keep the raw profile after demangling set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROFILER_DIR="$SCRIPT_DIR/profiler" SYMBOLICATE="$PROFILER_DIR/symbolicate_profile.py" SERVE_PROFILE="$PROFILER_DIR/serve_profile.py" usage() { cat >&2 < [args...] Profile a Lean binary and view the results in Firefox Profiler with demangled Lean names. Requirements: samply cargo install samply python3 (included with macOS / most Linux distros) Environment variables: SAMPLY_RATE sampling rate in Hz (default: 1000) SAMPLY_PORT port for samply symbolication server (default: 3756) SERVE_PORT port for serving the demangled profile (default: 3757) PROFILE_KEEP set to 1 to keep the temp directory after profiling Reading demangled names: Compiler suffixes are shown as modifier flags after the name: [arity↓] reduced-arity specialization (_redArg) [boxed] boxed calling-convention wrapper (_boxed) [λ] lambda-lifted closure (_lam_N, _lambda_N, _elam_N) [jp] join point (_jp_N) [closed] extracted closed subterm (_closed_N) [private] private (module-scoped) def (_private.Module.0. prefix) [impl] implementation detail (_impl) Specializations appear after the flags: Lean.Meta.foo [λ] spec at Lean.Meta.bar[λ, arity↓] = foo (with lambda closure), specialized at bar (lambda, reduced arity) Multiple "spec at" entries indicate chained specializations. See script/PROFILER_README.md for full documentation. EOF exit "${1:-0}" } if [ $# -eq 0 ]; then usage 1 fi case "${1:-}" in -h|--help) usage 0 ;; esac if ! command -v samply &>/dev/null; then echo "error: samply not found. Install with: cargo install samply" >&2 exit 1 fi RATE="${SAMPLY_RATE:-1000}" PORT="${SAMPLY_PORT:-3756}" SERVE="${SERVE_PORT:-3757}" TMPDIR=$(mktemp -d /tmp/lean-profile-XXXXXX) TMPFILE="$TMPDIR/profile.json.gz" DEMANGLED="$TMPDIR/profile-demangled.json.gz" SAMPLY_LOG="$TMPDIR/samply.log" SAMPLY_PID="" cleanup() { if [ -n "$SAMPLY_PID" ]; then kill "$SAMPLY_PID" 2>/dev/null || true wait "$SAMPLY_PID" 2>/dev/null || true fi # Safety net: kill anything still on the symbolication port lsof -ti :"$PORT" 2>/dev/null | xargs kill 2>/dev/null || true [ "${PROFILE_KEEP:-0}" = "1" ] || rm -rf "$TMPDIR" } trap cleanup EXIT # Step 1: Record echo "Recording profile (rate=${RATE} Hz)..." >&2 samply record --save-only -o "$TMPFILE" -r "$RATE" "$@" # Step 2: Start samply server for symbolication echo "Starting symbolication server..." >&2 samply load --no-open -P "$PORT" "$TMPFILE" > "$SAMPLY_LOG" 2>&1 & SAMPLY_PID=$! # Wait for server to be ready for i in $(seq 1 30); do if grep -q "Local server listening" "$SAMPLY_LOG" 2>/dev/null; then break fi sleep 0.2 done # Extract the token from samply's output TOKEN=$(grep -oE '[a-z0-9]{30,}' "$SAMPLY_LOG" | head -1) if [ -z "$TOKEN" ]; then echo "error: could not get samply server token" >&2 exit 1 fi SERVER_URL="http://127.0.0.1:${PORT}/${TOKEN}" # Step 3: Symbolicate + demangle echo "Symbolicating and demangling..." >&2 python3 "$SYMBOLICATE" --server "$SERVER_URL" "$TMPFILE" -o "$DEMANGLED" # Step 4: Kill symbolication server kill "$SAMPLY_PID" 2>/dev/null || true wait "$SAMPLY_PID" 2>/dev/null || true SAMPLY_PID="" # Step 5: Serve the demangled profile directly (without samply's re-symbolication) echo "Opening in Firefox Profiler..." >&2 python3 "$SERVE_PROFILE" "$DEMANGLED" -P "$SERVE"