#!/usr/bin/env python3 # # Inspired by d3 and brendangregg's flamegraph svg: # - https://d3js.org # - https://github.com/brendangregg/FlameGraph # # prevent local imports if __name__ == "__main__": __import__('sys').path.pop(0) import bisect import collections as co import csv import fnmatch import itertools as it import json import math as mt import re import shlex import shutil import subprocess as sp # some nicer colors borrowed from Seaborn # note these include a non-opaque alpha COLORS = [ '#7995c4', # was '#4c72b0bf', # blue '#e6a37d', # was '#dd8452bf', # orange '#80be8e', # was '#55a868bf', # green '#d37a7d', # was '#c44e52bf', # red '#a195c6', # was '#8172b3bf', # purple '#ae9a88', # was '#937860bf', # brown '#e3a8d2', # was '#da8bc3bf', # pink '#a9a9a9', # was '#8c8c8cbf', # gray '#d9cb97', # was '#ccb974bf', # yellow '#8bc8da', # was '#64b5cdbf', # cyan ] COLORS_DARK = [ '#7997b7', # was '#a1c9f4bf', # blue '#bf8761', # was '#ffb482bf', # orange '#6aac79', # was '#8de5a1bf', # green '#bf7774', # was '#ff9f9bbf', # red '#9c8cbf', # was '#d0bbffbf', # purple '#a68c74', # was '#debb9bbf', # brown '#bb84ab', # was '#fab0e4bf', # pink '#9b9b9b', # was '#cfcfcfbf', # gray '#bfbe7a', # was '#fffea3bf', # yellow '#8bb5b4', # was '#b9f2f0bf', # cyan ] WIDTH = 750 HEIGHT = 350 FONT = ['sans-serif'] FONT_SIZE = 10 CODE_PATH = ['./scripts/code.py'] STACK_PATH = ['./scripts/stack.py'] CTX_PATH = ['./scripts/ctx.py'] # open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): import os if path == '-': if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) else: return open(path, mode, buffering) def iself(path): # check for an elf file's magic string (\x7fELF) with open(path, 'rb') as f: return f.read(4) == b'\x7fELF' # parse different data representations def dat(x, *args): try: # allow the first part of an a/b fraction if '/' in x: x, _ = x.split('/', 1) # first try as int try: return int(x, 0) except ValueError: pass # then try as float try: return float(x) except ValueError: pass # else give up raise ValueError("invalid dat %r" % x) # default on error? except ValueError as e: if args: return args[0] else: raise # a representation of optionally key-mapped attrs class CsvAttr: def __init__(self, attrs, defaults=None): if attrs is None: attrs = [] if isinstance(attrs, dict): attrs = attrs.items() # normalize self.attrs = [] self.keyed = co.OrderedDict() for attr in attrs: if not isinstance(attr, tuple): attr = ((), attr) if attr[0] in {None, (), (None,), ('*',)}: attr = ((), attr[1]) if not isinstance(attr[0], tuple): attr = ((attr[0],), attr[1]) self.attrs.append(attr) if attr[0] not in self.keyed: self.keyed[attr[0]] = [] self.keyed[attr[0]].append(attr[1]) # create attrs object for defaults if isinstance(defaults, CsvAttr): self.defaults = defaults elif defaults is not None: self.defaults = CsvAttr(defaults) else: self.defaults = None def __repr__(self): if self.defaults is None: return 'CsvAttr(%r)' % ( [(','.join(attr[0]), attr[1]) for attr in self.attrs]) else: return 'CsvAttr(%r, %r)' % ( [(','.join(attr[0]), attr[1]) for attr in self.attrs], [(','.join(attr[0]), attr[1]) for attr in self.defaults.attrs]) def __iter__(self): if () in self.keyed: return it.cycle(self.keyed[()]) elif self.defaults is not None: return iter(self.defaults) else: return iter(()) def __bool__(self): return bool(self.attrs) def __getitem__(self, key): if isinstance(key, tuple): if len(key) > 0 and not isinstance(key[0], str): i, key = key if not isinstance(key, tuple): key = (key,) else: i, key = 0, key elif isinstance(key, str): i, key = 0, (key,) else: i, key = key, () # try to lookup by key best = None for ks, vs in self.keyed.items(): prefix = [] for j, k in enumerate(ks): if j < len(key) and fnmatch.fnmatchcase(key[j], k): prefix.append(k) else: prefix = None break if prefix is not None and ( best is None or len(prefix) >= len(best[0])): best = (prefix, vs) if best is not None: # cycle based on index return best[1][i % len(best[1])] # fallback to defaults? if self.defaults is not None: return self.defaults[i, key] raise KeyError(i, key) def get(self, key, default=None): try: return self.__getitem__(key) except KeyError: return default def __contains__(self, key): try: self.__getitem__(key) return True except KeyError: return False # get all results for a given key def getall(self, key, default=None): if not isinstance(key, tuple): key = (key,) # try to lookup by key best = None for ks, vs in self.keyed.items(): prefix = [] for j, k in enumerate(ks): if j < len(key) and fnmatch.fnmatchcase(key[j], k): prefix.append(k) else: prefix = None break if prefix is not None and ( best is None or len(prefix) >= len(best[0])): best = (prefix, vs) if best is not None: return best[1] # fallback to defaults? if self.defaults is not None: return self.defaults.getall(key, default) raise default # a key function for sorting by key order def key(self, key): if not isinstance(key, tuple): key = (key,) best = None for i, ks in enumerate(self.keyed.keys()): prefix = [] for j, k in enumerate(ks): if j < len(key) and (not k or key[j] == k): prefix.append(k) else: prefix = None break if prefix is not None and ( best is None or len(prefix) >= len(best[0])): best = (prefix, i) if best is not None: return best[1] # fallback to defaults? if self.defaults is not None: return len(self.keyed) + self.defaults.key(key) return len(self.keyed) # parse %-escaped strings # # attrs can override __getitem__ for lazy attr generation def punescape(s, attrs=None): pattern = re.compile( '%[%n]' '|' '%x..' '|' '%u....' '|' '%U........' '|' '%\((?P[^)]*)\)' '(?P[+\- #0-9\.]*[sdboxXfFeEgG])') def unescape(m): if m.group()[1] == '%': return '%' elif m.group()[1] == 'n': return '\n' elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) elif m.group()[1] == '(': if attrs is not None: try: v = attrs[m.group('field')] except KeyError: return m.group() else: return m.group() f = m.group('format') if f[-1] in 'dboxX': if isinstance(v, str): v = dat(v, 0) v = int(v) elif f[-1] in 'fFeEgG': if isinstance(v, str): v = dat(v, 0) v = float(v) else: f = ('<' if '-' in f else '>') + f.replace('-', '') v = str(v) # note we need Python's new format syntax for binary return ('{:%s}' % f).format(v) else: assert False return re.sub(pattern, unescape, s) # a type to represent tiles class Tile: def __init__(self, key, children, *, x=None, y=None, width=None, height=None, depth=None, attrs=None, label=None, color=None): self.key = key if isinstance(children, list): self.children = children self.value = sum(c.value for c in children) else: self.children = [] self.value = children self.x = x self.y = y self.width = width self.height = height self.depth = depth self.attrs = attrs self.label = label self.color = color def __repr__(self): return 'Tile(%r, %r, x=%r, y=%r, width=%r, height=%r)' % ( ','.join(self.key), self.value, self.x, self.y, self.width, self.height) # recursively build heirarchy @staticmethod def merge(tiles, prefix=()): # organize by 'by' field tiles_ = co.OrderedDict() for t in tiles: if len(prefix)+1 >= len(t.key): tiles_[t.key] = t else: key = prefix + (t.key[len(prefix)],) if key not in tiles_: tiles_[key] = [] tiles_[key].append(t) tiles__ = [] for key, t in tiles_.items(): if isinstance(t, Tile): tiles__.append(t) else: tiles__.append(Tile.merge(t, key)) tiles_ = tiles__ return Tile(prefix, tiles_, depth=len(prefix)) def __lt__(self, other): return self.value < other.value def __le__(self, other): return self.value <= other.value def __gt__(self, other): return self.value > other.value def __ge__(self, other): return self.value >= other.value # recursive traversals def tiles(self): yield self for child in self.children: yield from child.tiles() def leaves(self): for t in self.tiles(): if not t.children: yield t # sort recursively def sort(self): self.children.sort(reverse=True) for t in self.children: t.sort() # recursive align to pixel boundaries def align(self): # this extra +0.1 and using points instead of width/height is # to help minimize rounding errors x0 = int(self.x+0.1) y0 = int(self.y+0.1) x1 = int(self.x+self.width+0.1) y1 = int(self.y+self.height+0.1) self.x = x0 self.y = y0 self.width = x1 - x0 self.height = y1 - y0 # recurse for t in self.children: t.align() # return some interesting info about these tiles def stat(self): leaves = list(self.leaves()) mean = self.value / max(len(leaves), 1) stddev = mt.sqrt(sum((t.value - mean)**2 for t in leaves) / max(len(leaves), 1)) min_ = min((t.value for t in leaves), default=0) max_ = max((t.value for t in leaves), default=0) return { 'total': self.value, 'mean': mean, 'stddev': stddev, 'min': min_, 'max': max_, } # bounded division, limits result to dividend, useful for avoiding # divide-by-zero issues def bdiv(a, b): return a / max(b, 1) # our partitioning schemes def partition_binary(children, total, x, y, width, height): sums = [0] for t in children: sums.append(sums[-1] + t.value) # recursively partition into a roughly weight-balanced binary tree def partition_(i, j, value, x, y, width, height): # no child? guess we're done if i == j: return # single child? assign the partition elif i == j-1: children[i].x = x children[i].y = y children[i].width = width children[i].height = height return # binary search to find best split index target = sums[i] + (value / 2) k = bisect.bisect(sums, target, i+1, j-1) # nudge split index if it results in less error if k > i+1 and (sums[k] - target) > (target - sums[k-1]): k -= 1 l = sums[k] - sums[i] r = value - l # split horizontally? if width > height: dx = bdiv(sums[k] - sums[i], value) * width partition_(i, k, l, x, y, dx, height) partition_(k, j, r, x+dx, y, width-dx, height) # split vertically? else: dy = bdiv(sums[k] - sums[i], value) * height partition_(i, k, l, x, y, width, dy) partition_(k, j, r, x, y+dy, width, height-dy) partition_(0, len(children), total, x, y, width, height) def partition_slice(children, total, x, y, width, height): # give each child a slice x_ = x for t in children: t.x = x_ t.y = y t.width = bdiv(t.value, total) * width t.height = height x_ += t.width def partition_dice(children, total, x, y, width, height): # give each child a slice y_ = y for t in children: t.x = x t.y = y_ t.width = width t.height = bdiv(t.value, total) * height y_ += t.height def partition_squarify(children, total, x, y, width, height, *, aspect_ratio=1/1): # this algorithm is described here: # https://www.win.tue.nl/~vanwijk/stm.pdf i = 0 x_ = x y_ = y total_ = total width_ = width height_ = height # note we don't really care about width vs height until # actually slicing ratio = max(aspect_ratio, 1/aspect_ratio) while i < len(children): # calculate initial aspect ratio sum_ = children[i].value min_ = children[i].value max_ = children[i].value w = total_ * bdiv(ratio, max(bdiv(width_, height_), bdiv(height_, width_))) ratio_ = max(bdiv(max_*w, sum_**2), bdiv(sum_**2, min_*w)) # keep adding children to this row/col until it starts to hurt # our aspect ratio j = i + 1 while j < len(children): sum__ = sum_ + children[j].value min__ = min(min_, children[j].value) max__ = max(max_, children[j].value) ratio__ = max(bdiv(max__*w, sum__**2), bdiv(sum__**2, min__*w)) if ratio__ > ratio_: break sum_ = sum__ min_ = min__ max_ = max__ ratio_ = ratio__ j += 1 # vertical col? dice horizontally? if width_ > height_: dx = bdiv(sum_, total_) * width_ partition_dice(children[i:j], sum_, x_, y_, dx, height_) x_ += dx width_ -= dx # horizontal row? slice vertically? else: dy = bdiv(sum_, total_) * height_ partition_slice(children[i:j], sum_, x_, y_, width_, dy) y_ += dy height_ -= dy # start partitioning the other direction total_ -= sum_ i = j def collect_code(obj_paths, *, code_path=CODE_PATH, **args): # note code-path may contain extra args cmd = code_path + ['-O-'] + obj_paths if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, stdout=sp.PIPE, universal_newlines=True, errors='replace', close_fds=False) code = json.load(proc.stdout) proc.wait() if proc.returncode != 0: raise sp.CalledProcessError(proc.returncode, proc.args) return code def collect_stack(ci_paths, *, stack_path=STACK_PATH, **args): # note stack-path may contain extra args cmd = stack_path + ['-O-', '--depth=2'] + ci_paths if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, stdout=sp.PIPE, universal_newlines=True, errors='replace', close_fds=False) stack = json.load(proc.stdout) proc.wait() if proc.returncode != 0: raise sp.CalledProcessError(proc.returncode, proc.args) return stack def collect_ctx(obj_paths, *, ctx_path=CTX_PATH, **args): # note stack-path may contain extra args cmd = ctx_path + ['-O-', '--depth=2', '--internal'] + obj_paths if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, stdout=sp.PIPE, universal_newlines=True, errors='replace', close_fds=False) ctx = json.load(proc.stdout) proc.wait() if proc.returncode != 0: raise sp.CalledProcessError(proc.returncode, proc.args) return ctx def main(paths, output, *, namespace_depth=2, quiet=False, labels=[], colors=[], width=None, height=None, no_header=False, no_mode=False, no_stack=False, stack_ratio=1/5, no_ctx=False, no_frames=False, tile_code=False, tile_stack=False, tile_frames=False, tile_ctx=False, tile_1=False, no_javascript=False, mode_callgraph=False, mode_deepest=False, mode_callees=False, mode_callers=False, to_scale=None, to_ratio=1/1, title=None, padding=1, no_label=False, tiny=False, nested=False, dark=False, font=FONT, font_size=FONT_SIZE, background=None, **args): # tiny mode? if tiny: if to_scale is None: to_scale = 1 no_header = True no_label = True no_stack = True no_javascript = True # default to tiling based on code if (not tile_code and not tile_stack and not tile_frames and not tile_ctx and not tile_1): tile_code = True # default to all modes if (not mode_callgraph and not mode_deepest and not mode_callees and not mode_callers): mode_callgraph = True mode_deepest = True mode_callees = True mode_callers = True # what colors/labels to use? colors_ = CsvAttr(colors, defaults=COLORS_DARK if dark else COLORS) labels_ = CsvAttr(labels) if background is not None: background_ = background elif dark: background_ = '#000000' else: background_ = '#ffffff' # figure out width/height if width is not None: width_ = width else: width_ = WIDTH if height is not None: height_ = height else: height_ = HEIGHT # try to parse files as CSV/JSON results = [] try: # if any file starts with elf magic (\x7fELF), assume input is # elf/callgraph files fs = [] for path in paths: f = openio(path) if f.buffer.peek(4)[:4] == b'\x7fELF': for f_ in fs: f_.close() raise StopIteration() fs.append(f) for f in fs: with f: # csv or json? assume json starts with [ is_json = (f.buffer.peek(1)[:1] == b'[') # read csv? if not is_json: results.extend(csv.DictReader(f, restval='')) # read json? else: results.extend(json.load(f)) # fall back to extracting code/stack/ctx info from elf/callgraph files except StopIteration: # figure out paths obj_paths = [] ci_paths = [] for path in paths: if iself(path): obj_paths.append(path) else: ci_paths.append(path) # find code/stack/ctx sizes if obj_paths: results.extend(collect_code(obj_paths, **args)) if ci_paths: results.extend(collect_stack(ci_paths, **args)) if obj_paths: results.extend(collect_ctx(obj_paths, **args)) # don't render code/stack/ctx results if we don't have any nil_code = not any('code_size' in r for r in results) nil_stack = not any('stack_limit' in r for r in results) nil_frames = not any('stack_frame' in r for r in results) nil_ctx = not any('ctx_size' in r for r in results) if nil_frames: no_frames = True if nil_ctx: no_ctx = True if no_frames and no_ctx: no_stack = True # merge code/stack/ctx results functions = co.OrderedDict() for r in results: if 'function' not in r: continue if r['function'] not in functions: functions[r['function']] = {'name': r['function']} # code things if 'code_size' in r: functions[r['function']]['code'] = dat(r['code_size']) # stack things, including callgraph if 'stack_frame' in r: functions[r['function']]['frame'] = dat(r['stack_frame']) if 'stack_limit' in r: functions[r['function']]['stack'] = dat(r['stack_limit'], mt.inf) if 'children' in r: if 'children' not in functions[r['function']]: functions[r['function']]['children'] = [] functions[r['function']]['children'].extend( r_['function'] for r_ in r['children'] if r_.get('stack_frame', '') != '') # ctx things, including any arguments if 'ctx_size' in r: functions[r['function']]['ctx'] = dat(r['ctx_size']) if 'children' in r: if 'args' not in functions[r['function']]: functions[r['function']]['args'] = [] functions[r['function']]['args'].extend( {'name': r_['function'], 'ctx': dat(r_['ctx_size']), 'attrs': r_} for r_ in r['children'] if r_.get('ctx_size', '') != '') # keep track of other attrs for punescaping if 'attrs' not in functions[r['function']]: functions[r['function']]['attrs'] = {} functions[r['function']]['attrs'].update(r) # stack.py returns infinity for recursive functions, so we need to # recompute a bounded stack limit to show something useful def limitof(k, f, seen=set()): # found a cycle? stop here if k in seen: return 0 limit = 0 for child in f.get('children', []): if child not in functions: continue limit = max(limit, limitof(child, functions[child], seen | {k})) return f['frame'] + limit for k, f in functions.items(): if 'stack' in f: if mt.isinf(f['stack']): f['limit'] = limitof(k, f) else: f['limit'] = f['stack'] # organize into subsystems namespace_pattern = re.compile('_*[^_]+(?:_*$)?') namespace_slice = slice(namespace_depth if namespace_depth else None) subsystems = {} for k, f in functions.items(): # ignore leading/trailing underscores f['subsystem'] = ''.join( namespace_pattern.findall(k)[ namespace_slice]) if f['subsystem'] not in subsystems: subsystems[f['subsystem']] = {'name': f['subsystem']} # include ctx in subsystems to give them different colors for _, f in functions.items(): for a in f.get('args', []): a['subsystem'] = a['name'] if a['subsystem'] not in subsystems: subsystems[a['subsystem']] = {'name': a['subsystem']} # sort to try to keep things reproducible functions = co.OrderedDict(sorted(functions.items())) subsystems = co.OrderedDict(sorted(subsystems.items())) # sum code/stack/ctx/attrs for punescaping for k, s in subsystems.items(): s['code'] = sum( f.get('code', 0) for f in functions.values() if f['subsystem'] == k) s['stack'] = max( (f.get('stack', 0) for f in functions.values() if f['subsystem'] == k), default=0) s['ctx'] = max( (f.get('ctx', 0) for f in functions.values() if f['subsystem'] == k), default=0) s['attrs'] = {k_: v_ for f in functions.values() if f['subsystem'] == k for k_, v_ in f['attrs'].items()} # also build totals totals = {} totals['code'] = sum( f.get('code', 0) for f in functions.values()) totals['stack'] = max( (f.get('stack', 0) for f in functions.values()), default=0) totals['ctx'] = max( (f.get('ctx', 0) for f in functions.values()), default=0) totals['count'] = len(functions) totals['attrs'] = {k: v for f in functions.values() for k, v in f['attrs'].items()} # assign colors to subsystems, note this is after sorting, but # before tile generation, we want code and stack tiles to have the # same color if they're in the same subsystem for i, (k, s) in enumerate(subsystems.items()): color__ = colors_[i, k] # don't punescape unless we have to if '%' in color__: color__ = punescape(color__, s['attrs'] | s) s['color'] = color__ # build code heirarchy code = Tile.merge( Tile( (f['subsystem'], f['name']), f.get('code', 0) if tile_code and not nil_code else f.get('stack', 0) if tile_stack and not nil_stack else f.get('frame', 0) if tile_frames and not nil_frames else f.get('ctx', 0) if tile_ctx and not nil_ctx else 1, attrs=f) for f in functions.values()) # assign colors/labels to code tiles for i, t in enumerate(code.leaves()): # skip the top tile, yes this can happen if we have no code if t.depth == 0: continue t.color = subsystems[t.attrs['subsystem']]['color'] if (i, t.attrs['name']) in labels_: label__ = labels_[i, t.attrs['name']] # don't punescape unless we have to if '%' in label__: label__ = punescape(label__, t.attrs['attrs'] | t.attrs) t.label = label__ else: t.label = '%s%s%s%s' % ( t.attrs['name'], '\ncode %d' % t.attrs.get('code', 0) if not nil_code else '', '\nstack %s' % (lambda s: '∞' if mt.isinf(s) else s)( t.attrs.get('stack', 0)) if not nil_frames else '', '\nctx %d' % t.attrs.get('ctx', 0) if not nil_ctx else '') # build stack heirarchies if not no_stack and not no_frames: stacks = co.OrderedDict() for k, f in functions.items(): stack = [] def rec(f, seen=set()): if f['name'] in seen: stack.append(f) return seen.add(f['name']) stack.append(f) if f.get('children'): hot = max(f['children'], key=lambda k: functions[k].get('limit', 0) if k not in seen else -1) rec(functions[hot], seen) rec(f) stacks[k] = Tile.merge( Tile( (f['name'],), f.get('frame', 0), attrs=f) for f in stack) # assign colors/labels to stack tiles for i, t in enumerate(stacks[k].leaves()): t.color = subsystems[t.attrs['subsystem']]['color'] if (i, t.attrs['name']) in labels_: label__ = labels_[i, t.attrs['name']] # don't punescape unless we have to if '%' in label__: label__ = punescape(label__, t.attrs['attrs'] | t.attrs) t.label = label__ else: t.label = '%s\nframe %d' % ( t.attrs['name'], t.attrs.get('frame', 0)) # build ctx heirarchies if not no_stack and not no_ctx: ctxs = co.OrderedDict() for k, f in functions.items(): if f.get('args'): args_ = f['args'] else: args_ = [{ 'name': k, 'subsystem': f['subsystem'], 'ctx': f.get('ctx', 0), 'attrs': f}] ctxs[k] = Tile.merge( Tile( (a['name'],), a.get('ctx', 0), attrs=a) for a in args_) # assign colors/labels to ctx tiles for i, t in enumerate(ctxs[k].leaves()): t.color = subsystems[t.attrs['subsystem']]['color'] if (i, t.attrs['name']) in labels_: label__ = labels_[i, t.attrs['name']] # don't punescape unless we have to if '%' in label__: label__ = punescape(label__, t.attrs['attrs'] | t.attrs) t.label = label__ else: t.label = '%s\nctx %d' % ( t.attrs['name'], t.attrs.get('ctx', 0)) # scale width/height if requested now that we have our data if (to_scale is not None and (width is None or height is None)): total_value = (totals.get('code', 0) if tile_code else totals.get('stack', 0) if tile_stack else totals.get('frame', 0) if tile_frames else totals.get('ctx', 0) if tile_ctx else totals.get('count', 0)) if total_value: # don't include header/stack in scale width__ = width_ height__ = height_ if not no_header: height__ -= mt.ceil(FONT_SIZE * 1.3) if not no_stack: width__ *= (1 - stack_ratio) # scale width only if height is not None: width__ = mt.ceil((total_value * to_scale) / max(height__, 1)) # scale height only elif width is not None: height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) # scale based on aspect-ratio else: width__ = mt.ceil(mt.sqrt(total_value * to_scale * to_ratio)) height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) if not no_stack: width__ /= (1 - stack_ratio) if not no_header: height__ += mt.ceil(FONT_SIZE * 1.3) width_ = width__ height_ = height__ # our general purpose partition function def partition(tile, **args): if tile.depth == 0: # apply top padding tile.x += padding tile.y += padding tile.width -= min(padding, tile.width) tile.height -= min(padding, tile.height) # apply bottom padding if not tile.children: tile.width -= min(padding, tile.width) tile.height -= min(padding, tile.height) x__ = tile.x y__ = tile.y width__ = tile.width height__ = tile.height else: # apply bottom padding if not tile.children: tile.width -= min(padding, tile.width) tile.height -= min(padding, tile.height) x__ = tile.x y__ = tile.y width__ = tile.width height__ = tile.height # partition via requested scheme if tile.children: if args.get('binary'): partition_binary(tile.children, tile.value, x__, y__, width__, height__) elif (args.get('slice') or (args.get('slice_and_dice') and (tile.depth & 1) == 0) or (args.get('dice_and_slice') and (tile.depth & 1) == 1)): partition_slice(tile.children, tile.value, x__, y__, width__, height__) elif (args.get('dice') or (args.get('slice_and_dice') and (tile.depth & 1) == 1) or (args.get('dice_and_slice') and (tile.depth & 1) == 0)): partition_dice(tile.children, tile.value, x__, y__, width__, height__) elif (args.get('squarify') or args.get('squarify_ratio') or args.get('rectify')): partition_squarify(tile.children, tile.value, x__, y__, width__, height__, aspect_ratio=( args['squarify_ratio'] if args.get('squarify_ratio') else width_/height_ if args.get('rectify') else 1/1)) else: # default to binary partitioning partition_binary(tile.children, tile.value, x__, y__, width__, height__) # recursively partition for t in tile.children: partition(t, **args) # create space for header x__ = 0 y__ = 0 width__ = width_ height__ = height_ if not no_header: y__ += mt.ceil(FONT_SIZE * 1.3) height__ -= min(mt.ceil(FONT_SIZE * 1.3), height__) # split code/stack if not no_stack: code_split = width__ * (1 - stack_ratio) else: code_split = width__ # sort and partition code code.sort() code.x = x__ code.y = y__ code.width = code_split code.height = height__ partition(code, **args) # align to pixel boundaries code.align() # partition stacks/ctxs if not no_stack: deepest = max(functions.values(), key=lambda f: (f.get('limit', 0) if not no_frames else 0) + (f.get('ctx', 0) if not no_ctx else 0)) for k, f in functions.items(): # scale to deepest stack/ctx height___ = height__ * bdiv( (f.get('limit', 0) if not no_frames else 0) + (f.get('ctx', 0) if not no_ctx else 0), (deepest.get('limit', 0) if not no_frames else 0) + (deepest.get('ctx', 0) if not no_ctx else 0)) # split stack/ctx ctx_split = height___ * bdiv( (f.get('ctx', 0) if not no_ctx else 0), (f.get('limit', 0) if not no_frames else 0) + (f.get('ctx', 0) if not no_ctx else 0)) # partition ctx if not no_ctx: ctx = ctxs[k] ctx.x = code.x + code.width + 1 ctx.y = y__ ctx.width = width__ - ctx.x ctx.height = ctx_split partition(ctx, slice=True) # align to pixel boundaries ctx.align() # partition stack if not no_frames: stack = stacks[k] stack.x = code.x + code.width + 1 stack.y = ctx.y + ctx.height + 1 if ctx_split > 0 else y__ stack.width = width__ - stack.x stack.height = height___ - (stack.y - y__) partition(stack, dice=True) # align to pixel boundaries stack.align() # create svg file with openio(output, 'w') as f: def writeln(s=''): f.write(s) f.write('\n') f.writeln = writeln # yes this is svg f.write('' % dict( width=width_, height=height_, font=','.join(font), font_size=font_size, background=background_, user_select='none' if not no_javascript else 'auto')) # create header if not no_header: f.write('' % dict( js= 'cursor="pointer" ' 'onclick="click_header(this,event)">' if not no_javascript else '')) # add an invisible rect to make things more clickable f.write('' % dict( x=0, y=0, width=width_, height=y__)) f.write('') f.write('' % dict( color='#ffffff' if dark else '#000000')) f.write('') if title: f.write(punescape(title, totals['attrs'] | totals)) else: f.write('code %d stack %s ctx %d' % ( totals.get('code', 0), (lambda s: '∞' if mt.isinf(s) else s)( totals.get('stack', 0)), totals.get('ctx', 0))) f.write('') if not no_mode and not no_javascript: f.write('' % dict( x=width_-3)) f.write('mode: %s' % ( 'callgraph' if mode_callgraph else 'deepest' if mode_deepest else 'callees' if mode_callees else 'callers')) f.write('') f.write('') f.write('') # create code tiles for i, t in enumerate(code.leaves()): # skip the top tile, yes this can happen if we have no code if t.depth == 0: continue # skip anything with zero weight/height after aligning things if t.width == 0 or t.height == 0: continue f.write('' % dict( name=t.attrs['name'], x=t.x, y=t.y, js= 'data-name="%(name)s" ' # precompute x/y for javascript, svg makes this # weirdly difficult to figure out post-transform 'data-x="%(x)d" ' 'data-y="%(y)d" ' 'data-width="%(width)d" ' 'data-height="%(height)d" ' 'onmouseenter="enter_tile(this,event)" ' 'onmouseleave="leave_tile(this,event)" ' 'onclick="click_tile(this,event)">' % dict( name=t.attrs['name'], x=t.x, y=t.y, width=t.width, height=t.height) if not no_javascript else '')) # add an invisible rect to make things more clickable f.write('' % dict( width=t.width + padding, height=t.height + padding)) f.write('') f.write('') f.write(t.label) f.write('') f.write('' % dict( id=i, color=t.color, width=t.width, height=t.height)) f.write('') if not no_label: f.write('' % i) f.write('' % i) f.write('') f.write('') f.write('' % i) for j, l in enumerate(t.label.split('\n')): if j == 0: f.write('') f.write(l) f.write('') else: if t.children: f.write('') f.write(l) f.write('') else: f.write('') f.write(l) f.write('') f.write('') f.write('') # create stack/ctx tiles if not no_stack and (not no_ctx or not no_frames): for i, k in enumerate(functions.keys()): # only include the deepest stack if no_javascript, no reason to # include a bunch of tiles we will never render if no_javascript and functions[k]['name'] != deepest['name']: continue # create stack group # # note we conveniently don't need unique ids for each ctx/frame # tile, just for the entire stack group f.write('' % dict( name=k, js= 'visibility="%(visibility)s">' % dict( visibility="visible" if functions[k]['name'] == deepest['name'] else "hidden") if not no_javascript else '')) # add a separator between code/stack f.write('' % dict( x=code.x + code.width, y=code.y, width=1, height=max( stacks[k].y + stacks[k].height if not no_frames else 0, ctxs[k].y + ctxs[k].height if not no_ctx else 0) - code.y - padding, color='#7f7f7f' if dark else '#555555')) f.write('') # create ctx tiles if not no_ctx: for j, t in enumerate(ctxs[k].leaves()): # skip anything with zero weight/height after aligning things if t.width == 0 or t.height == 0: continue f.write('' % dict( id='%s-%s' % (i, j), x=t.x, y=t.y, js= 'data-name="%(name)s" ' 'data-func="%(func)s" ' # precompute x/y for javascript, svg makes # this weirdly difficult to figure out # post-transform 'data-x="%(x)d" ' 'data-y="%(y)d" ' 'data-width="%(width)d" ' 'data-height="%(height)d" ' 'onmouseenter="enter_tile(this,event)" ' 'onmouseleave="leave_tile(this,event)" ' 'onclick="click_tile(this,event)">' % dict( name=t.attrs['name'], func=k, x=t.x, y=t.y, width=t.width, height=t.height) if not no_javascript else '')) # add an invisible rect to make things more clickable f.write('' % dict( width=t.width + padding, height=t.height + padding)) f.write('') f.write('') f.write(t.label) f.write('') f.write('' % dict( id='%s-%s' % (i, j), color=t.color, width=t.width, height=t.height)) f.write('') if not no_label: f.write('' % ('%s-%s' % (i, j))) f.write('' % ('%s-%s' % (i, j))) f.write('') f.write('') f.write('' % ( '%s-%s' % (i, j))) for j, l in enumerate(t.label.split('\n')): if j == 0: f.write('') f.write(l) f.write('') else: if t.children: f.write('') f.write(l) f.write('') else: f.write('') f.write(l) f.write('') f.write('') f.write('') # add a separator between ctx/stack if not no_ctx and not no_frames: f.write('' % dict( x=ctxs[k].x, y=ctxs[k].y + ctxs[k].height, width=ctxs[k].width - padding, height=1, color='#7f7f7f' if dark else '#555555')) f.write('') # create stack tiles if not no_frames: for j, t in enumerate(stacks[k].leaves()): # skip anything with zero weight/height after aligning things if t.width == 0 or t.height == 0: continue f.write('' % dict( id='%s-%s' % (i, j), x=t.x, y=t.y, js= 'data-name="%(name)s" ' 'data-func="%(func)s" ' # precompute x/y for javascript, svg makes # this weirdly difficult to figure out # post-transform 'data-x="%(x)d" ' 'data-y="%(y)d" ' 'data-width="%(width)d" ' 'data-height="%(height)d" ' 'onmouseenter="enter_tile(this,event)" ' 'onmouseleave="leave_tile(this,event)" ' 'onclick="click_tile(this,event)"' % dict( name=t.attrs['name'], func=k, x=t.x, y=t.y, width=t.width, height=t.height) if not no_javascript else '')) # add an invisible rect to make things more clickable f.write('' % dict( width=t.width + padding, height=t.height + padding)) f.write('') f.write('') f.write(t.label) f.write('') f.write('' % dict( id='%s-%s' % (i, j), color=t.color, width=t.width, height=t.height)) f.write('') if not no_label: f.write('' % ('%s-%s' % (i, j))) f.write('' % ('%s-%s' % (i, j))) f.write('') f.write('') f.write('' % ( '%s-%s' % (i, j))) for j, l in enumerate(t.label.split('\n')): if j == 0: f.write('') f.write(l) f.write('') else: if t.children: f.write('') f.write(l) f.write('') else: f.write('') f.write(l) f.write('') f.write('') f.write('') f.write('') if not no_javascript: # arrowhead for arrows f.write('') f.write('' % dict( color='#000000' if dark else '#555555')) f.write('') f.write('') f.write('') # javascript for arrows # # why tf does svg support javascript? f.write('') f.write('') # print some summary info if not quiet: stat = code.stat() print('updated %s, code %d stack %s ctx %d' % ( output, totals.get('code', 0), (lambda s: '∞' if mt.isinf(s) else s)( totals.get('stack', 0)), totals.get('ctx', 0))) if (args.get('error_on_recursion') and mt.isinf(totals.get('stack', 0))): sys.exit(2) if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( description="Render code info as an interactive SVG treemap.", allow_abbrev=False) class AppendPath(argparse.Action): def __call__(self, parser, namespace, value, option): if getattr(namespace, 'paths', None) is None: namespace.paths = [] if value is None: pass elif isinstance(value, str): namespace.paths.append(value) else: namespace.paths.extend(value) parser.add_argument( 'obj_paths', nargs='*', action=AppendPath, help="Input *.o files.") parser.add_argument( 'ci_paths', nargs='*', action=AppendPath, help="Input *.ci files.") parser.add_argument( 'csv_paths', nargs='*', action=AppendPath, help="Input *.csv files.") parser.add_argument( 'json_paths', nargs='*', action=AppendPath, help="Input *.json files.") parser.add_argument( '-o', '--output', required=True, help="Output *.svg file.") parser.add_argument( '-_', '--namespace-depth', nargs='?', type=lambda x: int(x, 0), const=0, help="Number of underscore-separated namespaces to partition by. " "0 treats every function as its own subsystem, while -1 uses " "the longest matching prefix. Defaults to 2, which is " "probably a good level of detail for most standalone " "libraries.") parser.add_argument( '-v', '--verbose', action='store_true', help="Output commands that run behind the scenes.") parser.add_argument( '-q', '--quiet', action='store_true', help="Don't print info.") parser.add_argument( '-L', '--add-label', dest='labels', action='append', type=lambda x: ( lambda ks, v: ( tuple(k.strip() for k in ks.split(',')), v.strip()) )(*x.split('=', 1)) if '=' in x else x.strip(), help="Add a label to use. Can be assigned to a specific " "function/subsystem. Accepts %% modifiers.") parser.add_argument( '-C', '--add-color', dest='colors', action='append', type=lambda x: ( lambda ks, v: ( tuple(k.strip() for k in ks.split(',')), v.strip()) )(*x.split('=', 1)) if '=' in x else x.strip(), help="Add a color to use. Can be assigned to a specific " "function/subsystem. Accepts %% modifiers.") parser.add_argument( '-W', '--width', type=lambda x: int(x, 0), help="Width in pixels. Defaults to %r." % WIDTH) parser.add_argument( '-H', '--height', type=lambda x: int(x, 0), help="Height in pixels. Defaults to %r." % HEIGHT) parser.add_argument( '--no-header', action='store_true', help="Don't show the header.") parser.add_argument( '--no-mode', action='store_true', help="Don't show the mode state.") parser.add_argument( '-S', '--no-stack', action='store_true', help="Don't render any stack info.") parser.add_argument( '-s', '--stack-ratio', type=lambda x: ( (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) if ':' in x else float(x)), help="Ratio of width to use for stack info. Defaults to 1:5.") parser.add_argument( '--no-ctx', action='store_true', help="Don't render function context.") parser.add_argument( '--no-frames', action='store_true', help="Don't render function stack frame info.") parser.add_argument( '--tile-code', action='store_true', help="Tile based on code size. This is the default.") parser.add_argument( '--tile-stack', action='store_true', help="Tile based on stack limits.") parser.add_argument( '--tile-frames', action='store_true', help="Tile based on stack frames.") parser.add_argument( '--tile-ctx', action='store_true', help="Tile based on function context.") parser.add_argument( '--tile-1', action='store_true', help="Tile functions evenly.") parser.add_argument( '-J', '--no-javascript', action='store_true', help="Don't add javascript for interactability.") parser.add_argument( '--mode-callgraph', action='store_true', help="Include the callgraph rendering mode.") parser.add_argument( '--mode-deepest', action='store_true', help="Include the deepest rendering mode.") parser.add_argument( '--mode-callees', action='store_true', help="Include the callees rendering mode.") parser.add_argument( '--mode-callers', action='store_true', help="Include the callers rendering mode.") parser.add_argument( '--binary', action='store_true', help="Use the binary partitioning scheme. This attempts to " "recursively subdivide the tiles into a roughly " "weight-balanced binary tree. This is the default.") parser.add_argument( '--slice', action='store_true', help="Use the slice partitioning scheme. This simply slices " "tiles vertically.") parser.add_argument( '--dice', action='store_true', help="Use the dice partitioning scheme. This simply slices " "tiles horizontally.") parser.add_argument( '--slice-and-dice', action='store_true', help="Use the slice-and-dice partitioning scheme. This " "alternates between slicing and dicing each layer.") parser.add_argument( '--dice-and-slice', action='store_true', help="Use the dice-and-slice partitioning scheme. This is like " "slice-and-dice, but flipped.") parser.add_argument( '--squarify', action='store_true', help="Use the squarify partitioning scheme. This is a greedy " "algorithm created by Mark Bruls et al that tries to " "minimize tile aspect ratios.") parser.add_argument( '--rectify', action='store_true', help="Use the rectify partitioning scheme. This is like " "squarify, but tries to match the aspect ratio of the " "window.") parser.add_argument( '--squarify-ratio', type=lambda x: ( (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) if ':' in x else float(x)), help="Specify an explicit aspect ratio for the squarify " "algorithm. Implies --squarify.") parser.add_argument( '--to-scale', nargs='?', type=lambda x: ( (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) if ':' in x else float(x)), const=1, help="Scale the resulting treemap such that 1 pixel ~= 1/scale " "units. Defaults to scale=1. ") parser.add_argument( '--to-ratio', type=lambda x: ( (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) if ':' in x else float(x)), help="Aspect ratio to use with --to-scale. Defaults to 1:1.") parser.add_argument( '-t', '--tiny', action='store_true', help="Tiny mode, alias for --to-scale=1, --no-header, " "--no-label, --no-stack, and --no-javascript.") parser.add_argument( '--title', help="Add a title. Accepts %% modifiers.") parser.add_argument( '--padding', type=float, help="Padding to add to each level of the treemap. Defaults to 1.") parser.add_argument( '--no-label', action='store_true', help="Don't render any labels.") parser.add_argument( '--dark', action='store_true', help="Use the dark style.") parser.add_argument( '--font', type=lambda x: [x.strip() for x in x.split(',')], help="Font family to use.") parser.add_argument( '--font-size', help="Font size to use. Defaults to %r." % FONT_SIZE) parser.add_argument( '--background', help="Background color to use. Note #00000000 can make the " "background transparent.") parser.add_argument( '-e', '--error-on-recursion', action='store_true', help="Error if any functions are recursive.") parser.add_argument( '--code-path', type=lambda x: x.split(), default=CODE_PATH, help="Path to the code.py script, may include flags. " "Defaults to %r." % CODE_PATH) parser.add_argument( '--stack-path', type=lambda x: x.split(), default=STACK_PATH, help="Path to the stack.py script, may include flags. " "Defaults to %r." % STACK_PATH) parser.add_argument( '--ctx-path', type=lambda x: x.split(), default=CTX_PATH, help="Path to the ctx.py script, may include flags. " "Defaults to %r." % CTX_PATH) sys.exit(main(**{k: v for k, v in vars(parser.parse_intermixed_args()).items() if v is not None}))