Files
littlefs/scripts/codemapsvg.py
Christopher Haster 8cc81aef7d scripts: Adopt __get__ binding for write/writeln methods
This actually binds our custom write/writeln functions as methods to the
file object:

  def writeln(self, s=''):
      self.write(s)
      self.write('\n')
  f.writeln = writeln.__get__(f)

This doesn't really gain us anything, but is a bit more correct and may
be safer if other code messes with the file's internals.
2025-06-27 12:56:03 -05:00

2388 lines
89 KiB
Python
Executable File

#!/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<field>[^)]*)\)'
'(?P<format>[+\- #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(self, s=''):
self.write(s)
self.write('\n')
f.writeln = writeln.__get__(f)
# yes this is svg
f.write('<svg '
'xmlns="http://www.w3.org/2000/svg" '
'viewBox="0,0,%(width)d,%(height)d" '
'width="%(width)d" '
'height="%(height)d" '
'style="font: %(font_size)dpx %(font)s; '
'background-color: %(background)s; '
'user-select: %(user_select)s;">' % 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('<g '
'id="header" '
'%(js)s>' % 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('<rect '
'x="%(x)d" '
'y="%(y)d" '
'width="%(width)d" '
'height="%(height)d" '
'opacity="0">' % dict(
x=0,
y=0,
width=width_,
height=y__))
f.write('</rect>')
f.write('<text fill="%(color)s">' % dict(
color='#ffffff' if dark else '#000000'))
f.write('<tspan x="3" y="1.1em">')
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('</tspan>')
if not no_mode and not no_javascript:
f.write('<tspan id="mode" x="%(x)d" y="1.1em" '
'text-anchor="end">' % 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('</tspan>')
f.write('</text>')
f.write('</g>')
# 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('<g '
'id="c-%(name)s" '
'class="tile code" '
'transform="translate(%(x)d,%(y)d)" '
'%(js)s>' % 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('<rect '
'width="%(width)d" '
'height="%(height)d" '
'opacity="0">' % dict(
width=t.width + padding,
height=t.height + padding))
f.write('</rect>')
f.write('<title>')
f.write(t.label)
f.write('</title>')
f.write('<rect '
'id="c-tile-%(id)s" '
'fill="%(color)s" '
'width="%(width)d" '
'height="%(height)d">' % dict(
id=i,
color=t.color,
width=t.width,
height=t.height))
f.write('</rect>')
if not no_label:
f.write('<clipPath id="c-clip-%s">' % i)
f.write('<use href="#c-tile-%s">' % i)
f.write('</use>')
f.write('</clipPath>')
f.write('<text clip-path="url(#c-clip-%s)">' % i)
for j, l in enumerate(t.label.split('\n')):
if j == 0:
f.write('<tspan x="3" y="1.1em">')
f.write(l)
f.write('</tspan>')
else:
if t.children:
f.write('<tspan dx="3" y="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
else:
f.write('<tspan x="3" dy="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
f.write('</text>')
f.write('</g>')
# 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('<g '
'id="s-%(name)s" '
'class="stack" '
'%(js)s>' % 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('<rect '
'x="%(x)d" '
'y="%(y)d" '
'width="%(width)d" '
'height="%(height)d" '
'fill="%(color)s">' % 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('</rect>')
# 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('<g '
'id="x-%(id)s" '
'class="tile ctx" '
'transform="translate(%(x)d,%(y)d)" '
'%(js)s>' % 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('<rect '
'width="%(width)d" '
'height="%(height)d" '
'opacity="0">' % dict(
width=t.width + padding,
height=t.height + padding))
f.write('</rect>')
f.write('<title>')
f.write(t.label)
f.write('</title>')
f.write('<rect '
'id="x-tile-%(id)s" '
'fill="%(color)s" '
'width="%(width)d" '
'height="%(height)d">' % dict(
id='%s-%s' % (i, j),
color=t.color,
width=t.width,
height=t.height))
f.write('</rect>')
if not no_label:
f.write('<clipPath id="x-clip-%s">' % ('%s-%s' % (i, j)))
f.write('<use href="#x-tile-%s">' % ('%s-%s' % (i, j)))
f.write('</use>')
f.write('</clipPath>')
f.write('<text clip-path="url(#x-clip-%s)">' % (
'%s-%s' % (i, j)))
for j, l in enumerate(t.label.split('\n')):
if j == 0:
f.write('<tspan x="3" y="1.1em">')
f.write(l)
f.write('</tspan>')
else:
if t.children:
f.write('<tspan dx="3" y="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
else:
f.write('<tspan x="3" dy="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
f.write('</text>')
f.write('</g>')
# add a separator between ctx/stack
if not no_ctx and not no_frames:
f.write('<rect '
'x="%(x)d" '
'y="%(y)d" '
'width="%(width)d" '
'height="%(height)d" '
'fill="%(color)s">' % 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('</rect>')
# 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('<g '
'id="f-%(id)s" '
'class="tile frame" '
'transform="translate(%(x)d,%(y)d)" '
'%(js)s>' % 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('<rect '
'width="%(width)d" '
'height="%(height)d" '
'opacity="0">' % dict(
width=t.width + padding,
height=t.height + padding))
f.write('</rect>')
f.write('<title>')
f.write(t.label)
f.write('</title>')
f.write('<rect '
'id="f-tile-%(id)s" '
'fill="%(color)s" '
'width="%(width)d" '
'height="%(height)d">' % dict(
id='%s-%s' % (i, j),
color=t.color,
width=t.width,
height=t.height))
f.write('</rect>')
if not no_label:
f.write('<clipPath id="f-clip-%s">' % ('%s-%s' % (i, j)))
f.write('<use href="#f-tile-%s">' % ('%s-%s' % (i, j)))
f.write('</use>')
f.write('</clipPath>')
f.write('<text clip-path="url(#f-clip-%s)">' % (
'%s-%s' % (i, j)))
for j, l in enumerate(t.label.split('\n')):
if j == 0:
f.write('<tspan x="3" y="1.1em">')
f.write(l)
f.write('</tspan>')
else:
if t.children:
f.write('<tspan dx="3" y="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
else:
f.write('<tspan x="3" dy="1.1em" '
'fill-opacity="0.7">')
f.write(l)
f.write('</tspan>')
f.write('</text>')
f.write('</g>')
f.write('</g>')
if not no_javascript:
# arrowhead for arrows
f.write('<defs>')
f.write('<marker '
'id="arrowhead" '
'viewBox="0 0 10 10" '
'refX="10" '
'refY="5" '
'markerWidth="6" '
'markerHeight="6" '
'orient="auto-start-reverse" '
'fill="%(color)s">' % dict(
color='#000000' if dark else '#555555'))
f.write('<path d="M 0 0 L 10 5 L 0 10 z"/>')
f.write('</marker>')
f.write('</defs>')
# javascript for arrows
#
# why tf does svg support javascript?
f.write('<script><![CDATA[')
# embed our callgraph
f.write('const children = %s;' % json.dumps(
{f['name']: sorted(f.get('children', []),
key=lambda c: functions[c]['limit'],
reverse=True)
for f in functions.values()},
separators=(',', ':')))
# function for rect <-> line interesection
f.write('function rect_intersect(x, y, width, height, l_x, l_y) {')
f.write( 'let r_x = (x + width/2);')
f.write( 'let r_y = (y + height/2);')
f.write( 'let dx = l_x - r_x;')
f.write( 'let dy = l_y - r_y;')
f.write( 'let θ = Math.abs(dy / dx);')
f.write( 'let φ = height / width;')
f.write( 'if (θ > φ) {')
f.write( 'return [')
f.write( 'r_x + ((height/2)/θ)*Math.sign(dx),')
f.write( 'r_y + (height/2)*Math.sign(dy),')
f.write( '];')
f.write( '} else {')
f.write( 'return [')
f.write( 'r_x + (width/2)*Math.sign(dx),')
f.write( 'r_y + ((width/2)*θ)*Math.sign(dy),')
f.write( '];')
f.write( '}')
f.write('}')
# our main drawing functions
f.write('function draw_unfocus() {')
# lower opacity of unfocused tiles
f.write( 'for (let b of document.querySelectorAll(".tile")) {')
f.write( 'b.setAttribute("fill-opacity", 0.5);')
f.write( '}')
f.write('}')
f.write('function draw_focus(a) {')
# revert opacity and move to top
f.write( 'a.setAttribute("fill-opacity", 1);')
f.write( 'a.parentElement.appendChild(a);')
f.write('}')
# draw an arrow
f.write('function draw_arrow(a, b) {')
# no self-referential arrows
f.write( 'if (b == a) {')
f.write( 'return;')
f.write( '}')
# figure out rect intersections
f.write( 'let svg = document.documentElement;')
f.write( 'let ns = svg.getAttribute("xmlns");')
f.write( 'let a_x = parseInt(a.dataset.x);')
f.write( 'let a_y = parseInt(a.dataset.y);')
f.write( 'let a_width = parseInt(a.dataset.width);')
f.write( 'let a_height = parseInt(a.dataset.height);')
f.write( 'let b_x = parseInt(b.dataset.x);')
f.write( 'let b_y = parseInt(b.dataset.y);')
f.write( 'let b_width = parseInt(b.dataset.width);')
f.write( 'let b_height = parseInt(b.dataset.height);')
f.write( 'let [a_ix, a_iy] = rect_intersect(')
f.write( 'a_x, a_y, a_width, a_height,')
f.write( 'b_x + b_width/2, b_y + b_height/2);')
f.write( 'let [b_ix, b_iy] = rect_intersect(')
f.write( 'b_x, b_y, b_width, b_height,')
f.write( 'a_x + a_width/2, a_y + a_height/2);')
# create the actual arrow
f.write( 'let arrow = document.createElementNS(ns, "line");')
f.write( 'arrow.classList.add("arrow");')
f.write( 'arrow.setAttribute("x1", a_ix);')
f.write( 'arrow.setAttribute("y1", a_iy);')
f.write( 'arrow.setAttribute("x2", b_ix);')
f.write( 'arrow.setAttribute("y2", b_iy);')
f.write( 'arrow.setAttribute("stroke", "%(color)s");' % dict(
color='#000000' if dark else '#555555'))
f.write( 'arrow.setAttribute("marker-end", "url(#arrowhead)");')
f.write( 'arrow.setAttribute("pointer-events", "none");')
f.write( 'a.parentElement.appendChild(arrow);')
f.write('}')
# here are some drawing modes to choose from
# draw full callgraph
f.write('function draw_callgraph(a) {')
# track visited children to avoid cycles
f.write( 'let seen = {};')
# create new arrows
f.write( 'let recurse = function(a) {')
f.write( 'if (a.dataset.name in seen) {')
f.write( 'return;')
f.write( '}')
f.write( 'seen[a.dataset.name] = true;')
f.write( 'for (let child of ')
f.write( 'children[a.dataset.name] || []) {')
f.write( 'let b = document.getElementById("c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_arrow(a, b);')
f.write( 'recurse(b);')
f.write( '}')
f.write( '}')
f.write( '};')
f.write( 'recurse(a);')
# track visited children to avoid cycles
f.write( 'seen = {};')
# move in-focus tiles to the top
f.write( 'recurse = function(a) {')
f.write( 'if (a.dataset.name in seen) {')
f.write( 'return;')
f.write( '}')
f.write( 'seen[a.dataset.name] = true;')
f.write( 'for (let child of ')
f.write( 'children[a.dataset.name] || []) {')
f.write( 'let b = document.getElementById("c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_focus(b);')
f.write( 'recurse(b);')
f.write( '}')
f.write( '}')
f.write( '};')
f.write( 'recurse(a);')
# move our tile to the top
f.write( 'draw_focus(a);')
f.write('}')
# draw deepest set of calls
f.write('function draw_deepest(a) {')
# track visited children to avoid cycles
f.write( 'let seen = {};')
# create/ new arrows
f.write( 'let recurse = function(a) {')
f.write( 'if (a.dataset.name in seen) {')
f.write( 'return;')
f.write( '}')
f.write( 'seen[a.dataset.name] = true;')
f.write( 'if (children[a.dataset.name]) {')
# draw recursive arrows to show cycles
f.write( 'if (children[a.dataset.name][0] in seen) {')
f.write( 'let child = children[a.dataset.name][0];')
f.write( 'let b = document.getElementById('
'"c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_arrow(a, b);')
f.write( '}')
f.write( '}')
# but descend down the deepest non-recursive
# child to show useful stack info
f.write( 'let child = children[a.dataset.name]'
'.find((child) => !(child in seen));')
f.write( 'if (child) {')
f.write( 'let b = document.getElementById('
'"c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_arrow(a, b);')
f.write( 'recurse(b);')
f.write( '}')
f.write( '}')
f.write( '}')
f.write( '};')
f.write( 'recurse(a);')
# track visited children to avoid cycles
f.write( 'seen = {};')
# move in-focus tiles to the top
f.write( 'recurse = function(a) {')
f.write( 'if (a.dataset.name in seen) {')
f.write( 'return;')
f.write( '}')
f.write( 'seen[a.dataset.name] = true;')
f.write( 'if (children[a.dataset.name]) {')
f.write( 'let child = children[a.dataset.name]'
'.find((child) => !(child in seen));')
f.write( 'if (child) {')
f.write( 'let b = document.getElementById('
'"c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_focus(b);')
f.write( 'recurse(b);')
f.write( '}')
f.write( '}')
f.write( '}')
f.write( '};')
f.write( 'recurse(a);')
# move our tile to the top
f.write( 'draw_focus(a);')
f.write('}')
# draw one level of calls
f.write('function draw_callees(a) {')
# create new arrows
f.write( 'for (let child of children[a.dataset.name] || []) {')
f.write( 'let b = document.getElementById("c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_arrow(a, b);')
f.write( '}')
f.write( '}')
# move in-focus tiles to the top
f.write( 'for (let child of children[a.dataset.name] || []) {')
f.write( 'let b = document.getElementById("c-"+child);')
f.write( 'if (b) {')
f.write( 'draw_focus(b);')
f.write( '}')
f.write( '}')
# move our tile to the top
f.write( 'draw_focus(a);')
f.write('}')
# draw one level of callers
f.write('function draw_callers(a) {')
# create new arrows
f.write( 'for (let parent in children) {')
f.write( 'if ((children[parent] || []).includes(')
f.write( 'a.dataset.name)) {')
f.write( 'let b = document.getElementById('
'"c-"+parent);')
f.write( 'if (b) {')
f.write( 'draw_arrow(b, a);')
f.write( '}')
f.write( '}')
f.write( '}')
# move in-focus tiles to the top
f.write( 'for (let parent in children) {')
f.write( 'if ((children[parent] || []).includes(')
f.write( 'a.dataset.name)) {')
f.write( 'let b = document.getElementById('
'"c-"+parent);')
f.write( 'if (b) {')
f.write( 'draw_focus(b);')
f.write( '}')
f.write( '}')
f.write( '}')
# move our tile to the top
f.write( 'draw_focus(a);')
f.write('}')
# clear old arrows/tiles if we leave
f.write('function undraw() {')
# clear arrows
f.write( 'for (let arrow of document.querySelectorAll('
'".arrow")) {')
f.write( 'arrow.remove();')
f.write( '}')
# revert opacity
f.write( 'for (let b of document.querySelectorAll(".tile")) {')
f.write( 'b.setAttribute("fill-opacity", 1);')
f.write( '}')
f.write('}')
# render stack+ctx tiles
f.write('function switch_stack(name) {')
# update stack tiles
f.write( 'for (let b of document.querySelectorAll(".stack")) {')
f.write( 'b.setAttribute("visibility", "hidden");')
f.write( '}')
f.write( 'let s = document.getElementById("s-"+name);')
f.write( 'if (s) {')
# make visible
f.write( 's.setAttribute("visibility", "visible");')
# and refocus stack tiles in case they were
# unfocused
f.write( 'for (let b of s.querySelectorAll('
'".frame,.ctx")) {')
f.write( 'draw_focus(b);')
f.write( '}')
f.write( '}')
f.write('}')
# draw stack frame/ctx tile
f.write('function draw_stack(a) {')
# if a is null we just refocus the stack
f.write( 'if (!a) {')
f.write( 'let s = document.querySelector('
'".stack[visibility=\\"visible\\"]");')
f.write( 'for (let b of s.querySelectorAll('
'".frame,.ctx")) {')
f.write( 'draw_focus(b);')
f.write( '}')
f.write( 'return;')
f.write( '}')
# render the deepest call path of the relevant code
# tile
f.write( 'let c = document.getElementById("c-"+('
'a.classList.contains("ctx")'
'? a.dataset.func'
': a.dataset.name));')
f.write( 'if (c) {')
f.write( 'draw_deepest(c);')
f.write( '}')
# focus all tiles beneath this one, bit of a hack to
# avoid another recursive function, yes this includes
# all ctxs if any ctx is in focus
f.write( 'let y = parseInt(a.dataset.y);')
f.write( 'for (b of a.parentElement'
'.querySelectorAll(".frame,.ctx")) {')
f.write( 'if (parseInt(b.dataset.y) >= y) {')
f.write( 'draw_focus(b);')
f.write( '}')
f.write( '}')
# move our tile to the top
f.write( 'draw_focus(a);')
f.write('}')
# state machine for mouseover/clicks
f.write('const modes = [')
if mode_callgraph:
f.write('{name: "callgraph", draw: draw_callgraph},')
if mode_deepest:
f.write('{name: "deepest", draw: draw_deepest },')
if mode_callees:
f.write('{name: "callees", draw: draw_callees },')
if mode_callers:
f.write('{name: "callers", draw: draw_callers },')
f.write('];')
f.write('let state = 0;')
f.write('let hovered = null;')
f.write('let active_code = null;')
f.write('let active_stack = null;')
f.write('let paused = false;')
f.write('function enter_tile(a, event) {')
f.write( 'hovered = a;')
# do nothing if paused
f.write( 'if (paused) {')
f.write( 'return;')
f.write( '}')
# code tile or stack tile?
f.write( 'if (a.classList.contains("code")) {')
f.write( 'if (!active_code && !active_stack) {')
# reset
f.write( 'undraw();')
f.write( 'draw_unfocus();')
# draw selected mode
f.write( 'modes[state].draw(a);')
if not no_stack:
# render relevant stack tiles
f.write( 'switch_stack(a.dataset.name);')
f.write( '}')
f.write( '} else if (a.classList.contains("frame") '
'|| a.classList.contains("ctx")) {')
f.write( 'if (!active_stack) {')
# reset
f.write( 'undraw();')
f.write( 'draw_unfocus();')
if not no_stack:
# draw stack mode
f.write( 'draw_stack(a);')
f.write( '}')
f.write( '}')
f.write('}')
f.write('function leave_tile(a, event) {')
f.write( 'hovered = null;')
# do nothing if paused
f.write( 'if (paused) {')
f.write( 'return;')
f.write( '}')
# do nothing if ctrl is held
f.write( 'if (!active_stack) {')
# reset
f.write( 'undraw();')
f.write( 'if (!active_code) {')
if not no_stack:
# reset to deepest stack
f.write( 'switch_stack("%s");' % deepest['name'])
f.write( '} else {')
# reset to active code
f.write( 'draw_unfocus();')
f.write( 'modes[state].draw(active_code);')
if not no_stack:
f.write( 'draw_stack();')
f.write( '}')
f.write( '}')
f.write('}')
# update the mode string
f.write('function draw_mode() {')
f.write( 'let mode = document.getElementById("mode");')
f.write( 'if (mode) {')
f.write( 'mode.textContent = "mode: "'
'+ modes[state].name'
'+ ((paused) ? " (paused)"'
': (active_code || active_stack)'
'? " (frozen)"'
': "");')
f.write( '}')
f.write('}')
# redraw things
f.write('function redraw() {')
# reset
f.write( 'undraw();')
# redraw stack if active
f.write( 'if (active_stack) {')
f.write( 'draw_unfocus();')
if not no_stack:
f.write( 'draw_stack(active_stack);')
# redraw code if active
f.write( '} else if (active_code) {')
f.write( 'draw_unfocus();')
f.write( 'modes[state].draw(active_code);')
if not no_stack:
f.write( 'draw_stack();')
# otherwise try to enter hovered tile if there is one
f.write( '} else if (hovered) {')
f.write( 'enter_tile(hovered);')
f.write( '}')
f.write('}')
# clicking the mode element changes the mode
f.write('function click_header(a, event) {')
# do nothing if paused
f.write( 'if (paused) {')
f.write( 'return;')
f.write( '}')
# update state
f.write( 'state = (state + 1) % modes.length;')
# update the mode string
f.write( 'draw_mode();')
# redraw with new mode
f.write( 'redraw();')
f.write('}')
# click handler is kinda complicated, we handle both single
# and double clicks here
f.write('let prev_code = null;')
f.write('let prev_stack = null;')
f.write('function click_tile(a, event) {')
# do nothing if paused
f.write( 'if (paused) {')
f.write( 'return;')
f.write( '}')
# double clicking changes the mode
f.write( 'if (event && event.detail == 2 '
# limit this to double-clicking the active tile
'&& ((!prev_code && !prev_stack)'
'|| (a == prev_code && !prev_stack)'
'|| a == prev_stack)) {')
# undo single-click
f.write( 'active_code = prev_code;')
f.write( 'active_stack = prev_stack;')
# trigger a mode change if double-clicking code,
# note we could also change stack modes here if we
# had more than one
f.write( 'if (a.classList.contains("code")) {')
f.write( 'click_header();')
f.write( '}')
f.write( 'return;')
f.write( '}')
# save state in case we are trying to double click,
# double clicks always send a single click first
f.write( 'prev_code = active_code;')
f.write( 'prev_stack = active_stack;')
# clicking tiles toggles frozen mode
f.write( 'if (a.classList.contains("code")) {')
f.write( 'if (a == active_code && !active_stack) {')
f.write( 'active_code = null;')
f.write( '} else {')
f.write( 'active_code = null;')
f.write( 'active_stack = null;')
f.write( 'enter_tile(a);')
f.write( 'active_code = a;')
f.write( '}')
f.write( '} else if (a.classList.contains("frame")'
'|| a.classList.contains("ctx")) {')
f.write( 'if (a == active_stack) {')
f.write( 'active_stack = null;')
f.write( '} else {')
f.write( 'active_stack = null;')
f.write( 'enter_tile(a);')
f.write( 'active_stack = a;')
f.write( '}')
f.write( '}')
# update mode string
f.write( 'draw_mode();')
f.write('}')
# include some minor keybindings
f.write('function keydown(event) {')
# m => change mode
f.write( 'if (event.key == "m") {')
f.write( 'click_header();')
# escape/e => clear frozen/paused state
f.write( '} else if (event.key == "Escape"'
'|| event.key == "e") {')
# reset frozen state
f.write( 'active_code = null;')
f.write( 'active_stack = null;')
# reset paused state
f.write( 'if (paused) {')
f.write( 'keydown({key: "Pause"});')
f.write( '}')
# redraw things
f.write( 'draw_mode();')
f.write( 'redraw();')
# pause/p => pause all interactivity and allow
# copy-paste
f.write( '} else if (event.key == "Pause"'
'|| event.key == "p") {')
f.write( 'paused = !paused;')
# update mode string
f.write( 'draw_mode();')
f.write( 'if (paused) {')
# enabled copy-pasting when paused
f.write( 'for (let e of document.querySelectorAll('
'"[style*=\\"user-select\\"]")) {')
f.write( 'e.style["user-select"] = "auto";')
f.write( '}')
f.write( 'for (let e of document.querySelectorAll('
'"[cursor]")) {')
f.write( 'e.setAttribute("cursor", "auto");')
f.write( '}')
f.write( '} else {')
# reset copy-pasting
f.write( 'document.getSelection().empty();')
f.write( 'for (let e of document.querySelectorAll('
'"[style*=\\"user-select\\"]")) {')
f.write( 'e.style["user-select"] = "none";')
f.write( '}')
f.write( 'for (let e of document.querySelectorAll('
'"[cursor]")) {')
f.write( 'e.setAttribute("cursor", "pointer");')
f.write( '}')
f.write( '}')
f.write( '}')
f.write('}')
f.write('window.addEventListener("keydown", keydown);')
f.write(']]></script>')
f.write('</svg>')
# 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}))