#!/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