mirror of
https://github.com/littlefs-project/littlefs.git
synced 2025-12-06 15:42:43 +00:00
This should have no noticeable impact on plot.py, but shared classes have proven helpful for maintaining these scripts. Unfortunately, this did require some tweaking of the Canvas class to get things working. Now, instead of storing things in an internal high-resolution grid, the Canvas class only keeps track of the most recent character, with bitmasked ints storing sub-char info. This makes it so sub-char draws overwrite full characters, which is necessary for plot.py's axis/data overlap to work.
1353 lines
43 KiB
Python
Executable File
1353 lines
43 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
|
|
|
|
|
|
# we don't actually need that many chars/colors thanks to the
|
|
# 4-colorability of all 2d maps
|
|
COLORS = ['34', '31', '32', '35', '33', '36']
|
|
|
|
CHARS_DOTS = " .':"
|
|
CHARS_BRAILLE = (
|
|
'⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
|
|
'⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
|
|
'⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
|
|
'⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
|
|
'⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
|
|
'⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
|
|
'⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
|
|
'⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
|
|
|
|
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
|
|
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 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 = list(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)
|
|
|
|
# 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<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] == '(':
|
|
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)
|
|
|
|
# split %-escaped strings into chars
|
|
def psplit(s):
|
|
pattern = re.compile(
|
|
'%[%n]'
|
|
'|' '%x..'
|
|
'|' '%u....'
|
|
'|' '%U........'
|
|
'|' '%\((?P<field>[^)]*)\)'
|
|
'(?P<format>[+\- #0-9\.]*[sdboxXfFeEgG])')
|
|
return [m.group() for m in re.finditer(pattern.pattern + '|.', s)]
|
|
|
|
|
|
# a little ascii renderer
|
|
class Canvas:
|
|
def __init__(self, width, height, *,
|
|
color=False,
|
|
dots=False,
|
|
braille=False):
|
|
# scale if we're printing with dots or braille
|
|
if braille:
|
|
xscale, yscale = 2, 4
|
|
elif dots:
|
|
xscale, yscale = 1, 2
|
|
else:
|
|
xscale, yscale = 1, 1
|
|
|
|
self.width_ = width
|
|
self.height_ = height
|
|
self.width = xscale*width
|
|
self.height = yscale*height
|
|
self.xscale = xscale
|
|
self.yscale = yscale
|
|
self.color_ = color
|
|
self.dots = dots
|
|
self.braille = braille
|
|
|
|
# create initial canvas
|
|
self.chars = [0] * (width*height)
|
|
self.colors = [''] * (width*height)
|
|
|
|
def char(self, x, y, char=None):
|
|
# ignore out of bounds
|
|
if x < 0 or y < 0 or x >= self.width or y >= self.height:
|
|
return False
|
|
|
|
x_ = x // self.xscale
|
|
y_ = y // self.yscale
|
|
if char is not None:
|
|
c = self.chars[x_ + y_*self.width_]
|
|
# mask in sub-char pixel?
|
|
if isinstance(char, bool):
|
|
if not isinstance(c, int):
|
|
c = 0
|
|
self.chars[x_ + y_*self.width_] = (c
|
|
| (1
|
|
<< ((y%self.yscale)*self.xscale
|
|
+ (self.xscale-1)-(x%self.xscale))))
|
|
else:
|
|
self.chars[x_ + y_*self.width_] = char
|
|
else:
|
|
c = self.chars[x_ + y_*self.width_]
|
|
if isinstance(c, int):
|
|
return ((c
|
|
>> ((y%self.yscale)*self.xscale
|
|
+ (self.xscale-1)-(x%self.xscale)))
|
|
& 1) == 1
|
|
else:
|
|
return c
|
|
|
|
def color(self, x, y, color=None):
|
|
# ignore out of bounds
|
|
if x < 0 or y < 0 or x >= self.width or y >= self.height:
|
|
return ''
|
|
|
|
x_ = x // self.xscale
|
|
y_ = y // self.yscale
|
|
if color is not None:
|
|
self.colors[x_ + y_*self.width_] = color
|
|
else:
|
|
return self.colors[x_ + y_*self.width_]
|
|
|
|
def __getitem__(self, xy):
|
|
x, y = xy
|
|
return self.char(x, y)
|
|
|
|
def __setitem__(self, xy, char):
|
|
x, y = xy
|
|
self.char(x, y, char)
|
|
|
|
def point(self, x, y, *,
|
|
char=True,
|
|
color=''):
|
|
self.char(x, y, char)
|
|
self.color(x, y, color)
|
|
|
|
def line(self, x1, y1, x2, y2, *,
|
|
char=True,
|
|
color=''):
|
|
# incremental error line algorithm
|
|
ex = abs(x2 - x1)
|
|
ey = -abs(y2 - y1)
|
|
dx = +1 if x1 < x2 else -1
|
|
dy = +1 if y1 < y2 else -1
|
|
e = ex + ey
|
|
|
|
while True:
|
|
self.point(x1, y1, char=char, color=color)
|
|
e2 = 2*e
|
|
|
|
if x1 == x2 and y1 == y2:
|
|
break
|
|
|
|
if e2 > ey:
|
|
e += ey
|
|
x1 += dx
|
|
|
|
if x1 == x2 and y1 == y2:
|
|
break
|
|
|
|
if e2 < ex:
|
|
e += ex
|
|
y1 += dy
|
|
|
|
self.point(x2, y2, char=char, color=color)
|
|
|
|
def rect(self, x, y, w, h, *,
|
|
char=True,
|
|
color=''):
|
|
for j in range(h):
|
|
for i in range(w):
|
|
self.point(x+i, y+j, char=char, color=color)
|
|
|
|
def label(self, x, y, label, width=None, height=None, *,
|
|
color=''):
|
|
x_ = x
|
|
y_ = y
|
|
for char in label:
|
|
if char == '\n':
|
|
x_ = x
|
|
y_ -= self.yscale
|
|
else:
|
|
if ((width is None or x_ < x+width)
|
|
and (height is None or y_ > y-height)):
|
|
self.point(x_, y_, char=char, color=color)
|
|
x_ += self.xscale
|
|
|
|
def draw(self, row):
|
|
y_ = self.height_-1 - row
|
|
row_ = []
|
|
for x_ in range(self.width_):
|
|
# char?
|
|
c = self.chars[x_ + y_*self.width_]
|
|
if isinstance(c, int):
|
|
if self.braille:
|
|
assert c < 256
|
|
c = CHARS_BRAILLE[c]
|
|
elif self.dots:
|
|
assert c < 4
|
|
c = CHARS_DOTS[c]
|
|
else:
|
|
assert c < 2
|
|
c = '.' if c else ' '
|
|
|
|
# color?
|
|
if self.color_:
|
|
color = self.colors[x_ + y_*self.width_]
|
|
if color:
|
|
c = '\x1b[%sm%s\x1b[m' % (color, c)
|
|
|
|
row_.append(c)
|
|
|
|
return ''.join(row_)
|
|
|
|
|
|
# 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(aspect_ratio[0] / aspect_ratio[1],
|
|
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, *,
|
|
namespace_depth=2,
|
|
labels=[],
|
|
chars=[],
|
|
colors=[],
|
|
color=False,
|
|
dots=False,
|
|
braille=False,
|
|
width=None,
|
|
height=None,
|
|
no_header=False,
|
|
to_scale=None,
|
|
aspect_ratio=(1,1),
|
|
tiny=False,
|
|
title=None,
|
|
padding=0,
|
|
label=False,
|
|
no_label=False,
|
|
**args):
|
|
# figure out what color should be
|
|
if color == 'auto':
|
|
color = sys.stdout.isatty()
|
|
elif color == 'always':
|
|
color = True
|
|
else:
|
|
color = False
|
|
|
|
# tiny mode?
|
|
if tiny:
|
|
if to_scale is None:
|
|
to_scale = 1
|
|
no_header = True
|
|
|
|
# what chars/colors/labels to use?
|
|
chars_ = []
|
|
for char in chars:
|
|
if isinstance(char, tuple):
|
|
chars_.extend((char[0], c) for c in psplit(char[1]))
|
|
else:
|
|
chars_.extend(psplit(char))
|
|
chars_ = Attr(chars_)
|
|
|
|
colors_ = Attr(colors, defaults=COLORS)
|
|
|
|
labels_ = Attr(labels)
|
|
|
|
# figure out width/height
|
|
if width is None:
|
|
width_ = min(80, shutil.get_terminal_size((80, 5))[0])
|
|
elif width:
|
|
width_ = width
|
|
else:
|
|
width_ = shutil.get_terminal_size((80, 5))[0]
|
|
|
|
if height is None:
|
|
height_ = 2 if not no_header else 1
|
|
elif height:
|
|
height_ = height
|
|
else:
|
|
height_ = shutil.get_terminal_size((80, 5))[1] - 1
|
|
|
|
# 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_frames = not any('stack_frame' in r for r in results)
|
|
nil_ctx = not any('ctx_size' in r for r in results)
|
|
|
|
# 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'])
|
|
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['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()):
|
|
s['color'] = punescape(colors_[i, (k,)], s['attrs'] | s)
|
|
|
|
|
|
# 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())
|
|
|
|
# assign colors/chars/labels to code tiles
|
|
for i, t in enumerate(code.leaves()):
|
|
t.color = subsystems[t.attrs['subsystem']]['color']
|
|
if (i, (t.attrs['name'],)) in chars_:
|
|
t.char = punescape(
|
|
chars_[i, (t.attrs['name'],)],
|
|
t.attrs['attrs'] | t.attrs)[0] # limit to 1 char
|
|
elif len(t.attrs['subsystem']) < len(t.attrs['name']):
|
|
t.char = (t.attrs['name'][len(t.attrs['subsystem']):].lstrip('_')
|
|
or '')[0]
|
|
else:
|
|
t.char = (t.attrs['subsystem'].rstrip('_').rsplit('_', 1)[-1]
|
|
or '')[0]
|
|
if (i, (t.attrs['name'],)) in labels_:
|
|
t.label = punescape(
|
|
labels_[i, (t.attrs['name'],)],
|
|
t.attrs['attrs'] | t.attrs)
|
|
else:
|
|
t.label = t.attrs['name']
|
|
|
|
# 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 if needed
|
|
if braille:
|
|
xscale, yscale = 2, 4
|
|
elif dots:
|
|
xscale, yscale = 1, 2
|
|
else:
|
|
xscale, yscale = 1, 1
|
|
|
|
# scale width only
|
|
if height is not None:
|
|
width_ = mt.ceil(
|
|
((total_value * to_scale) / (height_*yscale))
|
|
/ xscale)
|
|
# scale height only
|
|
elif width is not None:
|
|
height_ = mt.ceil(
|
|
((total_value * to_scale) / (width_*xscale))
|
|
/ yscale)
|
|
# scale based on aspect-ratio
|
|
else:
|
|
width_ = mt.ceil(
|
|
(mt.sqrt(total_value * to_scale)
|
|
* (aspect_ratio[0] / aspect_ratio[1]))
|
|
/ xscale)
|
|
height_ = mt.ceil(
|
|
((total_value * to_scale) / (width_*xscale))
|
|
/ yscale)
|
|
|
|
# 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'], 1)
|
|
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 a canvas
|
|
canvas = Canvas(
|
|
width_,
|
|
height_ - (1 if not no_header else 0),
|
|
color=color,
|
|
dots=dots,
|
|
braille=braille)
|
|
|
|
# sort and partition code
|
|
code.sort()
|
|
code.x = 0
|
|
code.y = 0
|
|
code.width = canvas.width
|
|
code.height = canvas.height
|
|
partition(code, **args)
|
|
# align to pixel boundaries
|
|
code.align()
|
|
|
|
|
|
# render to canvas
|
|
labels__ = []
|
|
for t in code.leaves():
|
|
x__ = t.x
|
|
y__ = t.y
|
|
width__ = t.width
|
|
height__ = t.height
|
|
# skip anything with zero weight/height after aligning things
|
|
if width__ == 0 or height__ == 0:
|
|
continue
|
|
|
|
# flip y
|
|
y__ = canvas.height - (y__+height__)
|
|
|
|
canvas.rect(x__, y__, width__, height__,
|
|
# default to first letter of the last part of the key
|
|
char=(True if braille or dots
|
|
else t.char if getattr(t, 'char', None)
|
|
else t.key[len(by)-1][0] if t.key and t.key[len(by)-1]
|
|
else chars_[0]),
|
|
color=t.color if t.color is not None else colors_[0])
|
|
|
|
if label or (labels and not no_label):
|
|
if t.label is not None:
|
|
label__ = t.label
|
|
else:
|
|
label__ = ','.join(t.key)
|
|
|
|
# render these later so they get priority
|
|
labels__.append((x__, y__+height__-1, label__,
|
|
width__, height__))
|
|
|
|
for label__ in labels__:
|
|
canvas.label(*label__)
|
|
|
|
# print some summary info
|
|
if not no_header:
|
|
if title:
|
|
print(punescape(title, totals['attrs'] | totals))
|
|
else:
|
|
print('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)))
|
|
|
|
# draw canvas
|
|
for row in range(canvas.height//canvas.yscale):
|
|
line = canvas.draw(row)
|
|
print(line)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
import sys
|
|
parser = argparse.ArgumentParser(
|
|
description="Render code info as a 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(
|
|
'-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(
|
|
'-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(
|
|
'-.', '--add-char', '--chars',
|
|
dest='chars',
|
|
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 characters 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(
|
|
'--color',
|
|
choices=['never', 'always', 'auto'],
|
|
default='auto',
|
|
help="When to use terminal colors. Defaults to 'auto'.")
|
|
parser.add_argument(
|
|
'-:', '--dots',
|
|
action='store_true',
|
|
help="Use 1x2 ascii dot characters.")
|
|
parser.add_argument(
|
|
'-⣿', '--braille',
|
|
action='store_true',
|
|
help="Use 2x4 unicode braille characters. Note that braille "
|
|
"characters sometimes suffer from inconsistent widths.")
|
|
parser.add_argument(
|
|
'-W', '--width',
|
|
nargs='?',
|
|
type=lambda x: int(x, 0),
|
|
const=0,
|
|
help="Width in columns. 0 uses the terminal width. Defaults to "
|
|
"min(terminal, 80).")
|
|
parser.add_argument(
|
|
'-H', '--height',
|
|
nargs='?',
|
|
type=lambda x: int(x, 0),
|
|
const=0,
|
|
help="Height in rows. 0 uses the terminal height. Defaults to 1.")
|
|
parser.add_argument(
|
|
'-N', '--no-header',
|
|
action='store_true',
|
|
help="Don't show the header.")
|
|
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 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(
|
|
'-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.")
|
|
parser.add_argument(
|
|
'-t', '--tiny',
|
|
action='store_true',
|
|
help="Tiny mode, alias for --to-scale=1 and --no-header.")
|
|
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 0.")
|
|
parser.add_argument(
|
|
'-l', '--label',
|
|
action='store_true',
|
|
help="Render labels.")
|
|
parser.add_argument(
|
|
'--no-label',
|
|
action='store_true',
|
|
help="Don't render any labels.")
|
|
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}))
|