name: Profile
on:
workflow_dispatch:
inputs:
profile:
description: sf-decomposer fixture profile
type: choice
default: large
options: [small, medium, large, xlarge, manyfiles]
format:
description: Shard format passed to disassemble
type: choice
default: xml
options: [xml, json, json5, yaml]
sf_decomposer_ref:
description: sf-decomposer ref to take the fixture generator from
type: string
default: main
pull_request:
paths:
- examples/replay_workload.rs
- .github/workflows/profile.yml
permissions:
contents: read
concurrency:
group: profile-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
PROFILE: ${{ inputs.profile || 'large' }}
FORMAT: ${{ inputs.format || 'xml' }}
CARGO_TERM_COLOR: never
RUSTFLAGS: -C force-frame-pointers=yes
CARGO_PROFILE_RELEASE_DEBUG: line-tables-only
jobs:
profile:
name: Profile (${{ inputs.profile || 'large' }}, ${{ inputs.format || 'xml' }})
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Checkout sf-decomposer (fixture generator)
uses: actions/checkout@v7
with:
repository: mcarvin8/sf-decomposer
ref: ${{ inputs.sf_decomposer_ref || 'main' }}
path: sf-decomposer
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: lts/*
- name: Generate fixtures
run: |
npx --yes tsx scripts/gen-perf-fixtures.ts --profile "$PROFILE" --out "$RUNNER_TEMP/fixtures"
working-directory: sf-decomposer
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
cache: false
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
- name: Install inferno (flamegraphs)
uses: taiki-e/install-action@v2
with:
tool: inferno
- name: Install perf
run: |
sudo apt-get update -qq
# Package layout differs across runner images: newer Ubuntu ships a
# `linux-perf` package; older ones need the kernel-matched
# linux-tools package. Try both; whichever yields a working binary wins.
sudo apt-get install -y -qq linux-tools-common
sudo apt-get install -y -qq linux-perf \
|| sudo apt-get install -y -qq "linux-tools-$(uname -r)" \
|| sudo apt-get install -y -qq linux-tools-azure || true
# Prefer a versioned binary (the /usr/bin/perf wrapper on older images
# refuses to run when the tools package doesn't match the kernel), and
# keep the first candidate that actually executes.
PERF=""
for candidate in /usr/lib/linux-tools/*/perf /usr/lib/linux-tools-*/perf \
/usr/lib/linux-perf*/perf "$(command -v perf || true)"; do
if [ -x "$candidate" ] && "$candidate" --version >/dev/null 2>&1; then
PERF="$candidate"; break
fi
done
if [ -z "$PERF" ]; then
echo "::error::no working perf binary found"; find / -name perf -type f 2>/dev/null | head; exit 1
fi
echo "PERF=$PERF" >> "$GITHUB_ENV"
"$PERF" --version
sudo sysctl -w kernel.perf_event_paranoid=-1 kernel.kptr_restrict=0
- name: Build replay
run: cargo build --release --example replay_workload
- name: Unprofiled baseline (wall time, max RSS)
run: |
BIN=target/release/examples/replay_workload
OUT="$RUNNER_TEMP/out"; mkdir -p "$OUT"
cp -r "$RUNNER_TEMP/fixtures" "$RUNNER_TEMP/base"
for phase in decompose recompose; do
/usr/bin/time -v "$BIN" "$phase" "$RUNNER_TEMP/base" --format "$FORMAT" \
2> "$OUT/time-$phase.txt"
done
{
echo "## Unprofiled baseline (profile=$PROFILE, format=$FORMAT)"
echo
echo "Compare with sf-decomposer's perf comment rows \`$PROFILE.$FORMAT.decompose\` / \`.recompose\`."
echo
echo '| Phase | Wall | User CPU (s) | Sys CPU (s) | Max RSS (MB) |'
echo '|---|---:|---:|---:|---:|'
for phase in decompose recompose; do
f="$OUT/time-$phase.txt"
wall=$(grep 'Elapsed (wall clock)' "$f" | awk '{print $NF}')
user=$(grep 'User time' "$f" | awk '{print $NF}')
sys=$(grep 'System time' "$f" | awk '{print $NF}')
rss=$(grep 'Maximum resident' "$f" | awk '{printf "%.1f", $NF/1024}')
echo "| $phase | $wall | $user | $sys | $rss |"
done
echo
} > "$OUT/summary-baseline.md"
# Job summaries aren't readable through the API; print to the log too.
cat "$OUT/summary-baseline.md" | tee -a "$GITHUB_STEP_SUMMARY"
- name: CPU profile (perf)
run: |
BIN=target/release/examples/replay_workload
OUT="$RUNNER_TEMP/out"
cp -r "$RUNNER_TEMP/fixtures" "$RUNNER_TEMP/cpu"
for phase in decompose recompose; do
"$PERF" record -F 499 -g --call-graph fp -o "$OUT/perf-$phase.data" -- \
"$BIN" "$phase" "$RUNNER_TEMP/cpu" --format "$FORMAT" 2>/dev/null
"$PERF" report -i "$OUT/perf-$phase.data" --stdio --no-children --sort symbol \
--percent-limit 0.5 2>/dev/null > "$OUT/top-self-$phase.txt"
"$PERF" report -i "$OUT/perf-$phase.data" --stdio --children --sort symbol \
--percent-limit 2 -g none 2>/dev/null > "$OUT/top-inclusive-$phase.txt"
"$PERF" script -i "$OUT/perf-$phase.data" 2>/dev/null \
| inferno-collapse-perf > "$OUT/$phase.folded"
inferno-flamegraph --title "$phase ($PROFILE, $FORMAT)" < "$OUT/$phase.folded" \
> "$OUT/flamegraph-$phase.svg"
done
{
for phase in decompose recompose; do
echo "## $phase: top functions by self time"
echo
echo '```'
grep -E '^\s+[0-9.]+%' "$OUT/top-self-$phase.txt" | head -n 30
echo '```'
echo
echo "## $phase: top functions by inclusive time"
echo
echo '```'
grep -E '^\s+[0-9.]+%' "$OUT/top-inclusive-$phase.txt" | head -n 30
echo '```'
echo
done
} > "$OUT/summary-cpu.md"
cat "$OUT/summary-cpu.md" | tee -a "$GITHUB_STEP_SUMMARY"
- name: Upload CPU profiles
if: always()
uses: actions/upload-artifact@v7
with:
name: profile-cpu-${{ env.PROFILE }}-${{ env.FORMAT }}
path: |
${{ runner.temp }}/out/*.svg
${{ runner.temp }}/out/*.txt
${{ runner.temp }}/out/*.md
${{ runner.temp }}/out/*.folded
retention-days: 30
if-no-files-found: warn