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/> >;' % (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">((%s))</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()