|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Simple production-style flamegraph helper using perf + inferno (Linux) |
| 5 | +# or cargo-flamegraph (macOS). |
| 6 | +# Intended to be invoked via `make flamegraph-prod` with environment |
| 7 | +# overrides, e.g.: |
| 8 | +# FLAME_FILE=samples/security_big_sample.evtx \ |
| 9 | +# FORMAT=json \ |
| 10 | +# DURATION=30 \ |
| 11 | +# BIN=./target/release/evtx_dump \ |
| 12 | +# make flamegraph-prod |
| 13 | +# |
| 14 | +OS="$(uname -s || echo unknown)" |
| 15 | + |
| 16 | +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 17 | + |
| 18 | +# Optional label for this run (used in output filenames). |
| 19 | +: "${TAG:=default}" |
| 20 | + |
| 21 | +: "${BIN:=$ROOT_DIR/target/release/evtx_dump}" |
| 22 | +: "${FLAME_FILE:=$ROOT_DIR/samples/security_big_sample.evtx}" |
| 23 | +: "${FORMAT:=json}" |
| 24 | +: "${DURATION:=30}" |
| 25 | +# For JSON formats, choose parser implementation: streaming | legacy. |
| 26 | +: "${JSON_PARSER:=streaming}" |
| 27 | +: "${OUT_DIR:=$ROOT_DIR/profile}" |
| 28 | + |
| 29 | +mkdir -p "$OUT_DIR" |
| 30 | + |
| 31 | +echo "Profiling" |
| 32 | +echo " FLAME_FILE=$FLAME_FILE" |
| 33 | +echo " FORMAT=$FORMAT" |
| 34 | +echo " DURATION=${DURATION}s" |
| 35 | +echo " OUT_DIR=$OUT_DIR" |
| 36 | +echo " TAG=$TAG" |
| 37 | + |
| 38 | +# Map FORMAT to evtx_dump arguments. |
| 39 | +case "$FORMAT" in |
| 40 | + json|jsonl) |
| 41 | + # Use streaming JSON path by default; caller can change via JSON_PARSER env. |
| 42 | + FMT_ARGS=(-t 1 -o "$FORMAT" --json-parser "$JSON_PARSER") |
| 43 | + ;; |
| 44 | + xml) |
| 45 | + FMT_ARGS=(-t 1 -o xml) |
| 46 | + ;; |
| 47 | + *) |
| 48 | + echo "warning: unknown FORMAT='$FORMAT', defaulting to json" >&2 |
| 49 | + FMT_ARGS=(-t 1 -o json --json-parser streaming) |
| 50 | + ;; |
| 51 | +esac |
| 52 | + |
| 53 | +if [[ "$OS" == "Darwin" ]]; then |
| 54 | + # macOS path: use cargo-flamegraph (wraps dtrace + inferno). |
| 55 | + if ! command -v cargo >/dev/null 2>&1; then |
| 56 | + echo "error: cargo not found in PATH; required for cargo-flamegraph on macOS." >&2 |
| 57 | + exit 1 |
| 58 | + fi |
| 59 | + |
| 60 | + echo "Detected macOS; using cargo flamegraph (you may be prompted for sudo)." |
| 61 | + |
| 62 | + FOLDED_STACKS="$OUT_DIR/stacks_${TAG}.folded" |
| 63 | + |
| 64 | + # Ask cargo-flamegraph to tee the folded stacks into our own file. |
| 65 | + (cd "$ROOT_DIR" && \ |
| 66 | + cargo flamegraph \ |
| 67 | + --root \ |
| 68 | + --bin evtx_dump \ |
| 69 | + --output "$OUT_DIR/flamegraph_${TAG}.svg" \ |
| 70 | + --post-process "tee $FOLDED_STACKS" \ |
| 71 | + -- "${FMT_ARGS[@]}" "$FLAME_FILE") |
| 72 | + |
| 73 | + if [[ -f "$FOLDED_STACKS" ]] && [[ -s "$FOLDED_STACKS" ]]; then |
| 74 | + # Extract top leafs (leaf functions) from folded stacks |
| 75 | + { |
| 76 | + echo "Top leaf functions (by total samples):" |
| 77 | + awk '{ |
| 78 | + n = split($1, stack, ";"); |
| 79 | + if (n > 0) { |
| 80 | + leaf = stack[n]; |
| 81 | + count = $2 + 0; |
| 82 | + leafs[leaf] += count; |
| 83 | + } |
| 84 | + } |
| 85 | + END { |
| 86 | + for (f in leafs) { |
| 87 | + printf "%d %s\n", leafs[f], f; |
| 88 | + } |
| 89 | + }' "$FOLDED_STACKS" | sort -nr | head -20 | awk '{printf " %s: %s\n", $2, $1}' |
| 90 | + } > "$OUT_DIR/top_leaf_${TAG}.txt" |
| 91 | + |
| 92 | + # Extract top titles (root functions) from folded stacks |
| 93 | + { |
| 94 | + echo "Top title functions (by total samples):" |
| 95 | + awk '{ |
| 96 | + n = split($1, stack, ";"); |
| 97 | + if (n > 0) { |
| 98 | + title = stack[1]; |
| 99 | + count = $2 + 0; |
| 100 | + titles[title] += count; |
| 101 | + } |
| 102 | + } |
| 103 | + END { |
| 104 | + for (f in titles) { |
| 105 | + printf "%d %s\n", titles[f], f; |
| 106 | + } |
| 107 | + }' "$FOLDED_STACKS" | sort -nr | head -20 | awk '{printf " %s: %s\n", $2, $1}' |
| 108 | + } > "$OUT_DIR/top_titles_${TAG}.txt" |
| 109 | + |
| 110 | + echo "Top leafs written to $OUT_DIR/top_leaf_${TAG}.txt" |
| 111 | + echo "Top titles written to $OUT_DIR/top_titles_${TAG}.txt" |
| 112 | + else |
| 113 | + echo "warning: folded stacks file is empty or missing, skipping text summaries" >&2 |
| 114 | + fi |
| 115 | + |
| 116 | + echo "Flamegraph written to $OUT_DIR/flamegraph_${TAG}.svg" |
| 117 | + exit 0 |
| 118 | +fi |
| 119 | + |
| 120 | +# Linux / perf + inferno path. |
| 121 | +# |
| 122 | +# Requirements: |
| 123 | +# - perf |
| 124 | +# - inferno-collapse-perf |
| 125 | +# - inferno-flamegraph |
| 126 | + |
| 127 | +if ! command -v perf >/dev/null 2>&1; then |
| 128 | + echo "error: perf not found in PATH; flamegraph_prod.sh currently expects Linux + perf." >&2 |
| 129 | + exit 1 |
| 130 | +fi |
| 131 | + |
| 132 | +if ! command -v inferno-collapse-perf >/dev/null 2>&1; then |
| 133 | + echo "error: inferno-collapse-perf not found in PATH." >&2 |
| 134 | + exit 1 |
| 135 | +fi |
| 136 | + |
| 137 | +if ! command -v inferno-flamegraph >/dev/null 2>&1; then |
| 138 | + echo "error: inferno-flamegraph not found in PATH." >&2 |
| 139 | + exit 1 |
| 140 | +fi |
| 141 | + |
| 142 | +perf record -F 999 -g --output "$OUT_DIR/perf.data" -- \ |
| 143 | + "$BIN" "${FMT_ARGS[@]}" "$FLAME_FILE" >/dev/null |
| 144 | + |
| 145 | +perf script -i "$OUT_DIR/perf.data" | inferno-collapse-perf > "$OUT_DIR/stacks.folded" |
| 146 | +cat "$OUT_DIR/stacks.folded" | inferno-flamegraph > "$OUT_DIR/flamegraph_${TAG}.svg" |
| 147 | + |
| 148 | +# Extract top leafs (functions at end of stack) and top titles (functions at start of stack) |
| 149 | +# Folded format: "func1;func2;func3 12345" where number is sample count |
| 150 | +{ |
| 151 | + echo "Top leaf functions (by total samples):" |
| 152 | + awk '{ |
| 153 | + n = split($1, stack, ";"); |
| 154 | + if (n > 0) { |
| 155 | + leaf = stack[n]; |
| 156 | + count = $2 + 0; |
| 157 | + leafs[leaf] += count; |
| 158 | + } |
| 159 | + } |
| 160 | + END { |
| 161 | + for (f in leafs) { |
| 162 | + printf "%d %s\n", leafs[f], f; |
| 163 | + } |
| 164 | + }' "$OUT_DIR/stacks.folded" | sort -nr | head -20 | awk '{printf " %s: %s\n", $2, $1}' |
| 165 | +} > "$OUT_DIR/top_leaf_${TAG}.txt" |
| 166 | + |
| 167 | +{ |
| 168 | + echo "Top title functions (by total samples):" |
| 169 | + awk '{ |
| 170 | + n = split($1, stack, ";"); |
| 171 | + if (n > 0) { |
| 172 | + title = stack[1]; |
| 173 | + count = $2 + 0; |
| 174 | + titles[title] += count; |
| 175 | + } |
| 176 | + } |
| 177 | + END { |
| 178 | + for (f in titles) { |
| 179 | + printf "%d %s\n", titles[f], f; |
| 180 | + } |
| 181 | + }' "$OUT_DIR/stacks.folded" | sort -nr | head -20 | awk '{printf " %s: %s\n", $2, $1}' |
| 182 | +} > "$OUT_DIR/top_titles_${TAG}.txt" |
| 183 | + |
| 184 | +echo "Flamegraph written to $OUT_DIR/flamegraph_${TAG}.svg" |
| 185 | +echo "Top leafs written to $OUT_DIR/top_leaf_${TAG}.txt" |
| 186 | +echo "Top titles written to $OUT_DIR/top_titles_${TAG}.txt" |
| 187 | + |
| 188 | + |
0 commit comments