tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

iongraph (20028B)


      1 #!/usr/bin/env python3
      2 # vim: set ts=4 sw=4 tw=99 et:
      3 
      4 # iongraph -- Translate IonMonkey JSON to GraphViz.
      5 # Originally by Sean Stangl. See LICENSE.
      6 
      7 import argparse
      8 import html
      9 import json
     10 import glob
     11 import os
     12 import re
     13 import shutil
     14 import subprocess
     15 import sys
     16 import tempfile
     17 import urllib.request
     18 
     19 def quote(s):
     20     return '"%s"' % str(s)
     21 
     22 # Simple classes for the used subset of GraphViz' Dot format.
     23 # There are more complicated constructors out there, but they all
     24 # pull in annoying dependencies (and are annoying dependencies themselves).
     25 class GraphWidget:
     26     def __init__(self):
     27         self.name = ''
     28         self.props = {}
     29 
     30     def addprops(self, propdict):
     31         for p in propdict:
     32             self.props[p] = propdict[p]
     33 
     34 
     35 class Node(GraphWidget):
     36     def __init__(self, name):
     37         GraphWidget.__init__(self)
     38         self.name = str(name)
     39 
     40 class Edge(GraphWidget):
     41     def __init__(self, nfrom, nto):
     42         GraphWidget.__init__(self)
     43         self.nfrom = str(nfrom)
     44         self.nto = str(nto)
     45 
     46 class Graph(GraphWidget):
     47     def __init__(self, name, func, type):
     48         GraphWidget.__init__(self)
     49         self.name = name
     50         self.func = func
     51         self.type = str(type)
     52         self.props = {}
     53         self.nodes = []
     54         self.edges = []
     55 
     56     def addnode(self, n):
     57         self.nodes.append(n)
     58 
     59     def addedge(self, e):
     60         self.edges.append(e)
     61 
     62     def writeprops(self, f, o):
     63         if len(o.props) == 0:
     64             return
     65 
     66         print('[', end=' ', file=f)
     67         for p in o.props:
     68             print(str(p) + '=' + str(o.props[p]), end=' ', file=f)
     69         print(']', end=' ', file=f)
     70 
     71     def write(self, f):
     72         print(self.type, '{', file=f)
     73 
     74         # Use the pass name as the graph title (at the top).
     75         legend = '<font color="blue">movable</font>, <u>guard</u>, <font color="red">in worklist</font>, <font color="gray50">recovered on bailout</font>'
     76         print('labelloc = t;', file=f)
     77         print('labelfontsize = 30;', file=f)
     78         print('label = <<b>%s - %s</b><br/>%s<br/>&nbsp;>;' % (self.func['name'], self.name, legend), file=f)
     79 
     80         # Output graph properties.
     81         for p in self.props:
     82             print('  ' + str(p) + '=' + str(self.props[p]), file=f)
     83         print('', file=f)
     84 
     85         # Output node list.
     86         for n in self.nodes:
     87             print('  ' + n.name, end=' ', file=f)
     88             self.writeprops(f, n)
     89             print(';', file=f)
     90         print('', file=f)
     91         
     92         # Output edge list.
     93         for e in self.edges:
     94             print('  ' + e.nfrom, '->', e.nto, end=' ', file=f)
     95             self.writeprops(f, e)
     96             print(';', file=f)
     97 
     98         print('}', file=f)
     99 
    100 
    101 # block obj -> node string with quotations
    102 def getBlockNodeName(b):
    103     return blockNumToNodeName(b['id'])
    104 
    105 # int -> node string with quotations
    106 def blockNumToNodeName(i):
    107     return quote('Block' + str(i))
    108 
    109 # resumePoint obj -> HTML-formatted string
    110 def getResumePointRow(rp, mode):
    111     if mode != None and mode != rp['mode']:
    112         return ''
    113 
    114     # Left column: caller.
    115     rpCaller = '<td align="left"></td>'
    116     if 'caller' in rp:
    117         rpCaller = '<td align="left">&#40;&#40;%s&#41;&#41;</td>' % str(rp['caller'])
    118 
    119     # Middle column: ordered contents of the MResumePoint.
    120     insts = ''.join('%s ' % t for t in rp['operands'])
    121     rpContents = '<td align="left"><font color="grey50">resumepoint %s</font></td>' % insts
    122 
    123     # Right column: unused.
    124     rpRight = '<td></td>'
    125 
    126     return '<tr>%s%s%s</tr>' % (rpCaller, rpContents, rpRight)
    127 
    128 # memInputs obj -> HTML-formatted string
    129 def getMemInputsRow(list):
    130     if len(list) == 0:
    131         return ''
    132 
    133     # Left column: caller.
    134     memLeft = '<td align="left"></td>'
    135 
    136     # Middle column: ordered contents of the MResumePoint.
    137     insts = ''.join('%s ' % str(t) for t in list)
    138     memContents = '<td align="left"><font color="grey50">memory %s</font></td>' % insts
    139 
    140     # Right column: unused.
    141     memRight = '<td></td>'
    142 
    143     return '<tr>%s%s%s</tr>' % (memLeft, memContents, memRight)
    144 
    145 # Outputs a single row for an instruction, excluding MResumePoints.
    146 # instruction -> HTML-formatted string
    147 def getInstructionRow(inst):
    148     # Left column: instruction ID.
    149     instId = str(inst['id'])
    150     instLabel = '<td align="right" port="i%s">%s</td>' % (instId, instId)
    151 
    152     # Middle column: instruction name.
    153     instName = inst['opcode'].replace('->', '→').replace('<-', '←')
    154     instName = html.escape(instName)
    155     if 'attributes' in inst:
    156         if 'RecoveredOnBailout' in inst['attributes']:
    157             instName = '<font color="gray50">%s</font>' % instName
    158         elif 'Movable' in inst['attributes']:
    159             instName = '<font color="blue">%s</font>' % instName
    160         if 'Guard' in inst['attributes']:
    161             instName = '<u>%s</u>' % instName
    162         if 'InWorklist' in inst['attributes']:
    163             instName = '<font color="red">%s</font>' % instName
    164     instName = '<td align="left">%s</td>' % instName
    165 
    166     # Right column: instruction MIRType.
    167     instType = ''
    168     if 'type' in inst and inst['type'] != "None":
    169         instType = '<td align="right">%s</td>' % html.escape(inst['type'])
    170 
    171     return '<tr>%s%s%s</tr>' % (instLabel, instName, instType)
    172 
    173 # block obj -> HTML-formatted string
    174 def getBlockLabel(b):
    175     s = '<<table border="0" cellborder="0" cellpadding="1">'
    176 
    177     if 'blockUseCount' in b:
    178         blockUseCount = " (Count: %s)" % str(b['blockUseCount'])
    179     else:
    180         blockUseCount = ""
    181 
    182     blockAttr = ""
    183     if 'attributes' in b:
    184         if 'backedge' in b['attributes']:
    185             blockAttr = ' <font color="lightpink">(backedge)</font>'
    186         if 'loopheader' in b['attributes']:
    187             blockAttr = ' <font color="lightgreen">(loop header)</font>'
    188         if 'splitedge' in b['attributes']:
    189             blockAttr = " (split edge)"
    190 
    191     blockTitle = '<font color="white">Block %s%s%s</font>' % (str(b['id']), blockUseCount, blockAttr)
    192     blockTitle = '<td align="center" bgcolor="black" colspan="3">%s</td>' % blockTitle
    193     s += '<tr>%s</tr>' % blockTitle
    194     
    195     if 'resumePoint' in b:
    196         s += getResumePointRow(b['resumePoint'], None)
    197 
    198     for inst in b['instructions']:
    199         if 'resumePoint' in inst:
    200             s += getResumePointRow(inst['resumePoint'], 'At')
    201 
    202         s += getInstructionRow(inst)
    203 
    204         if 'memInputs' in inst:
    205             s += getMemInputsRow(inst['memInputs'])
    206 
    207         if 'resumePoint' in inst:
    208             s += getResumePointRow(inst['resumePoint'], 'After')
    209 
    210     s += '</table>>'
    211     return s
    212 
    213 # str -> ir obj -> ir obj -> Graph
    214 # 'ir' is the IR to be used.
    215 # 'mir' is always the MIR.
    216 #  This is because the LIR graph does not contain successor information.
    217 def buildGraphForIR(name, func, ir, mir):
    218     if len(ir['blocks']) == 0:
    219         return None
    220 
    221     g = Graph(name, func, 'digraph')
    222     g.addprops({'rankdir':'TB', 'splines':'true'})
    223 
    224     for i in range(0, len(ir['blocks'])):
    225         bactive = ir['blocks'][i] # Used for block contents.
    226         b = mir['blocks'][i] # Used for drawing blocks and edges.
    227 
    228         node = Node(getBlockNodeName(bactive))
    229         node.addprops({'shape':'box', 'label':getBlockLabel(bactive)})
    230         
    231         if 'backedge' in b['attributes']:
    232             node.addprops({'color':'crimson'})
    233         if 'loopheader' in b['attributes']:
    234             node.addprops({'color':'limegreen'})
    235         if 'splitedge' in b['attributes']:
    236             node.addprops({'style':'dashed'})
    237 
    238         g.addnode(node)
    239 
    240         for succ in b['successors']: # which are integers
    241             edge = Edge(getBlockNodeName(bactive), blockNumToNodeName(succ))
    242                 
    243             if len(b['successors']) == 2:
    244                 if succ == b['successors'][0]:
    245                     edge.addprops({'label':'1'})
    246                 else:
    247                     edge.addprops({'label':'0'})
    248 
    249             g.addedge(edge)
    250 
    251     return g
    252 
    253 # pass obj -> output file -> (Graph OR None, Graph OR None)
    254 # The return value is (MIR, LIR); either one may be absent.
    255 def buildGraphsForPass(p, func):
    256     name = p['name']
    257     mir = p['mir']
    258     lir = p['lir']
    259     return (buildGraphForIR(name, func, mir, mir), buildGraphForIR(name, func, lir, mir))
    260 
    261 # function obj -> (Graph OR None, Graph OR None) list
    262 # First entry in each tuple corresponds to MIR; second, to LIR.
    263 def buildGraphs(func):
    264     graphstup = []
    265     for p in func['passes']:
    266         gtup = buildGraphsForPass(p, func)
    267         graphstup.append(gtup)
    268     return graphstup
    269 
    270 # function obj -> (Graph OR None, Graph OR None) list
    271 # Only builds the final pass.
    272 def buildOnlyFinalPass(func):
    273     if len(func['passes']) == 0:
    274         return [None, None]
    275     p = func['passes'][-1]
    276     return [buildGraphsForPass(p, func)]
    277 
    278 # Write out a graph, constructing a nice filename.
    279 # function id -> pass id -> IR string -> Graph -> void
    280 def outputPass(dir, fnum, pnum, irname, g):
    281     funcid = str(fnum).zfill(2)
    282     passid = str(pnum).zfill(2)
    283 
    284     filename = os.path.join(dir, 'func%s-pass%s-%s-%s.gv' % (funcid, passid, g.name, str(irname)))
    285     with open(filename, 'w') as fd:
    286         g.write(fd)
    287 
    288 # Add in closing } and ] braces to close a JSON file in case of error.
    289 def parenthesize(s):
    290     stack = []
    291     inString = False
    292 
    293     for c in s:
    294         if c == '"': # Doesn't handle escaped strings.
    295             inString = not inString
    296 
    297         if not inString:
    298             if   c == '{' or c == '[':
    299                 stack.append(c)
    300             elif c == '}' or c == ']':
    301                 stack.pop()
    302     
    303     while stack:
    304         c = stack.pop()
    305         if   c == '{': s += '}'
    306         elif c == '[': s += ']'
    307 
    308     return s
    309 
    310 
    311 def genfiles(format, indir, outdir):
    312     gvs = glob.glob(os.path.join(indir, '*.gv'))
    313     gvs.sort()
    314     for gv in gvs:
    315         with open(os.path.join(outdir, '%s.%s' % (os.path.basename(gv), format)), 'w') as outfile:
    316             sys.stderr.write(' writing %s\n' % (outfile.name))
    317             subprocess.run(['dot', gv, '-T%s' % (format)], stdout=outfile, check=True)
    318 
    319 
    320 def gengvs(indir, outdir):
    321     gvs = glob.glob(os.path.join(indir, '*.gv'))
    322     gvs.sort()
    323     for gv in gvs:
    324         sys.stderr.write(' writing %s\n' % (os.path.basename(gv)))
    325         shutil.copy(gv, outdir)
    326 
    327 
    328 def genmergedpdfs(indir, outdir):
    329     gvs = glob.glob(os.path.join(indir, '*.gv'))
    330     gvs.sort()
    331     for gv in gvs:
    332         with open(os.path.join(indir, '%s.pdf' % (os.path.basename(gv))), 'w') as outfile:
    333             sys.stderr.write(' writing pdf %s\n' % (outfile.name))
    334             subprocess.run(['dot', gv, '-Tpdf'], stdout=outfile, check=True)
    335 
    336     sys.stderr.write('combining pdfs...\n')
    337     which = (shutil.which('pdftk') and 'pdftk') or (shutil.which('qpdf') and 'qpdf')
    338     prefixes = [os.path.basename(x) for x in glob.glob(os.path.join(indir, 'func*.pdf'))]
    339     prefixes = [re.match(r'func[^-]*', x).group(0) for x in prefixes]
    340     prefixes = list(set(prefixes))
    341     prefixes.sort()
    342     for prefix in prefixes:
    343         pages = glob.glob(os.path.join(indir, '%s-*.pdf' % (prefix)))
    344         pages.sort()
    345         outfile = os.path.join(outdir, '%s.pdf' % (prefix))
    346         sys.stderr.write(' writing pdf %s\n' % (outfile))
    347         if which == 'pdftk':
    348             subprocess.run(['pdftk', *pages, 'cat', 'output', outfile], check=True)
    349         elif which == 'qpdf':
    350             subprocess.run(['qpdf', '--empty', '--pages', *pages, '--', outfile], check=True)
    351         else:
    352             raise Exception("unknown pdf program")
    353 
    354 
    355 def parsenums(numstr):
    356     if numstr is None:
    357         return None
    358     return [int(x) for x in numstr.split(',')]
    359 
    360 
    361 def parsenames(namestr):
    362     return namestr and namestr.split(',')
    363 
    364 
    365 def validate(args):
    366     if not shutil.which('dot'):
    367         sys.stderr.write("ERROR: graphviz (dot) is not installed\n")
    368         exit(1)
    369     if args.format == 'pdf':
    370         if not (shutil.which('pdftk') or shutil.which('qpdf')):
    371             sys.stderr.write("ERROR: either pdftk or qpdf must be installed in order to combine the generated PDFs.\n")
    372             sys.stderr.write("If you don't care and just want individual PDFs, use `--format pdfs`.\n")
    373             exit(1)
    374     if not os.path.isdir(args.outdir):
    375         sys.stderr.write("ERROR: could not find a directory at %s\n" % (args.outdir))
    376         exit(1)
    377 
    378 
    379 def gen(args):
    380     validate(args)
    381 
    382     with open(args.input, 'r') as fd:
    383         s = fd.read()
    384 
    385     sys.stderr.write("loading %s...\n" % (args.input))
    386     fixed = parenthesize(s)
    387 
    388     # Emit HTML early instead of generating graphviz :)
    389     if args.format == 'html':
    390         with urllib.request.urlopen("https://mozilla-spidermonkey.github.io/iongraph/standalone.html") as response:
    391             html = response.read().decode("utf-8")
    392         formatted = re.sub(r'\{\{\s*IONJSON\s*\}\}', fixed, html)
    393         outname = "iongraph.html"
    394         with open(os.path.join(args.outdir, outname), "w") as outfile:
    395             outfile.write(formatted)
    396         sys.stderr.write("output written to %s\n" % (outname))
    397         return
    398 
    399     ion = json.loads(fixed)
    400     sys.stderr.write("generating graphviz...\n")
    401 
    402     funcnums = parsenums(args.funcnum)
    403     funcnames = parsenames(args.funcname)
    404     passnums = parsenums(args.passnum)
    405     with tempfile.TemporaryDirectory() as tmpdir:
    406         for i in range(0, len(ion['functions'])):
    407             func = ion['functions'][i]
    408 
    409             if funcnums and i not in funcnums:
    410                 continue
    411             if funcnames and func['name'] not in funcnames:
    412                 continue
    413 
    414             gtl = buildOnlyFinalPass(func) if args.final else buildGraphs(func)
    415 
    416             if len(gtl) == 0:
    417                 sys.stderr.write(" function %d (%s): abort during SSA construction.\n" % (i, func['name']))
    418             else:
    419                 sys.stderr.write(" function %d (%s): success; %d passes.\n" % (i, func['name'], len(gtl)))
    420 
    421             for j in range(0, len(gtl)):
    422                 gt = gtl[j]
    423                 if gt == None:
    424                     continue
    425 
    426                 mir = gt[0]
    427                 lir = gt[1]
    428 
    429                 if passnums and j in passnums:
    430                     if lir != None and args.out_lir:
    431                         lir.write(args.out_lir)
    432                     if mir != None and args.out_mir:
    433                         mir.write(args.out_mir)
    434                     if args.out_lir and args.out_mir:
    435                         break
    436                 elif passnums:
    437                     continue
    438 
    439                 # If only the final pass is requested, output both MIR and LIR.
    440                 if args.final:
    441                     if lir != None:
    442                         outputPass(tmpdir, i, j, 'lir', lir)
    443                     if mir != None:
    444                         outputPass(tmpdir, i, j, 'mir', mir)
    445                     continue
    446 
    447                 # Normally, only output one of (MIR, LIR), preferring LIR.
    448                 if lir != None:
    449                     outputPass(tmpdir, i, j, 'lir', lir)
    450                 elif mir != None:
    451                     outputPass(tmpdir, i, j, 'mir', mir)
    452 
    453         if args.format == 'pdf':
    454             sys.stderr.write("generating pdfs...\n")
    455             genmergedpdfs(tmpdir, args.outdir)
    456         elif args.format == 'gv':
    457             sys.stderr.write("copying gvs...\n")
    458             gengvs(tmpdir, args.outdir)
    459         else:
    460             format = 'pdf' if args.format == 'pdfs' else args.format
    461             sys.stderr.write("generating %ss...\n" % (format))
    462             genfiles(format, tmpdir, args.outdir)
    463 
    464 
    465 def js_and_gen(args):
    466     validate(args)
    467 
    468     jsargs = args.remaining[1:]
    469     jsenv = os.environ.copy()
    470     flags = (jsenv['IONFLAGS'] if 'IONFLAGS' in jsenv else 'logs').split(',')
    471     if 'logs' not in flags:
    472         flags.append('logs')
    473     jsenv['IONFLAGS'] = ','.join(flags)
    474     subprocess.run([args.jspath or 'js', *jsargs], env=jsenv, check=True)
    475 
    476     args.input = '/tmp/ion.json'
    477     gen(args)
    478 
    479 
    480 def jittest_and_gen(args):
    481     validate(args)
    482 
    483     testargs = args.remaining[1:]
    484     testenv = os.environ.copy()
    485     flags = (testenv['IONFLAGS'] if 'IONFLAGS' in testenv else 'logs').split(',')
    486     if 'logs' not in flags:
    487         flags.append('logs')
    488     testenv['IONFLAGS'] = ','.join(flags)
    489     jittest_path = os.path.join("js", "src", "jit-test", "jit_test.py")
    490     cmd = " ".join([sys.executable, jittest_path, shutil.which("js"), "--one", *testargs])
    491     print("Command:", cmd)
    492     subprocess.run(cmd, env=testenv, shell=True, check=True)
    493 
    494     args.input = '/tmp/ion.json'
    495     gen(args)
    496 
    497 
    498 def add_main_arguments(parser):
    499     parser.add_argument('-o', '--outdir', help='The directory in which to store the output file(s).',
    500                                           default='.')
    501     parser.add_argument('-f', '--funcnum', help='Only operate on the specified function(s), by index. Multiple functions can be separated by commas, e.g. `1,5,234`.')
    502     parser.add_argument('-n', '--funcname', help='Only operate on the specified function(s), by name. Multiple functions can be separated by commas, e.g. `foo,bar,baz`.')
    503     parser.add_argument('-p', '--passnum', help='Only operate on the specified pass(es), by index. Multiple passes can be separated by commas, e.g. `1,5,234`.')
    504     parser.add_argument('--format', help='The output file format (html by default). `html` will produce a standalone HTML file with the iongraph web viewer. All other options will use graphviz. `pdf` will merge all the graphs for each function into a single PDF; all other formats will produce a single file per graph.',
    505                                     choices=['html', 'gv', 'pdf', 'pdfs', 'png', 'svg'], default='html')
    506     parser.add_argument('--final', help='Only generate the final optimized MIR/LIR graphs.',
    507                                    action='store_true')
    508     parser.add_argument('--out-mir', help='Select the file where the MIR output would be written to.',
    509                                      type=argparse.FileType('w'))
    510     parser.add_argument('--out-lir', help='Select the file where the LIR output would be written to.',
    511                                      type=argparse.FileType('w'))
    512 
    513 
    514 def main():
    515     parser = argparse.ArgumentParser(description='Visualize Ion graphs using GraphViz.')
    516     subparsers = parser.add_subparsers()
    517     json = subparsers.add_parser('json', formatter_class=argparse.RawDescriptionHelpFormatter,
    518                                          help='Generate a graph from a JSON file.',
    519                                          description='Generate a graph from a JSON file.')
    520     js = subparsers.add_parser('js', formatter_class=argparse.RawDescriptionHelpFormatter,
    521                                      help='Run js and iongraph together in one call.',
    522                                      description='Run js and iongraph together in one call. Arguments before the -- separator are for iongraph, while arguments after the -- will be passed to js.\n\nexample:\n  iongraph js --funcnum 1 -- -m mymodule.mjs')
    523     jittest = subparsers.add_parser('jit-test', formatter_class=argparse.RawDescriptionHelpFormatter,
    524                                                 help='Run jit-test and iongraph together in one call.',
    525                                                 description='Run jit-test and iongraph together in one call. Arguments before the -- separator are for iongraph, while arguments after the -- will be passed to jit-test.\n\nexample:\n  iongraph jit-test --funcnum 1 -- category/test.js')
    526 
    527     add_main_arguments(json)
    528     json.add_argument('input', help='The JSON log file generated by IonMonkey. (Default: /tmp/ion.json)',
    529                                  nargs='?', default='/tmp/ion.json')
    530     json.set_defaults(func=gen)
    531 
    532     js.add_argument('--jspath', help='The path to the js executable.')
    533     add_main_arguments(js)
    534     js.add_argument('remaining', nargs=argparse.REMAINDER)
    535     js.set_defaults(func=js_and_gen)
    536 
    537     add_main_arguments(jittest)
    538     jittest.add_argument('remaining', nargs=argparse.REMAINDER)
    539     jittest.set_defaults(func=jittest_and_gen)
    540 
    541     args = parser.parse_args()
    542     args.func(args)
    543 
    544 
    545 if __name__ == '__main__':
    546     main()