#!/usr/bin/python from __future__ import print_function """ This script reads in the profile-raw file generated by Glulxe profiling, and lists the ten most costly functions. (In terms of how much total time was spent inside each function. If a function calls other functions, the time spent in them is not charged to the parent; that is, a function which does nothing but call other functions will be considered uncostly.) Optionally, this script can also read the debug output of the Inform 6 compiler (or the assembly output), and use that to figure out the names of all the functions that were profiled. Both the old and new (Inform 6.33) debug file format are supported. You can also generate profiling output in the same form as dumbfrotz's Z-machine profiling output. (If that happens to be what you want.) Use the --dumbfrotz argument. Using this script is currently a nuisance. The requirements: - You must compile Glulxe with profiling (the VM_PROFILING compile-time option). - (If you want function names) you should compile your Inform 6 source using the -k switch. This generates a "gameinfo.dbg" file. - Run Glulxe, using the "--profile profile-raw" option. (Optionally, also the "--profcalls" option.) Play some of the game, and quit. This generates a data file called "profile-raw". - Run this script, giving gameinfo.dbg and profile-raw as arguments. - You can provide "--glk dispatch_dump.xml" as an optional extra argument. This file gives the names of Glk functions; it is available from https://github.com/erkyrath/glk-dev/tree/master/dispatch_dump . To sum up, in command-line form: % inform -G -k game.inf % glulxe --profile profile-raw game.ulx % python profile-analyze.py profile-raw gameinfo.dbg --glk dispatch_dump.xml You can replace the debug output with the assembly output of the Inform compiler, which you get with the -a switch. Save the output and use it instead of the debug file: % inform -G -a game.inf > game.asm % glulxe --profile profile-raw game.ulx % python profile-analyze.py profile-raw game.asm --glk dispatch_dump.xml * The output: The output will be a list of ten functions, showing how much time and how many CPU cycles each one cost. Both the function's own cost and its cost including children (the functions it calls itself) are shown. (The top function is generally @glk_$c0 (or @glk_select). This just means that the game spent a lot of time waiting for user input.) You can change how many functions are listed with the --count option. You can change the sorting criterion with the --sort option: the options are self_time, self_ops, total_time, total_ops, and call_count. * The limitations: The profiling code is not smart about VM operations that rearrange the call stack. In fact, it's downright stupid. @restart, @restore, @restoreundo, or @throw will kill the interpreter. The old debug file format (Inform 6.32 and earlier) does not work correctly with game files larger than 16 megabytes. If you leave off the "--glk dispatch_dump.xml" argument, everything will still work, but @glk function entries will be listed by number rather than by name. If you included the "--profcalls" argument when running Glulxe, the function information will include the number of times it called, and was called by, every other function. This is rather slow, so it distorts the function timing information. Only use "--profcalls" when you are interested in specific call patterns (i.e., trying to identify where a particular function is being called from). * Interactive browsing: You can explore the profiling data in more detail by running the script interactively: % python -i profile-analyze.py profile-raw game.asm --glk dispatch_dump.xml After it runs, you'll be left at a Python prompt. You might want to list functions sorted in other ways: >>> list_by('total_time') # top 10 by total time including children >>> list_by('total_ops') # top 10 by total CPU cycles including children >>> list_by('self_ops', 20) # top 20 by CPU cycles excluding children You can also dig into the data directly. The environment will contain mappings called "functions" (mapping addresses to function objects), and "function_names" (names to function objects). >>> functions[0x3c] >>> function_names['Main__'] >>> function_names['Main__'].dump() Main__: at $00003c (line 0); called 1 times 0.000067 sec (1 ops) spent executing 6.273244 sec (117578 ops) including child calls >>> function_names['Main__'].show_calls() Main__: at $00003c (line 0); called 1 times: made 1 calls to other functions: 1 to (Again, the show_calls() information is only available if you use the "--profcalls" argument when running Glulxe.) A Function object has lots of attributes: addr=INT: The VM address of the function (in hex). hexaddr=STRING: The VM address of the function in hex (as a string). name=STRING: The function's name, or '' if the function is not known (veneer functions). linenum=INT: The line number of the function from the source code, or 0 if it is not derived from source (Main__, etc). call_count=INT: The number of times the function was called. accel_count=INT: The number of times the function was called with acceleration. total_time=FLOAT: The amount of time spent during all calls to the function (in seconds, as a floating-point value). total_ops=INT: The number of opcodes executed during all calls to the function. self_time=FLOAT: The amount of time spent during all calls to the function, excluding time spent in subcalls (functions called *by* the function). self_ops=INT: The number of opcodes executed during all calls to the function, excluding time spent in subcalls. max_depth=INT: The deepest this function has been nested on the stack, during any call. max_stack_use=INT: The greatest number of words on the stack, during any call. (This is measured when the function returns, so it may not capture the peak stack usage. If a function never returns, e.g. Main__(), then this value is approximate.) (The self_time is the "cost" used for the original listing.) Note that if a function does not make any function calls, total_time will be the same as self_time (and total_ops the same as self_ops). Some of the function entries refer to special interpreter operations. (These have high addresses, outside the range of normal game files.) Functions with addresses in the 0xE0000000 range are the interpreter's output opcodes: @streamchar, @streamunichar, @streamnum, @streamstr. Functions with addresses in the 0xF0000000 range are @glk opcode calls. The number in the lower bits specifies which Glk function was called. You will always see a large self_time for function 0xF00000C0; this represents all the time spent waiting for input in glk_select(). (Both the 0xE0000000 and 0xF0000000 entries represent time spent in the Glk library, but they get there by different code paths.) The function with the lowest address is the top-level Main__() function generated by the compiler. Its total_time is the running time of the entire program; its total_ops is the number of opcodes executed by the entire program; its max_depth is zero. * Debug file browsing: If you just want to browse an Inform debug file, and bypass all the profiling stuff, just do: # python -i profile-analyze.py -d gameinfo.dbg This will leave you at a Python prompt. The global "debugfile" contains the parsed debug information, which you can browse: >>> debugfile.arrays[0] )> """ import sys, os.path import optparse import io import xml.sax from struct import unpack from chunk import Chunk popt = optparse.OptionParser(usage='profile-analyze.py [options] profile-raw [ gameinfo.dbg | game.asm ]') popt.add_option('--glk', action='store', dest='dispatchfile', metavar='DISPATCH_DUMP', help='path to dispatch_dump.xml file (optional)') popt.add_option('--dumbfrotz', action='store_true', dest='dumbfrotz', help='use dumbfrotz-compatible output format') popt.add_option('-d', '--debug', action='store_true', dest='debugonly', help='read only the debug data, no profile data') popt.add_option('-c', '--count', action='store', dest='listcount', type=int, default=10, help='list the N slowest functions') popt.add_option('-s', '--sort', action='store', dest='listsort', type=str, default='self_time', help='sort criterion for functions (self_time, self_ops, total_time, total_ops, call_count)') (opts, args) = popt.parse_args() if (not args): print('Usage: profile-analyze.py [--dumbfrotz] [--glk dispatch_dump.xml] profile-raw [ gameinfo.dbg | game.asm ]') sys.exit(1) profile_raw = None game_file_data = None if not opts.debugonly: # Normal operation profile_raw = args[0] if (not os.path.exists(profile_raw)): print('File not readable:', profile_raw) sys.exit(1) if (len(args) >= 2): game_file_data = args[1] if (not os.path.exists(game_file_data)): print('File not readable:', game_file_data) sys.exit(1) else: # Debug-only operation game_file_data = args[0] if (not os.path.exists(game_file_data)): print('File not readable:', game_file_data) sys.exit(1) if (opts.dispatchfile): if (not os.path.exists(opts.dispatchfile)): print('File not readable:', opts.dispatchfile) sys.exit(1) special_functions = { 0xE0000001: 'streamchar', 0xE0000002: 'streamunichar', 0xE0000003: 'streamnum', 0xE0000004: 'streamstr', } glk_functions = {} functions = None callcounts = None sourcemap = None class Function: def __init__(self, addr, hexaddr, attrs): self.addr = addr self.hexaddr = hexaddr val = special_functions.get(addr) if (addr >= 0xF0000000): name = glk_functions.get(addr-0xF0000000) if (not name): name = hex(addr-0xF0000000)[2:] name = '$' + name.replace('L', '') self.name = '<@glk_' + name + '>' self.special = True elif (val is None): self.name = '' self.special = False else: self.name = '<@' + val + '>' self.special = True self.linenum = 0 self.call_count = int(attrs['call_count']) self.accel_count = 0 val = attrs.get('accel_count') if (val): self.accel_count = int(val) self.total_ops = int(attrs['total_ops']) self.total_time = float(attrs['total_time']) self.self_ops = int(attrs['self_ops']) self.self_time = float(attrs['self_time']) val = attrs.get('max_depth') if (val): self.max_depth = int(val) val = attrs.get('max_stack_use') if (val): self.max_stack_use = int(val) self.incalls = {} self.outcalls = {} def __repr__(self): return '' def dump(self): print('%s:' % (self.name,)) print(' %s' % (self.get_summary(),)) print(' %.6f sec (%d ops) spent executing' % (self.self_time, self.self_ops)) print(' %.6f sec (%d ops) including child calls' % (self.total_time, self.total_ops)) def dump_dumbfrotz_style(self): percent1 = ' ' percent2 = ' ' pc1 = int(100*(float(self.self_ops)/float(ops_executed))) pc2 = int(100*(float(self.total_ops)/float(ops_executed))) if (pc1 > 0): percent1 = "%3d%%" % pc1 if (pc2 > 0): percent2 = "%3d%%" % pc2 print('%-36s %s %-10lu %s %-10lu %-10lu %-4d' % (self.name, percent1, self.self_ops, percent2, self.total_ops, self.call_count, self.max_depth)) def get_summary(self): res = 'at $%06x' % (self.addr,) if (self.linenum): res += ' (line %d)' % (self.linenum,) res += '; called %d times' % (self.call_count,) if (self.accel_count): res += ' (%d accelerated)' % (self.accel_count,) return res def show_calls(self): if not callcounts: raise Exception('Profile data did not include call counts!') print('%s:' % (self.name,)) print(' %s:' % (self.get_summary(),)) ls = list(self.incalls.items()) ls.sort() # by addr for (addr, count) in ls: func = functions.get(addr, '') print(' %d from %s' % (count, func)) ls = list(self.outcalls.items()) ls.sort() # by addr val = sum([count for (addr, count) in ls]) print(' made %d calls to other functions:' % (val,)) for (addr, count) in ls: func = functions.get(addr, '') print(' %d to %s' % (count, func)) class DispatchDumpHandler(xml.sax.handler.ContentHandler): def startElement(self, name, attrs): if (name == 'function'): addr = int(attrs['id']) glk_functions[addr] = str(attrs['name']) class ProfileRawHandler(xml.sax.handler.ContentHandler): def startElement(self, name, attrs): global functions, callcounts if (name == 'profile'): functions = {} callcounts = {} if (name == 'function'): hexaddr = attrs.get('addr') addr = int(hexaddr, 16) func = Function(addr, hexaddr, attrs) functions[addr] = func if (name == 'calls'): hexaddr = attrs.get('fromaddr') fromaddr = int(hexaddr, 16) hexaddr = attrs.get('toaddr') toaddr = int(hexaddr, 16) count = int(attrs.get('count')) callcounts[(fromaddr, toaddr)] = count class SFrameHandler: def __init__(self, tag, parent=None, depth=None, children={}, active=None, handler=None): self.tag = tag self.parent = parent self.depth = depth self.children = children self.handler = handler if active is None: active = (handler is not None) self.active = active class SFrameFrame: def __init__(self, name, attrs, depth): self.name = name self.attrs = attrs self.depth = depth self.children = None self.handler = None self.accumchar = None self.accumobj = None def final(self): self.name = None self.attrs = None self.children = None self.handler = None self.accumchar = None self.accumobj = None class SimpleXMLFrame(xml.sax.handler.ContentHandler): def __init__(self): xml.sax.handler.ContentHandler.__init__(self) self.sstack = None self.taghandlers = {} self.init() def startDocument(self): self.sstack = [] def endDocument(self): assert len(self.sstack) == 0 def startElement(self, name, attrs): parframe = None if self.sstack: parframe = self.sstack[-1] frame = SFrameFrame(name, attrs, len(self.sstack)) self.sstack.append(frame) parhan = None if parframe and parframe.children: parhan = parframe.children.get(name) if parhan is not None: if type(parhan) is list: parhan = parhan[0] frame.handler = parhan if parhan in (int, str): frame.accumchar = [] elif parhan is (): taghan = self.taghandlers[name] frame.handler = taghan frame.children = taghan.children frame.accumobj = {} elif callable(parhan): frame.handler = parhan frame.accumchar = [] else: raise Exception('unknown element handler thingie: %s' % (parhan,)) else: taghan = self.taghandlers.get(name) if taghan: active = taghan.active if active: if taghan.parent is not None: if not (parframe and parframe.name == taghan.parent): active = False if taghan.depth is not None: if not (frame.depth == taghan.depth): active = False if active: frame.handler = taghan frame.children = taghan.children frame.accumobj = {} def characters(self, data): frame = self.sstack[-1] if frame.accumchar is not None: frame.accumchar.append(data) def endElement(self, name): frame = self.sstack.pop() assert frame.name == name res = None if frame.handler is None: pass elif frame.handler is str: res = ''.join(frame.accumchar) elif frame.handler is int: val = ''.join(frame.accumchar) res = int(val.strip()) elif isinstance(frame.handler, SFrameHandler): if frame.handler.handler: res = frame.handler.handler(frame.name, frame.attrs, frame.accumobj) else: res = frame.accumobj elif callable(frame.handler): val = ''.join(frame.accumchar) res = frame.handler(frame.name, frame.attrs, val) else: raise Exception('unknown element handler thingie: %s' % (frame.handler,)) frame.final() if self.sstack and res is not None: parframe = self.sstack[-1] if parframe.accumobj is not None: if type(parframe.children.get(name)) is list: subls = parframe.accumobj.get(name) if subls: subls.append(res) else: subls = [ res ] parframe.accumobj[name] = subls else: parframe.accumobj[name] = res def handle_tag(self, tag, parent=None, depth=None, children={}, active=None, handler=None): self.taghandlers[tag] = SFrameHandler(tag, parent, depth, children, active, handler) class NewDebugFile: def __init__(self): self.sourcefiles = {} self.constants = {} self.objects = [] self.globals = [] self.arrays = [] self.functions = [] class NewDebugSourceFile: def __init__(self, index, filename): self.index = index self.filename = filename def __repr__(self): return '' % (self.index, self.filename,) class NewDebugConstant: def __init__(self, ident, value=None, sourceloc=None): self.id = ident self.value = value if not sourceloc: sourceloc = 'compiler' self.sourceloc = sourceloc def __repr__(self): return '' % (self.id, self.value, self.sourceloc) class NewDebugObject: def __init__(self, ident, value=None, sourceloc=None, artificial=False): self.id = ident self.value = value if not sourceloc: sourceloc = 'compiler' self.sourceloc = sourceloc self.artificial = '*' if artificial else '' def __repr__(self): return '' % (self.id, self.artificial, self.value, self.sourceloc) class NewDebugGlobal: def __init__(self, ident, address, sourceloc=None): self.id = ident self.address = address if not sourceloc: sourceloc = 'compiler' self.sourceloc = sourceloc def __repr__(self): return '' % (self.id, self.address, self.sourceloc) class NewDebugArray: def __init__(self, ident, address, bytecount, bytesperel, sourceloc=None): self.id = ident self.address = address self.bytecount = bytecount self.bytesperel = bytesperel self.elcount = bytecount / bytesperel if not sourceloc: sourceloc = 'compiler' self.sourceloc = sourceloc def __repr__(self): return '' % (self.id, self.elcount, self.bytesperel, self.bytecount, self.address, self.sourceloc) class NewDebugFunction: def __init__(self, ident, address, args=(), sourceloc=None, artificial=False): self.id = ident self.address = address self.args = args if not sourceloc: sourceloc = 'veneer' self.sourceloc = sourceloc self.artificial = '*' if artificial else '' def __repr__(self): return '' % (self.id, self.artificial, self.address, self.sourceloc) class NewDebugSourceLoc: def __init__(self, line, fileref=None): self.line = line self.fileref = fileref def __repr__(self): if (isinstance(self.fileref, NewDebugSourceFile)): return '' % (self.fileref.filename, self.line) elif (self.fileref): return '' % (self.fileref, self.line) else: return '' % (self.line,) class NewDebugHandler(SimpleXMLFrame): def init(self): self._debugfile = NewDebugFile() self.handle_tag('source', parent='inform-story-file', children={'given-path':str}, handler=self.handle_source_file) self.handle_tag('constant', parent='inform-story-file', children={'identifier':str, 'value':int, 'source-code-location':()}, handler=self.handle_constant) self.handle_tag('object', parent='inform-story-file', children={'identifier':self.handle_ident_artificial, 'value':int, 'source-code-location':()}, handler=self.handle_object) self.handle_tag('global-variable', parent='inform-story-file', children={'identifier':str, 'address':int, 'source-code-location':()}, handler=self.handle_global_var) self.handle_tag('array', parent='inform-story-file', children={'identifier':str, 'value':int, 'byte-count':int, 'bytes-per-element':int, 'source-code-location':()}, handler=self.handle_array) self.handle_tag('routine', parent='inform-story-file', children={'identifier':self.handle_ident_artificial, 'address':int, 'source-code-location':(), 'local-variable':[()]}, handler=self.handle_function) self.handle_tag('source-code-location', active=False, children={'line':int, 'file-index':int}, handler=self.handle_source_code_loc) self.handle_tag('local-variable', children={'identifier':str}) def debugfile(self): return self._debugfile def handle_source_file(self, name, attrs, obj): srcfile = NewDebugSourceFile(int(attrs['index']), obj['given-path']) self._debugfile.sourcefiles[srcfile.index] = srcfile def handle_constant(self, name, attrs, obj): con = NewDebugConstant(obj['identifier'], obj.get('value'), obj.get('source-code-location')) self._debugfile.constants[con.id] = con def handle_object(self, name, attrs, obj): (ident, artificial) = obj['identifier'] con = NewDebugObject(ident, obj.get('value'), obj.get('source-code-location'), artificial=artificial) self._debugfile.objects.append(con) def handle_global_var(self, name, attrs, obj): glob = NewDebugGlobal(obj['identifier'], obj['address'], obj.get('source-code-location')) self._debugfile.globals.append(glob) def handle_array(self, name, attrs, obj): arr = NewDebugArray(obj['identifier'], obj['value'], obj['byte-count'], obj['bytes-per-element'], obj.get('source-code-location')) self._debugfile.arrays.append(arr) def handle_function(self, name, attrs, obj): (ident, artificial) = obj['identifier'] args = obj.get('local-variable') if not args: args = () else: args = tuple([ loc['identifier'] for loc in args ]) func = NewDebugFunction(ident, obj['address'], args, obj.get('source-code-location'), artificial=artificial) self._debugfile.functions.append(func) def handle_ident_artificial(self, name, attrs, obj): artificial = attrs.get('artificial') return (obj.strip(), artificial) def handle_source_code_loc(self, name, attrs, obj): fileref = obj.get('file-index') if fileref is not None: srcfile = self._debugfile.sourcefiles.get(fileref) if srcfile: fileref = srcfile return NewDebugSourceLoc(obj['line'], fileref) def parse_inform_assembly(fl): global sourcemap sourcemap = {} lasttup = None while True: ln = fl.readline() if (not ln): break ln = ln.strip() ls = ln.split() if (lasttup and not ls): (linenum, funcname, addr) = lasttup sourcemap[addr] = (linenum, funcname) lasttup = None try: if (len(ls) >= 4 and ls[2] == '[' and ls[1].startswith('+')): linenum = int(ls[0]) funcname = ls[3] addr = int(ls[1][1:], 16) lasttup = (linenum, funcname, addr) except ValueError: pass class InformFunc: def __init__(self, funcnum): self.funcnum = funcnum self.name = '' self.addr = 0 self.linenum = None self.endaddr = None self.endlinenum = None self.locals = None self.seqpts = None def __repr__(self): return '' class DebugFile: def __init__(self, fl): self.files = {} self.functions = {} self.function_names = {} self.classes = [] self.objects = {} self.arrays = {} self.globals = {} self.properties = {} self.attributes = {} self.actions = {} self.fake_actions = {} self.map = {} self.header = None dat = fl.read(2) val = unpack('>H', dat)[0] if (val != 0xDEBF): raise ValueError('not an Inform debug file') dat = fl.read(2) self.debugversion = unpack('>H', dat)[0] dat = fl.read(2) self.informversion = unpack('>H', dat)[0] rectable = { 1: self.read_file_rec, 2: self.read_class_rec, 3: self.read_object_rec, 4: self.read_global_rec, 5: self.read_attr_rec, 6: self.read_prop_rec, 7: self.read_fake_action_rec, 8: self.read_action_rec, 9: self.read_header_rec, 10: self.read_lineref_rec, 11: self.read_routine_rec, 12: self.read_array_rec, 13: self.read_map_rec, 14: self.read_routine_end_rec, } while True: dat = fl.read(1) rectype = unpack('>B', dat)[0] if (rectype == 0): break recfunc = rectable.get(rectype) if (not recfunc): raise ValueError('unknown debug record type: %d' % (rectype,)) recfunc(fl) for func in self.functions.values(): self.function_names[func.name] = func def read_file_rec(self, fl): dat = fl.read(1) filenum = unpack('>B', dat)[0] includename = self.read_string(fl) realname = self.read_string(fl) self.files[filenum] = ( includename, realname ) def read_class_rec(self, fl): name = self.read_string(fl) start = self.read_linenum(fl) end = self.read_linenum(fl) self.classes.append( (name, start, end) ) def read_object_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) start = self.read_linenum(fl) end = self.read_linenum(fl) self.objects[num] = (name, start, end) def read_global_rec(self, fl): dat = fl.read(1) num = unpack('>B', dat)[0] name = self.read_string(fl) self.globals[num] = name def read_array_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) self.arrays[num] = name def read_attr_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) self.attributes[num] = name def read_prop_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) self.properties[num] = name def read_action_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) self.actions[num] = name def read_fake_action_rec(self, fl): dat = fl.read(2) num = unpack('>H', dat)[0] name = self.read_string(fl) self.fake_actions[num] = name def read_routine_rec(self, fl): dat = fl.read(2) funcnum = unpack('>H', dat)[0] func = self.get_function(funcnum) func.linenum = self.read_linenum(fl) dat = fl.read(3) addr = unpack('>I', b'\0'+dat)[0] func.addr = int(addr) func.name = self.read_string(fl) locals = [] while True: val = self.read_string(fl) if (not val): break locals.append(val) func.locals = locals def read_lineref_rec(self, fl): dat = fl.read(2) funcnum = unpack('>H', dat)[0] func = self.get_function(funcnum) if (not func.seqpts): func.seqpts = [] dat = fl.read(2) count = unpack('>H', dat)[0] for ix in range(count): linenum = self.read_linenum(fl) dat = fl.read(2) addr = unpack('>H', dat)[0] func.seqpts.append( (linenum, addr) ) def read_routine_end_rec(self, fl): dat = fl.read(2) funcnum = unpack('>H', dat)[0] func = self.get_function(funcnum) func.endlinenum = self.read_linenum(fl) dat = fl.read(3) addr = unpack('>I', b'\0'+dat)[0] func.endaddr = int(addr) def read_header_rec(self, fl): dat = fl.read(64) self.header = dat def read_map_rec(self, fl): while True: name = self.read_string(fl) if (not name): break dat = fl.read(3) addr = unpack('>I', b'\0'+dat)[0] addr = int(addr) self.map[name] = addr def read_linenum(self, fl): dat = fl.read(4) (funcnum, linenum, charnum) = unpack('>BHB', dat) return (funcnum, linenum, charnum) def read_string(self, fl): val = b'' while True: dat = fl.read(1) if (dat == b'\0'): return val.decode() val += dat def get_function(self, funcnum): func = self.functions.get(funcnum) if (not func): func = InformFunc(funcnum) self.functions[funcnum] = func return func # A simple I/O wrapper class: read a subrange of a (readable binary) # file. We use this to treat a Blorb chunk as a readable file. class BinaryRangeIO(io.RawIOBase): def __init__(self, file, start, length): self.file = file self.start = start self.length = length self.offset = 0 self.file.seek(self.start) def readable(self): return True def close(self): self.file = None io.RawIOBase.close(self) def tell(self): return self.offset def readinto(self, dat): count = len(dat) if count > self.length - self.offset: count = self.length - self.offset newdat = self.file.read(count) newlen = len(newdat) dat[:newlen] = newdat else: newlen = self.file.readinto(dat) self.offset += newlen return newlen def typestring(dat): return "'" + dat.decode() + "'" class BlorbChunk: def __init__(self, formchunk, typ, start, len, formtype=None): self.formchunk = formchunk self.type = typ self.start = start self.len = len self.formtype = formtype def __repr__(self): return '' % (typestring(self.type), self.start, self.len) def data(self, max=None): self.formchunk.seek(self.start) toread = self.len if (max is not None): toread = min(self.len, max) return self.formchunk.read(toread) def describe(self): if (not self.formtype): return '%s (%d bytes, start %d)' % (typestring(self.type), self.len, self.start) else: return '%s/%s (%d+8 bytes, start %d)' % (typestring(self.type), typestring(self.formtype), self.len, self.start) def blorb_find_debug_chunk(filename): file = open(filename, 'rb') formchunk = Chunk(file) formchunk = formchunk if (formchunk.getname() != b'FORM'): raise Exception('This does not appear to be a Blorb file.') formtype = formchunk.read(4) if (formtype != b'IFRS'): raise Exception('This does not appear to be a Blorb file.') chunks = [] debugchunk = None formlen = formchunk.getsize() while formchunk.tell() < formlen: chunk = Chunk(formchunk) start = formchunk.tell() size = chunk.getsize() formtype = None if chunk.getname() == b'FORM': formtype = chunk.read(4) subchunk = BlorbChunk(formchunk, chunk.getname(), start, size, formtype) chunks.append(subchunk) chunk.skip() chunk.close() for chunk in chunks: if (chunk.type == b'Dbug'): debugchunk = chunk formchunk.close() file.close() file = None return debugchunk def list_by(key='self_time', limit=10): ls = functions.values() ls.sort(lambda x1, x2: cmp(getattr(x2, key), getattr(x1, key))) for func in ls[:limit]: func.dump() # Read in the various files if (opts.dispatchfile): # Fills out the glk_functions global xml.sax.parse(opts.dispatchfile, DispatchDumpHandler()) if (profile_raw): # Fills out the functions global xml.sax.parse(profile_raw, ProfileRawHandler()) need_function_address_offset = False if (game_file_data): # Fill out the sourcemap global, by one of various methods fl = open(game_file_data, 'rb') val = fl.read(2) fl.close() if (not val): pass elif (val == b'\xde\xbf'): # Old-style Inform debug info. need_function_address_offset = True fl = open(game_file_data, 'rb') debugfile = DebugFile(fl) fl.close() sourcemap = {} for func in debugfile.functions.values(): sourcemap[func.addr] = ( func.linenum[1], func.name ) elif (val == b' ops_executed): ops_executed = func.total_ops routine_calls = routine_calls + func.call_count print('Total opcodes: %lu' % ops_executed) print('Total routine calls: %lu' % routine_calls) print('Max. stack usage: %li' % max_stack_use) print('') print('%-35s %-10s %-10s %-10s %-4s' % ('Routine', 'Ops', 'Ops(+Subs)', 'Calls', 'Nest')) for func in ls: func.dump_dumbfrotz_style() else: print('Functions that consumed the most time (excluding children):') ls = list(functions.values()) sortfunc = lambda fn:-fn.self_time if (opts.listsort in ('self_time', 'self-time', 'selftime')): sortfunc = lambda fn:-fn.self_time elif (opts.listsort in ('self_ops', 'self-ops', 'selfops')): sortfunc = lambda fn:-fn.self_ops elif (opts.listsort in ('total_time', 'total-time', 'totaltime')): sortfunc = lambda fn:-fn.total_time elif (opts.listsort in ('total_ops', 'total-ops', 'totalops')): sortfunc = lambda fn:-fn.total_ops elif (opts.listsort in ('call_count', 'call-count', 'callcount')): sortfunc = lambda fn:-fn.call_count ls.sort(key=sortfunc) for func in ls[0 : opts.listcount]: func.dump()