You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
7.3 KiB
260 lines
7.3 KiB
#!/bin/bash |
|
# |
|
# Poor man's sampling profiler for NuttX. |
|
# |
|
# Usage: Install flamegraph.pl in your PATH, configure your .gdbinit, run the script with proper arguments and go |
|
# have a coffee. When you're back, you'll see the flamegraph. Note that frequent calls to GDB significantly |
|
# interfere with normal operation of the target, which means that you can't profile real-time tasks with it. |
|
# |
|
# Requirements: ARM GDB with Python support |
|
# |
|
|
|
set -e |
|
root=$(dirname $0)/.. |
|
|
|
function die() |
|
{ |
|
echo "$@" |
|
exit 1 |
|
} |
|
|
|
function usage() |
|
{ |
|
echo "Invalid usage. Supported options:" |
|
cat $0 | sed -n 's/^\s*--\([^)\*]*\).*/\1/p' # Don't try this at home. |
|
exit 1 |
|
} |
|
|
|
which flamegraph.pl > /dev/null || die "Install flamegraph.pl first" |
|
|
|
# |
|
# Parsing the arguments. Read this section for usage info. |
|
# |
|
nsamples=0 |
|
sleeptime=0.1 # Doctors recommend 7-8 hours a day |
|
taskname= |
|
elf=$root/Build/px4fmu-v2_default.build/firmware.elf |
|
append=0 |
|
fgfontsize=10 |
|
fgwidth=1900 |
|
|
|
for i in "$@" |
|
do |
|
case $i in |
|
--nsamples=*) |
|
nsamples="${i#*=}" |
|
;; |
|
--sleeptime=*) |
|
sleeptime="${i#*=}" |
|
;; |
|
--taskname=*) |
|
taskname="${i#*=}" |
|
;; |
|
--elf=*) |
|
elf="${i#*=}" |
|
;; |
|
--append) |
|
append=1 |
|
;; |
|
--fgfontsize=*) |
|
fgfontsize="${i#*=}" |
|
;; |
|
--fgwidth=*) |
|
fgwidth="${i#*=}" |
|
;; |
|
*) |
|
usage |
|
;; |
|
esac |
|
shift |
|
done |
|
|
|
# |
|
# Temporary files |
|
# |
|
stacksfile=/tmp/pmpn-stacks.log |
|
foldfile=/tmp/pmpn-folded.txt |
|
graphfile=/tmp/pmpn-flamegraph.svg |
|
gdberrfile=/tmp/pmpn-gdberr.log |
|
|
|
# |
|
# Sampling if requested. Note that if $append is true, the stack file will not be rewritten. |
|
# |
|
cd $root |
|
|
|
if [[ $nsamples > 0 ]] |
|
then |
|
[[ $append = 0 ]] && (rm -f $stacksfile; echo "Old stacks removed") |
|
|
|
echo "Sampling the task '$taskname'..." |
|
|
|
for x in $(seq 1 $nsamples) |
|
do |
|
if [[ "$taskname" = "" ]] |
|
then |
|
arm-none-eabi-gdb $elf --batch -ex "set print asm-demangle on" -ex bt \ |
|
2> $gdberrfile \ |
|
| sed -n 's/\(#.*\)/\1/p' \ |
|
>> $stacksfile |
|
else |
|
arm-none-eabi-gdb $elf --batch -ex "set print asm-demangle on" \ |
|
-ex "source $root/Debug/Nuttx.py" \ |
|
-ex "show mybt $taskname" \ |
|
2> $gdberrfile \ |
|
| sed -n 's/0\.0:\(#.*\)/\1/p' \ |
|
>> $stacksfile |
|
fi |
|
echo -e '\n\n' >> $stacksfile |
|
echo -ne "\r$x/$nsamples" |
|
sleep $sleeptime |
|
done |
|
|
|
echo |
|
echo "Stacks saved to $stacksfile" |
|
else |
|
echo "Sampling skipped - set 'nsamples' to re-sample." |
|
fi |
|
|
|
# |
|
# Folding the stacks. |
|
# |
|
[ -f $stacksfile ] || die "Where are the stack samples?" |
|
|
|
cat << 'EOF' > /tmp/pmpn-folder.py |
|
# |
|
# This stack folder correctly handles C++ types. |
|
# |
|
from __future__ import print_function, division |
|
import fileinput, collections, os, sys |
|
|
|
def enforce(x, msg='Invalid input'): |
|
if not x: |
|
raise Exception(msg) |
|
|
|
def split_first_part_with_parens(line): |
|
LBRACES = {'(':'()', '<':'<>', '[':'[]', '{':'{}'} |
|
RBRACES = {')':'()', '>':'<>', ']':'[]', '}':'{}'} |
|
QUOTES = set(['"', "'"]) |
|
quotes = collections.defaultdict(bool) |
|
braces = collections.defaultdict(int) |
|
out = '' |
|
for ch in line: |
|
out += ch |
|
# escape character cancels further processing |
|
if ch == '\\': |
|
continue |
|
# special cases |
|
if out.endswith('operator>') or out.endswith('operator>>') or out.endswith('operator->'): # gotta love c++ |
|
braces['<>'] += 1 |
|
if out.endswith('operator<') or out.endswith('operator<<'): |
|
braces['<>'] -= 1 |
|
# switching quotes |
|
if ch in QUOTES: |
|
quotes[ch] = not quotes[ch] |
|
# counting parens only when outside quotes |
|
if sum(quotes.values()) == 0: |
|
if ch in LBRACES.keys(): |
|
braces[LBRACES[ch]] += 1 |
|
if ch in RBRACES.keys(): |
|
braces[RBRACES[ch]] -= 1 |
|
# sanity check |
|
for v in braces.values(): |
|
enforce(v >= 0, 'Unaligned braces: ' + str(dict(braces))) |
|
# termination condition |
|
if ch == ' ' and sum(braces.values()) == 0: |
|
break |
|
out = out.strip() |
|
return out, line[len(out):] |
|
|
|
def parse(line): |
|
def take_path(line, output): |
|
line = line.strip() |
|
if line.startswith('at '): |
|
line = line[3:].strip() |
|
if line: |
|
output['file_full_path'] = line.rsplit(':', 1)[0].strip() |
|
output['file_base_name'] = os.path.basename(output['file_full_path']) |
|
output['line'] = int(line.rsplit(':', 1)[1]) |
|
return output |
|
|
|
def take_args(line, output): |
|
line = line.lstrip() |
|
if line[0] == '(': |
|
output['args'], line = split_first_part_with_parens(line) |
|
return take_path(line.lstrip(), output) |
|
|
|
def take_function(line, output): |
|
output['function'], line = split_first_part_with_parens(line.lstrip()) |
|
return take_args(line.lstrip(), output) |
|
|
|
def take_mem_loc(line, output): |
|
line = line.lstrip() |
|
if line.startswith('0x'): |
|
end = line.find(' ') |
|
num = line[:end] |
|
output['memloc'] = int(num, 16) |
|
line = line[end:].lstrip() |
|
end = line.find(' ') |
|
enforce(line[:end] == 'in') |
|
line = line[end:].lstrip() |
|
return take_function(line, output) |
|
|
|
def take_frame_num(line, output): |
|
line = line.lstrip() |
|
enforce(line[0] == '#') |
|
end = line.find(' ') |
|
num = line[1:end] |
|
output['frame_num'] = int(num) |
|
return take_mem_loc(line[end:], output) |
|
|
|
return take_frame_num(line, {}) |
|
|
|
stacks = collections.defaultdict(int) |
|
current = '' |
|
|
|
stack_tops = collections.defaultdict(int) |
|
num_stack_frames = 0 |
|
|
|
for idx,line in enumerate(fileinput.input()): |
|
try: |
|
line = line.strip() |
|
if line: |
|
inf = parse(line) |
|
fun = inf['function'] |
|
current = (fun + ';' + current) if current else fun |
|
|
|
if inf['frame_num'] == 0: |
|
num_stack_frames += 1 |
|
stack_tops[fun] += 1 |
|
elif current: |
|
stacks[current] += 1 |
|
current = '' |
|
except Exception, ex: |
|
print('ERROR (line %d):' % (idx + 1), ex, file=sys.stderr) |
|
|
|
for s, f in sorted(stacks.items(), key=lambda (s, f): s): |
|
print(s, f) |
|
|
|
print('Total stack frames:', num_stack_frames, file=sys.stderr) |
|
print('Top consumers (distribution of the stack tops):', file=sys.stderr) |
|
for name,num in sorted(stack_tops.items(), key=lambda (name, num): num, reverse=True)[:10]: |
|
print('% 5.1f%% ' % (100 * num / num_stack_frames), name, file=sys.stderr) |
|
EOF |
|
|
|
cat $stacksfile | python /tmp/pmpn-folder.py > $foldfile |
|
|
|
echo "Folded stacks saved to $foldfile" |
|
|
|
# |
|
# Graphing. |
|
# |
|
cat $foldfile | flamegraph.pl --fontsize=$fgfontsize --width=$fgwidth > $graphfile |
|
echo "FlameGraph saved to $graphfile" |
|
|
|
# On KDE, xdg-open prefers Gwenview by default, which doesn't handle interactive SVGs, so we need a browser. |
|
# The current implementation is hackish and stupid. Somebody, please do something about it. |
|
opener=xdg-open |
|
which firefox > /dev/null && opener=firefox |
|
which google-chrome > /dev/null && opener=google-chrome |
|
|
|
$opener $graphfile
|
|
|