"vllm/vscode:/vscode.git/clone" did not exist on "210207525e085b111a304089f4413fb5056263b4"
offcpu_flamegraph.sh 4.63 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/bin/bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Off-CPU flame graph using BPF offcputime + flamegraph.
# Shows what threads are blocked on (mutexes, I/O, futex, socket waits).
#
# Usage:
#   sudo ./offcpu_flamegraph.sh --pid <PID>
#   sudo ./offcpu_flamegraph.sh --pid <PID> --duration 30

set -euo pipefail

PID=""
DURATION="${DURATION:-30}"
OUTPUT_DIR="${OUTPUT_DIR:-.}"
OUTPUT_NAME="offcpu_flamegraph_$(date +%Y%m%d_%H%M%S)"
MIN_US="${MIN_US:-1000}"  # Minimum off-CPU time to record (1ms)

while [[ $# -gt 0 ]]; do
    case $1 in
        --pid|-p)       PID="$2"; shift 2 ;;
        --duration|-d)  DURATION="$2"; shift 2 ;;
        --output-dir)   OUTPUT_DIR="$2"; shift 2 ;;
        --output)       OUTPUT_NAME="$2"; shift 2 ;;
        --min-us)       MIN_US="$2"; shift 2 ;;
        -h|--help)
            echo "Usage: sudo $0 [OPTIONS]"
            echo ""
            echo "Options:"
            echo "  --pid PID         Target process (required)"
            echo "  --duration N      Capture duration in seconds (default: 30)"
            echo "  --output-dir DIR  Output directory (default: .)"
            echo "  --min-us N        Minimum off-CPU microseconds to record (default: 1000)"
            exit 0
            ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

if [[ -z "$PID" ]]; then
    echo "ERROR: --pid is required"
    exit 1
fi

mkdir -p "$OUTPUT_DIR"
RAW_STACKS="${OUTPUT_DIR}/${OUTPUT_NAME}.raw"
FOLDED_STACKS="${OUTPUT_DIR}/${OUTPUT_NAME}.stacks"
NEEDS_FOLD=false

# Try bpftrace-based offcputime
if command -v bpftrace &>/dev/null; then
    echo "Capturing off-CPU stacks for PID $PID for ${DURATION}s..."

    # Use timeout to limit duration; keep stderr separate from stack data
    timeout "$DURATION" bpftrace -p "$PID" -e '
        tracepoint:sched:sched_switch {
            if (args.prev_state != 0) {
                @off[tid] = nsecs;
                @stack[tid] = kstack;
            }
        }
        tracepoint:sched:sched_switch {
            $start = @off[args.next_pid];
            if ($start) {
                $delta = (nsecs - $start) / 1000;
                if ($delta > '"$MIN_US"') {
                    @stacks[@stack[args.next_pid], comm] = sum($delta);
                }
                delete(@off[args.next_pid]);
                delete(@stack[args.next_pid]);
            }
        }
        END { print(@stacks); clear(@off); clear(@stack); }
    ' > "$RAW_STACKS" 2>/dev/null || true

    NEEDS_FOLD=true
    echo "Raw stacks captured: $RAW_STACKS"

# Try bcc offcputime — outputs folded format directly with -f
elif command -v offcputime-bpfcc &>/dev/null; then
    echo "Using bcc offcputime for PID $PID for ${DURATION}s..."
    offcputime-bpfcc -d "$DURATION" -p "$PID" -m "$MIN_US" -f > "$FOLDED_STACKS"
else
    echo "ERROR: No BPF tool found. Install bpftrace or bcc-tools."
    exit 1
fi

# Convert bpftrace native format to folded stacks.
# bpftrace @stacks[kstack, comm] format:
#   @stacks[
#       leaf_func+offset
#       ...
#       root_func+offset
#   , comm_name]: value
# Folded format: comm;root_func;...;leaf_func value
if [[ "$NEEDS_FOLD" == true ]] && [[ -f "$RAW_STACKS" ]]; then
    awk '
    /^@stacks\[/ { n=0; next }
    /^[[:space:]]+[a-zA-Z_]/ {
        gsub(/^[[:space:]]+/, "")
        sub(/\+[0-9]+$/, "")
        frames[n++] = $0
        next
    }
    /^, / {
        sub(/^, /, "")
        idx = index($0, "]: ")
        comm = substr($0, 1, idx-1)
        val = substr($0, idx+3) + 0
        if (n > 0 && val > 0) {
            printf "%s", comm
            for (i=n-1; i>=0; i--) printf ";%s", frames[i]
            printf " %d\n", val
        }
        n = 0
        next
    }
    ' "$RAW_STACKS" > "$FOLDED_STACKS"
    echo "Folded stacks: $FOLDED_STACKS ($(wc -l < "$FOLDED_STACKS") entries)"
fi

# Generate flamegraph SVG from folded stacks
if [[ ! -s "$FOLDED_STACKS" ]]; then
    echo "WARNING: No stacks captured — SVG not generated"
    exit 0
fi

if command -v flamegraph.pl &>/dev/null; then
    flamegraph.pl --color=io --title="Off-CPU Flame Graph (PID $PID)" \
        --countname="us" < "$FOLDED_STACKS" > "${OUTPUT_DIR}/${OUTPUT_NAME}.svg"
    echo "Flame graph: ${OUTPUT_DIR}/${OUTPUT_NAME}.svg"
elif command -v inferno-flamegraph &>/dev/null; then
    inferno-flamegraph --colors io --title "Off-CPU Flame Graph (PID $PID)" \
        --countname "us" < "$FOLDED_STACKS" > "${OUTPUT_DIR}/${OUTPUT_NAME}.svg"
    echo "Flame graph: ${OUTPUT_DIR}/${OUTPUT_NAME}.svg"
else
    echo "Folded stacks: $FOLDED_STACKS"
    echo "Install flamegraph tools to generate SVG: cargo install inferno"
fi