diff --git a/scripts/codemapd3.py b/scripts/codemapd3.py new file mode 100755 index 00000000..a277ae02 --- /dev/null +++ b/scripts/codemapd3.py @@ -0,0 +1,2929 @@ +#!/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'] + + +def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout + 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' + + +# TODO adopt this default scheme elsewhere? +# 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 + +def collect(csv_paths, defines=[]): + # collect results from CSV files + fields = [] + results = [] + for path in csv_paths: + try: + with openio(path) as f: + reader = csv.DictReader(f, restval='') + fields.extend( + k for k in reader.fieldnames + if k not in fields) + for r in reader: + # filter by matching defines + if not all(k in r and r[k] in vs for k, vs in defines): + continue + + results.append(r) + except FileNotFoundError: + pass + + return fields, results + +def fold(results, by=None, fields=None, defines=[]): + # filter by matching defines + if defines: + results_ = [] + for r in results: + if all(k in r and r[k] in vs for k, vs in defines): + results_.append(r) + results = results_ + + if by: + # find all 'by' values + keys = set() + for r in results: + keys.add(tuple(r.get(k, '') for k in by)) + keys = sorted(keys) + + # collect datasets + datasets = co.OrderedDict() + dataattrs = co.OrderedDict() + for key in (keys if by else [()]): + for field in fields: + # organize by 'by' and field + dataset = [] + dataattr = {} + for r in results: + # filter by 'by' + if by and not all( + k in r and r[k] == v + for k, v in zip(by, key)): + continue + + # find field + if field is not None: + if field not in r: + continue + try: + v = dat(r[field]) + except ValueError: + continue + else: + v = None + + # do _not_ sum v here, it's tempting but risks + # incorrect and misleading results + dataset.append(v) + + # include all fields in dataattrs in case we use + # them for % modifiers + dataattr.update(r) + + # hide 'field' if there is only one field + key_ = key + if len(fields or []) > 1 or not key_: + key_ += (field,) + datasets[key_] = dataset + dataattrs[key_] = dataattr + + return datasets, dataattrs + +# a representation of optionally key-mapped attrs +class Attr: + def __init__(self, attrs, *, + defaults=None): + # include defaults? + if (defaults is not None + and not any( + not isinstance(attr, tuple) + or attr[0] in {None, (), ('*',)} + for attr in (attrs or []))): + attrs = defaults + (attrs or []) + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in (attrs or []): + if not isinstance(attr, tuple): + attr = ((), attr) + elif attr[0] in {None, (), ('*',)}: + attr = ((), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + def __repr__(self): + return 'Attr(%r)' % [ + (','.join(attr[0]), attr[1]) + for attr in self.attrs] + + def __iter__(self): + return it.cycle(self.keyed[()]) + + 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 + else: + 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])] + + return None + + def __contains__(self, key): + return self.__getitem__(key) is not None + + # a key function for sorting by key order + def key(self, key): + # allow key to be a tuple to make sorting dicts easier + if (isinstance(key, tuple) + and len(key) >= 1 + and isinstance(key[0], tuple)): + key = key[0] + + 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] + + return len(self.keyed) + +# TODO adopt this elsewhere (try_dat -> default) +# parse %-escaped strings +def punescape(s, attrs=None): + if attrs is None: + attrs = {} + if isinstance(attrs, dict): + attrs_ = attrs + attrs = lambda k: attrs_[k] + + 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] == '(': + try: + v = attrs(m.group('field')) + except KeyError: + 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, %r, %r, %r, %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 + + # 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 int 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(bdiv(aspect_ratio[0], aspect_ratio[1]), + bdiv(aspect_ratio[1], aspect_ratio[0])) + + 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, + no_javascript=False, + mode_callgraph=False, + mode_deepest=False, + mode_callees=False, + mode_callers=False, + to_scale=None, + aspect_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 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_ = Attr(colors, defaults=COLORS_DARK if dark else COLORS) + + labels_ = Attr(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: + # TODO adopt this rename (json -> is_json) in other scripts + # 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_frames = not any('stack_frame' in r for r in results) + if nil_frames: + no_frames = True + nil_ctx = not any('ctx_size' in r for r in results) + 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 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']) + # TODO, what if not children in ctx? in stack? + 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) + +# import pprint +# pprint.pprint(functions) + +# # 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 +# functions = co.OrderedDict() +# if obj_paths: +# for r in collect_code(obj_paths, **args): +# if r['function'] not in functions: +# functions[r['function']] = {} +# functions[r['function']]['code'] = dat(r['code_size']) +# if ci_paths: +# for r in collect_stack(ci_paths, **args): +# if r['function'] not in functions: +# functions[r['function']] = {} +# functions[r['function']]['frame'] = dat(r['stack_frame']) +# functions[r['function']]['stack'] = dat(r['stack_limit'], mt.inf) +# if 'children' in r: +# functions[r['function']]['children'] = [ +# r_['function'] for r_ in r['children']] +# if obj_paths: +# for r in collect_ctx(obj_paths, **args): +# if r['function'] not in functions: +# functions[r['function']] = {} +# functions[r['function']]['ctx'] = dat(r['ctx_size']) +# if 'children' in r: +# functions[r['function']]['args'] = [ +# (r_['function'], dat(r_['ctx_size'])) +# for r_ in r['children']] + + # 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 +# # cached? +# if not hasattr(limitof, 'cache'): +# limitof.cache = {} +# if k in limitof.cache: +# return limitof.cache[k] + + limit = 0 + for child in f.get('children', []): + if child not in functions: + continue + limit = max(limit, limitof(child, functions[child], seen | {k})) + +# limitof.cache[k] = f['frame'] + limit + 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['attrs'] = {k: v + for f in functions.values() + for k, v in f['attrs'].items()} + +# import pprint +# pprint.pprint(functions) + + # 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()): + s['color'] = punescape(colors_[i, (k,)], s['attrs'] | s) + +# # TODO make this configurable? +# stack_ratio = 1/5 + +# # use colors for top of tree +# for i, t in enumerate(tile.children): +# for t_ in t.tiles(): +# t_.color = colors_[i, t_.key] + +# # build functions +# datasets = co.OrderedDict() +# dataattrs = co.OrderedDict() +# for k, v in functions.items(): +# name = ('_'.join(k.split('_')[:2]), k) +# +# try: +# if 'code' in v: +# datasets[name] = [dat(v['code'])] +# except ValueError: +# pass +# +# # bring over everything else +# if name not in dataattrs: +# dataattrs[name] = {} +# dataattrs[name].update(v | { +# 'subsystem': name[0], +# 'name': name[1], +# 'code': datasets[name][0], +# 'stack': v.get('stack', 0), +# 'ctx': v.get('ctx', 0)}) + +# # break down by namespace +# # TODO namespace depth? +# results_ = [] +# for r in results: +# name = (r['function'] if 'function' in r +# else r['struct'] if 'struct' in r +# else r['name'] if name in r +# else '?') +# results_.append({ +# 'subsystem': '_'.join(name.split('_')[:2]), +# 'name': name, +# 'code': r['code'] if 'code' in r +# else r['size'] if 'size' in r +# else '0', +# 'frame': r['frame'] if 'code' in r +# else '0', +# 'limit': r['stack'] if 'stack' in r +# else r['limit'] if 'limit' in r +# else '0', +# 'ctx': r['ctx'] if 'ctx' in r +# else '0'}) +# results = results_ +# +# by = ['subsystem', 'name'] +# fields = ['code'] +# labels_ = Attr(['%(name)s%ncode %(code)d%nstack %(stack)d%nctx %(ctx)d']) +# +# if not by and not fields: +# print("error: needs --by or --fields to figure out fields", +# file=sys.stderr) +# sys.exit(-1) +# +# # if by not specified, guess it's anything not in fields/labels/defines +# if not by: +# by = [k for k in fields_ +# if k not in (fields or []) +# and k not in (labels or []) +# and not any(k == k_ for k_, _ in defines)] +# +# # if fields not specified, guess it's anything not in by/labels/defines +# if not fields: +# fields = [k for k in fields_ +# if k not in (by or []) +# and k not in (labels or []) +# and not any(k == k_ for k_, _ in defines)] +# +# # then extract the requested dataset +# datasets, dataattrs = fold(results, by, fields, defines) +# +# import pprint +# pprint.pprint(datasets) + + # build code heirarchy + code = Tile.merge( + Tile( (f['subsystem'], f['name']), + # fallback to stack/ctx + f.get('code', 0) if not nil_code + else f.get('frame', 0) if not nil_frames + else f.get('ctx', 0), + attrs=f) + for f in functions.values()) + + # 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) + + # 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) + +# ctxs[k].attrs = {'name': k, 'subsystem': f['subsystem']} +# print(ctxs[k]) + + +# children = [] +# for k, f in functions.items(): +# children.append(Tile( +# k, +# f['code'], +# attrs=f)) +# +## for key, dataset in datasets.items(): +## for i, v in enumerate(dataset): +## children.append(Tile( +## key + ((str(i),) if len(dataset) > 1 else ()), +## v, +## attrs=dataattrs[key])) +# +# tile = Tile.merge(children) +# +# # merge attrs +# for t in tile.tiles(): +# if t.children: +# t.attrs = {k: v +# for t_ in t.leaves() +# for k, v in t_.attrs.items()} +# # also sum fields here in case they're used by % modifiers, +# # note other fields are _not_ summed +# for k in fields: +# t.attrs[k] = sum(t_.value +# for t_ in t.leaves() +# if len(fields) == 1 or t_.key[len(by)] == k) +# +# # assign colors/labels before sorting to keep things reproducible +# +# # use colors for top of tree +# for i, t in enumerate(tile.children): +# for t_ in t.tiles(): +# t_.color = colors_[i, t_.key] +# +# # and labels everywhere +# for i, t in enumerate(tile.tiles()): +# if (i, t.key) in labels_: +# t.label = punescape(labels_[i, t.key], t.attrs) + + # 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 not nil_code + else totals.get('frame', 0) if not nil_frames + else totals.get('ctx', 0)) + if total_value: + # scale width only + if height is not None: + if not no_stack: + width_ = mt.ceil(((total_value * to_scale) / height_) + # add space for stack + / (1 - stack_ratio)) + else: + width_ = mt.ceil((total_value * to_scale) / height_) + # scale height only + elif width is not None: + if not no_stack: + height_ = mt.ceil((total_value * to_scale) + # carve out space for stack + / (width_ * (1 - stack_ratio))) + else: + height_ = mt.ceil((total_value * to_scale) / width_) + # scale based on aspect-ratio + else: + if not no_stack: + width_ = mt.ceil(mt.sqrt(total_value * to_scale) + * (aspect_ratio[0] / aspect_ratio[1]) + # add space for stack + / (1 - stack_ratio)) + height_ = mt.ceil((total_value * to_scale) + # carve out space for stack + / (width_ * (1 - stack_ratio))) + else: + width_ = mt.ceil(mt.sqrt(total_value * to_scale) + * (aspect_ratio[0] / aspect_ratio[1])) + height_ = mt.ceil((total_value * to_scale) / width_) + + # our general purpose partition function + def partition(tile, scheme): + 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 scheme == 'binary': + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + elif (scheme == 'slice' + or (scheme == 'slice_and_dice' and (tile.depth & 1) == 0) + or (scheme == 'dice_and_slice' and (tile.depth & 1) == 1)): + partition_slice(tile.children, tile.value, + x__, y__, width__, height__) + elif (scheme == 'dice' + or (scheme == 'slice_and_dice' and (tile.depth & 1) == 1) + or (scheme == 'dice_and_slice' and (tile.depth & 1) == 0)): + partition_dice(tile.children, tile.value, + x__, y__, width__, height__) + elif scheme == 'squarify': + partition_squarify(tile.children, tile.value, + x__, y__, width__, height__) + elif scheme == 'rectify': + partition_squarify(tile.children, tile.value, + x__, y__, width__, height__, + aspect_ratio=(width_, height_)) + else: + # default to binary partitioning + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + + # recursively partition + for t in tile.children: + partition(t, scheme) + + # 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, 'binary') + # 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 + 2 + ctx.y = y__ + ctx.width = width__ - ctx.x + ctx.height = ctx_split + partition(ctx, 'slice') + # align to pixel boundaries + ctx.align() + + # partition stack + if not no_frames: + stack = stacks[k] + stack.x = code.x + code.width + 2 + stack.y = ctx.y + ctx.height + 2 if ctx_split > 0 else y__ + stack.width = width__ - stack.x + stack.height = height___ - (stack.y - y__) + partition(stack, 'dice') + # 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( +# 'pointer-events="none" ' +# 'onkeydown="keydown(this,event)" ' +# 'onkeyup="keydown(this,event)" ' + 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: callgraph') + f.write('') + f.write('') + f.write('') + +# # put all our tiles in a big group to catch clicks +# f.write('') +# # add an invisible rect to make things more clickable +# f.write('' % dict( +# x=0, +# y=y__, +# width=width_, +# height=height_ - y__)) +# f.write('') + + # create code tiles + for i, t in enumerate(code.leaves()): + # skip anything with zero weight/height after aligning things + if t.width == 0 or t.height == 0: + continue + + label__ = labels_[i, (t.attrs['name'],)] + if label__ is not None: + label__ = punescape(label__, t.attrs['attrs'] | t.attrs) + else: + 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 '') + + 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" ' + # 'cursor="pointer" ' + '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(label__) + f.write('') + f.write('' % dict( + id=i, + color=subsystems[t.attrs['subsystem']]['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(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('') + +# # show the deepest stack/ctx by default +# default = max(functions.values(), +# key=lambda f: f.get('stack', 0) + f.get('ctx', 0)) + + # 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 line between code/stack + f.write('' % dict( + x=code.x + code.width, + y=code.y, + width=2, + 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)) + f.write('') + +# # create a group to catch stack-enter events +# f.write('' % dict( +# func=k)) +# # add an invisible rect to make things more clickable +# f.write('' % dict( +# x=code.x + code.width + 2, +# y=y__, +# width=width_ - (code.x + code.width + 2), +# height=height_ - y__)) +# 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 + +# print(ctx) + label__ = labels_[j, (t.attrs['name'],)] + if label__ is not None: + label__ = punescape(label__, t.attrs['attrs'] | t.attrs) + else: + label__ = '%s\nctx %d' % ( + t.attrs['name'], + t.value) + + 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" ' + # 'cursor="pointer" ' + '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(label__) + f.write('') + f.write('' % dict( + id='%s-%s' % (i, j), + color=subsystems[t.attrs['subsystem']]['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(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 line 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=2)) + 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 + + label__ = labels_[j, (t.attrs['name'],)] + if label__ is not None: + label__ = punescape(label__, t.attrs['attrs'] | t.attrs) + else: + label__ = '%s\nframe %d' % ( + t.attrs['name'], + t.attrs.get('frame', 0)) + + 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" ' +# 'cursor="pointer" ' + '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(label__) + f.write('') + f.write('' % dict( + id='%s-%s' % (i, j), + color=subsystems[t.attrs['subsystem']]['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(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('') + +# f.write('') + +# f.write('') + + if not no_javascript: + # arrowhead for arrows + f.write('') + f.write('') + f.write('') + f.write('') + f.write('') + + # TODO why are we running into NaNs? for v1? + + # 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 __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Render code info as an interactive d3-esque 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( + '-n', '--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(), + # TODO % modifiers in color everywhere? chars? formats? etc? + 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( + '-N', '--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( + '--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( + '-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( + '--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( + '-R', '--aspect-ratio', + type=lambda x: ( + tuple(float(v) for v in x.split(':', 1)) + if ':' in x else (float(x), 1)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + # TODO add tiny to treemap.py / codemap.py? + 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( + '--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})) diff --git a/scripts/treemap.py b/scripts/treemap.py index dc316fad..f718ec75 100755 --- a/scripts/treemap.py +++ b/scripts/treemap.py @@ -1141,22 +1141,24 @@ if __name__ == "__main__": parser.add_argument( '--to-scale', nargs='?', - type=float, + 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( '-R', '--aspect-ratio', - type=lambda x: tuple(float(v) for v in x.split(':', 1)), - default=(1, 1), + type=lambda x: ( + tuple(float(v) for v in x.split(':', 1)) + if ':' in x else (float(x), 1)), help="Aspect ratio to use with --to-scale. Defaults to 1:1.") parser.add_argument( '--title', - help="Add a title.") + help="Add a title. Accepts %% modifiers.") parser.add_argument( '--padding', type=float, - default=0, help="Padding to add to each level of the treemap. Defaults to 0.") parser.add_argument( '-l', '--label', diff --git a/scripts/treemapd3.py b/scripts/treemapd3.py index e8609cd3..1cd7a755 100755 --- a/scripts/treemapd3.py +++ b/scripts/treemapd3.py @@ -577,7 +577,8 @@ def main(csv_paths, output, *, **args): # tiny mode? if tiny: - to_scale = True + if to_scale is None: + to_scale = 1 no_header = True no_label = True @@ -666,7 +667,9 @@ def main(csv_paths, output, *, t.label = punescape(labels_[i, t.key], t.attrs) # scale width/height if requested now that we have our data - if to_scale and (width is None or height is None) and tile.value != 0: + if (to_scale is not None + and (width is None or height is None) + and tile.value != 0): # scale width only if height is not None: width_ = mt.ceil((tile.value * to_scale) / height_) @@ -907,7 +910,7 @@ if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( - description="Render CSV files as a treemap to a d3-esque svg.", + description="Render CSV files as a d3-esque treemap.", allow_abbrev=False) parser.add_argument( 'csv_paths', @@ -1022,14 +1025,17 @@ if __name__ == "__main__": parser.add_argument( '--to-scale', nargs='?', - type=float, + 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( '-R', '--aspect-ratio', - type=lambda x: tuple(float(v) for v in x.split(':', 1)), - default=(1, 1), + type=lambda x: ( + tuple(float(v) for v in x.split(':', 1)) + if ':' in x else (float(x), 1)), help="Aspect ratio to use with --to-scale. Defaults to 1:1.") parser.add_argument( '-t', '--tiny', @@ -1042,11 +1048,10 @@ if __name__ == "__main__": help="Show nested tiles.") parser.add_argument( '--title', - help="Add a title.") + help="Add a title. Accepts %% modifiers.") parser.add_argument( '--padding', type=float, - default=1, help="Padding to add to each level of the treemap. Defaults to 1.") parser.add_argument( '--no-label',