Skip to content

Commit

Permalink
Add support for stackcollapse format.
Browse files Browse the repository at this point in the history
Fixes #6
  • Loading branch information
richvdh authored and jrfonseca committed Mar 10, 2024
1 parent a05b7bf commit 14e0d54
Show file tree
Hide file tree
Showing 4 changed files with 1,620 additions and 3 deletions.
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ It can:
* [Java's HPROF](https://docs.oracle.com/javase/7/docs/technotes/samples/hprof.html)
* prof, [gprof](https://sourceware.org/binutils/docs/gprof/)
* [DTrace](https://en.wikipedia.org/wiki/DTrace)
* stackcollapse from [FlameGraph](https://github.com/brendangregg/FlameGraph)
* prune nodes and edges below a certain threshold;
* use an heuristic to propagate time inside mutually recursive functions;
* use color efficiently to draw attention to hot-spots;
Expand Down Expand Up @@ -89,9 +90,9 @@ Options:
-e PERCENTAGE, --edge-thres=PERCENTAGE
eliminate edges below this threshold [default: 0.1]
-f FORMAT, --format=FORMAT
profile format: axe, callgrind, dtrace, hprof, json,
oprofile, perf, prof, pstats, sleepy, sysprof or xperf
[default: prof]
profile format: axe, callgrind, collapse, dtrace,
hprof, json, oprofile, perf, prof, pstats, sleepy,
sysprof or xperf [default: prof]
--total=TOTALMETHOD preferred method of calculating total time: callratios
or callstacks (currently affects only perf format)
[default: callratios]
Expand Down Expand Up @@ -222,6 +223,27 @@ See [Russell Power's blog post](http://web.archive.org/web/20220122110828/http:/
# TODO: add an encoding flag to tell gprof2dot how to decode the profile file.
iconv -f ISO-8859-1 -t UTF-8 out.user_stacks | gprof2dot.py -f dtrace

### stackcollapse

Brendan Gregg's FlameGraph tool takes as its input a text file containing one
line per sample. This format can be generated from various other inputs using
the `stackcollapse` scripts in the [FlameGraph
repository](https://github.com/brendangregg/FlameGraph). It can also be
generated by tools such as [py-spy](https://github.com/benfred/py-spy).

Example usage:

* Perf

perf record -g -- /path/to/your/executable
perf script | FlameGraph/stackcollapse-perf.pl > out.collapse
gprof2dot.py -f collapse out.collapse | dot -Tpng -o output.png

* Py-spy

py-spy record -p <pidfile> -f raw -o out.collapse
gprof2dot.py -f collapse out.collapse | dot -Tpng -o output.png

## Output

A node in the output graph represents a function and has the following layout:
Expand Down
96 changes: 96 additions & 0 deletions gprof2dot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2916,9 +2916,105 @@ def parse_call(self):

return function, None


class CollapseParser(LineParser):
"""Parser for the output of stackcollapse
(from https://github.com/brendangregg/FlameGraph)
"""

def __init__(self, infile):
LineParser.__init__(self, infile)
self.profile = Profile()

def parse(self):
profile = self.profile
profile[SAMPLES] = 0

self.readline()
while not self.eof():
self.parse_event()

# compute derived data
profile.validate()
profile.find_cycles()
profile.ratio(TIME_RATIO, SAMPLES)
profile.call_ratios(SAMPLES2)
if totalMethod == "callratios":
# Heuristic approach. TOTAL_SAMPLES is unused.
profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
elif totalMethod == "callstacks":
# Use the actual call chains for functions.
profile[TOTAL_SAMPLES] = profile[SAMPLES]
profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES)
# Then propagate that total time to the calls.
for function in compat_itervalues(profile.functions):
for call in compat_itervalues(function.calls):
if call.ratio is not None:
callee = profile.functions[call.callee_id]
call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
else:
assert False

return profile

def parse_event(self):
line = self.consume()

stack, count = line.rsplit(' ',maxsplit=1)
count=int(count)
self.profile[SAMPLES] += count

calls = stack.split(';')
functions = [self._make_function(call) for call in calls]

functions[-1][SAMPLES] += count

# TOTAL_SAMPLES excludes loops
for func in set(functions):
func[TOTAL_SAMPLES] += count

# add call data
callee = functions[-1]
for caller in reversed(functions[:-1]):
call = caller.calls.get(callee.id)
if call is None:
call = Call(callee.id)
call[SAMPLES2] = 0
caller.add_call(call)
call[SAMPLES2] += count
callee = caller

call_re = re.compile(r'^(?P<func>[^ ]+) \((?P<file>.*):(?P<line>[0-9]+)\)$')

def _make_function(self, call):
"""turn a call str into a Function
takes a call site, as found between semicolons in the input, and returns
a Function definition corresponding to that call site.
"""
mo = self.call_re.match(call)
if mo:
name, module, line = mo.groups()
func_id = "%s:%s" % (module, name)
else:
name = func_id = call
module = None

func = self.profile.functions.get(func_id)
if func is None:
func = Function(func_id, name)
func.module = module
func[SAMPLES] = 0
func[TOTAL_SAMPLES] = 0
self.profile.add_function(func)
return func


formats = {
"axe": AXEParser,
"callgrind": CallgrindParser,
"collapse": CollapseParser,
"hprof": HProfParser,
"json": JsonParser,
"oprofile": OprofileParser,
Expand Down
Loading

0 comments on commit 14e0d54

Please sign in to comment.