| """Class for printing reports on profiled python code.""" | 
 |  | 
 | # Class for printing reports on profiled python code. rev 1.0  4/1/94 | 
 | # | 
 | # Based on prior profile module by Sjoerd Mullender... | 
 | #   which was hacked somewhat by: Guido van Rossum | 
 | # | 
 | # see profile.doc and profile.py for more info. | 
 |  | 
 | # Copyright 1994, by InfoSeek Corporation, all rights reserved. | 
 | # Written by James Roskind | 
 | #  | 
 | # Permission to use, copy, modify, and distribute this Python software | 
 | # and its associated documentation for any purpose (subject to the | 
 | # restriction in the following sentence) without fee is hereby granted, | 
 | # provided that the above copyright notice appears in all copies, and | 
 | # that both that copyright notice and this permission notice appear in | 
 | # supporting documentation, and that the name of InfoSeek not be used in | 
 | # advertising or publicity pertaining to distribution of the software | 
 | # without specific, written prior permission.  This permission is | 
 | # explicitly restricted to the copying and modification of the software | 
 | # to remain in Python, compiled Python, or other languages (such as C) | 
 | # wherein the modified or derived code is exclusively imported into a | 
 | # Python module. | 
 | #  | 
 | # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS | 
 | # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | 
 | # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY | 
 | # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER | 
 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF | 
 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | 
 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | 
 |  | 
 |  | 
 | import os | 
 | import time | 
 | import string | 
 | import marshal | 
 | import re | 
 |  | 
 | import fpformat | 
 |  | 
 | class Stats: | 
 | 	"""This class is used for creating reports from data generated by the | 
 | 	Profile class.  It is a "friend" of that class, and imports data either | 
 | 	by direct access to members of Profile class, or by reading in a dictionary | 
 | 	that was emitted (via marshal) from the Profile class. | 
 |  | 
 | 	The big change from the previous Profiler (in terms of raw functionality) | 
 | 	is that an "add()" method has been provided to combine Stats from | 
 | 	several distinct profile runs.  Both the constructor and the add() | 
 | 	method now take arbitrarily many file names as arguments. | 
 |  | 
 | 	All the print methods now take an argument that indicates how many lines | 
 | 	to print.  If the arg is a floating point number between 0 and 1.0, then | 
 | 	it is taken as a decimal percentage of the available lines to be printed | 
 | 	(e.g., .1 means print 10% of all available lines).  If it is an integer, | 
 | 	it is taken to mean the number of lines of data that you wish to have | 
 | 	printed. | 
 |  | 
 | 	The sort_stats() method now processes some additional options (i.e., in | 
 | 	addition to the old -1, 0, 1, or 2).  It takes an arbitrary number of quoted | 
 | 	strings to select the sort order.  For example sort_stats('time', 'name') | 
 | 	sorts on the major key of "internal function time", and on the minor | 
 | 	key of 'the name of the function'.  Look at the two tables in sort_stats() | 
 | 	and get_sort_arg_defs(self) for more examples. | 
 |  | 
 | 	All methods now return "self",  so you can string together commands like: | 
 | 	    Stats('foo', 'goo').strip_dirs().sort_stats('calls').\ | 
 | 	                        print_stats(5).print_callers(5) | 
 | 	""" | 
 | 	 | 
 | 	def __init__(self, *args): | 
 | 		if not len(args): | 
 | 			arg = None | 
 | 		else: | 
 | 			arg = args[0] | 
 | 			args = args[1:] | 
 | 		self.init(arg) | 
 | 		apply(self.add, args).ignore() | 
 | 			 | 
 | 	def init(self, arg): | 
 | 		self.all_callees = None  # calc only if needed | 
 | 		self.files = [] | 
 | 		self.fcn_list = None | 
 | 		self.total_tt = 0 | 
 | 		self.total_calls = 0 | 
 | 		self.prim_calls = 0 | 
 | 		self.max_name_len = 0 | 
 | 		self.top_level = {} | 
 | 		self.stats = {} | 
 | 		self.sort_arg_dict = {} | 
 | 		self.load_stats(arg) | 
 | 		trouble = 1 | 
 | 		try: | 
 | 			self.get_top_level_stats() | 
 | 			trouble = 0 | 
 | 		finally: | 
 | 			if trouble: | 
 | 				print "Invalid timing data", | 
 | 				if self.files: print self.files[-1], | 
 | 				print | 
 |  | 
 |  | 
 | 	def load_stats(self, arg): | 
 | 		if not arg:  self.stats = {} | 
 | 		elif type(arg) == type(""): | 
 | 			f = open(arg, 'rb') | 
 | 			self.stats = marshal.load(f) | 
 | 			f.close() | 
 | 			try: | 
 | 				file_stats = os.stat(arg) | 
 | 				arg = time.ctime(file_stats[8]) + "    " + arg | 
 | 			except:  # in case this is not unix | 
 | 				pass | 
 | 			self.files = [ arg ] | 
 | 		elif hasattr(arg, 'create_stats'): | 
 | 			arg.create_stats() | 
 | 			self.stats = arg.stats | 
 | 			arg.stats = {} | 
 | 		if not self.stats: | 
 | 			raise TypeError,  "Cannot create or construct a " \ | 
 | 				  + `self.__class__` \ | 
 | 				  + " object from '" + `arg` + "'" | 
 | 		return | 
 |  | 
 | 	def get_top_level_stats(self): | 
 | 		for func in self.stats.keys(): | 
 | 			cc, nc, tt, ct, callers = self.stats[func] | 
 | 			self.total_calls = self.total_calls + nc | 
 | 			self.prim_calls  = self.prim_calls  + cc | 
 | 			self.total_tt    = self.total_tt    + tt | 
 | 			if callers.has_key(("jprofile", 0, "profiler")): | 
 | 				self.top_level[func] = None | 
 | 			if len(func_std_string(func)) > self.max_name_len: | 
 | 				self.max_name_len = len(func_std_string(func)) | 
 | 					 | 
 | 	def add(self, *arg_list): | 
 | 		if not arg_list: return self | 
 | 		if len(arg_list) > 1: apply(self.add, arg_list[1:]) | 
 | 		other = arg_list[0] | 
 | 		if type(self) != type(other) or \ | 
 | 			  self.__class__ != other.__class__: | 
 | 			other = Stats(other) | 
 | 		self.files = self.files + other.files | 
 | 		self.total_calls = self.total_calls + other.total_calls | 
 | 		self.prim_calls = self.prim_calls + other.prim_calls | 
 | 		self.total_tt = self.total_tt + other.total_tt | 
 | 		for func in other.top_level.keys(): | 
 | 			self.top_level[func] = None | 
 |  | 
 | 		if self.max_name_len < other.max_name_len: | 
 | 			self.max_name_len = other.max_name_len | 
 |  | 
 | 		self.fcn_list = None | 
 |  | 
 | 		for func in other.stats.keys(): | 
 | 			if self.stats.has_key(func): | 
 | 				old_func_stat = self.stats[func] | 
 | 			else: | 
 | 				old_func_stat = (0, 0, 0, 0, {},) | 
 | 			self.stats[func] = add_func_stats(old_func_stat, \ | 
 | 				  other.stats[func]) | 
 | 		return self | 
 | 			 | 
 |  | 
 |  | 
 | 	# list the tuple indices and directions for sorting, | 
 | 	# along with some printable description | 
 | 	sort_arg_dict_default = {\ | 
 | 		  "calls"     : (((1,-1),              ), "call count"),\ | 
 | 		  "cumulative": (((3,-1),              ), "cumulative time"),\ | 
 | 		  "file"      : (((4, 1),              ), "file name"),\ | 
 | 		  "line"      : (((5, 1),              ), "line number"),\ | 
 | 		  "module"    : (((4, 1),              ), "file name"),\ | 
 | 		  "name"      : (((6, 1),              ), "function name"),\ | 
 | 		  "nfl"       : (((6, 1),(4, 1),(5, 1),), "name/file/line"), \ | 
 | 		  "pcalls"    : (((0,-1),              ), "call count"),\ | 
 | 		  "stdname"   : (((7, 1),              ), "standard name"),\ | 
 | 		  "time"      : (((2,-1),              ), "internal time"),\ | 
 | 		  } | 
 |  | 
 | 	def get_sort_arg_defs(self): | 
 | 		"""Expand all abbreviations that are unique.""" | 
 | 		if not self.sort_arg_dict: | 
 | 			self.sort_arg_dict = dict = {} | 
 | 			std_list = dict.keys() | 
 | 			bad_list = {} | 
 | 			for word in self.sort_arg_dict_default.keys(): | 
 | 				fragment = word | 
 | 				while fragment: | 
 | 					if not fragment: | 
 | 						break | 
 | 					if dict.has_key(fragment): | 
 | 						bad_list[fragment] = 0 | 
 | 						break | 
 | 					dict[fragment] = self. \ | 
 | 						  sort_arg_dict_default[word] | 
 | 					fragment = fragment[:-1] | 
 | 			for word in bad_list.keys(): | 
 | 				del dict[word] | 
 | 		return self.sort_arg_dict | 
 | 			 | 
 |  | 
 | 	def sort_stats(self, *field): | 
 | 		if not field: | 
 | 			self.fcn_list = 0 | 
 | 			return self | 
 | 		if len(field) == 1 and type(field[0]) == type(1): | 
 | 			# Be compatible with old profiler | 
 | 			field = [ {-1: "stdname", \ | 
 | 				  0:"calls", \ | 
 | 				  1:"time", \ | 
 | 				  2: "cumulative" }  [ field[0] ] ] | 
 |  | 
 | 		sort_arg_defs = self.get_sort_arg_defs() | 
 | 		sort_tuple = () | 
 | 		self.sort_type = "" | 
 | 		connector = "" | 
 | 		for word in field: | 
 | 			sort_tuple = sort_tuple + sort_arg_defs[word][0] | 
 | 			self.sort_type = self.sort_type + connector + \ | 
 | 				  sort_arg_defs[word][1] | 
 | 			connector = ", " | 
 | 					 | 
 | 		stats_list = [] | 
 | 		for func in self.stats.keys(): | 
 | 			cc, nc, tt, ct, callers = self.stats[func] | 
 | 			stats_list.append((cc, nc, tt, ct) + func_split(func) \ | 
 | 				           + (func_std_string(func), func,)  ) | 
 |  | 
 | 		stats_list.sort(TupleComp(sort_tuple).compare) | 
 |  | 
 | 		self.fcn_list = fcn_list = [] | 
 | 		for tuple in stats_list: | 
 | 			fcn_list.append(tuple[-1]) | 
 | 		return self | 
 |  | 
 |  | 
 | 	def reverse_order(self): | 
 | 		if self.fcn_list: self.fcn_list.reverse() | 
 | 		return self | 
 |  | 
 | 	def strip_dirs(self): | 
 | 		oldstats = self.stats | 
 | 		self.stats = newstats = {} | 
 | 		max_name_len = 0 | 
 | 		for func in oldstats.keys(): | 
 | 			cc, nc, tt, ct, callers = oldstats[func] | 
 | 			newfunc = func_strip_path(func) | 
 | 			if len(func_std_string(newfunc)) > max_name_len: | 
 | 				max_name_len = len(func_std_string(newfunc)) | 
 | 			newcallers = {} | 
 | 			for func2 in callers.keys(): | 
 | 				newcallers[func_strip_path(func2)] = \ | 
 | 					  callers[func2] | 
 |  | 
 | 			if newstats.has_key(newfunc): | 
 | 				newstats[newfunc] = add_func_stats( \ | 
 | 					  newstats[newfunc],\ | 
 | 					  (cc, nc, tt, ct, newcallers)) | 
 | 			else: | 
 | 				newstats[newfunc] = (cc, nc, tt, ct, newcallers) | 
 | 		old_top = self.top_level | 
 | 		self.top_level = new_top = {} | 
 | 		for func in old_top.keys(): | 
 | 			new_top[func_strip_path(func)] = None | 
 |  | 
 | 		self.max_name_len = max_name_len | 
 |  | 
 | 		self.fcn_list = None | 
 | 		self.all_callees = None | 
 | 		return self | 
 |  | 
 |  | 
 |  | 
 | 	def calc_callees(self): | 
 | 		if self.all_callees: return | 
 | 		self.all_callees = all_callees = {} | 
 | 		for func in self.stats.keys(): | 
 | 			if not all_callees.has_key(func): | 
 | 				all_callees[func] = {} | 
 | 			cc, nc, tt, ct, callers = self.stats[func] | 
 | 			for func2 in callers.keys(): | 
 | 				if not all_callees.has_key(func2): | 
 | 					all_callees[func2] = {} | 
 | 				all_callees[func2][func]  = callers[func2] | 
 | 		return | 
 |  | 
 | 	#****************************************************************** | 
 | 	# The following functions support actual printing of reports | 
 | 	#****************************************************************** | 
 |  | 
 | 	# Optional "amount" is either a line count, or a percentage of lines. | 
 |  | 
 | 	def eval_print_amount(self, sel, list, msg): | 
 | 		new_list = list | 
 | 		if type(sel) == type(""): | 
 | 			new_list = [] | 
 | 			for func in list: | 
 | 				if re.search(sel, func_std_string(func)): | 
 | 					new_list.append(func) | 
 | 		else: | 
 | 			count = len(list) | 
 | 			if type(sel) == type(1.0) and 0.0 <= sel < 1.0: | 
 | 				count = int (count * sel + .5) | 
 | 				new_list = list[:count] | 
 | 			elif type(sel) == type(1) and 0 <= sel < count: | 
 | 				count = sel | 
 | 				new_list = list[:count] | 
 | 		if len(list) != len(new_list): | 
 | 			msg = msg + "   List reduced from " + `len(list)` \ | 
 | 				  + " to " + `len(new_list)` + \ | 
 | 				  " due to restriction <" + `sel` + ">\n" | 
 | 			 | 
 | 		return new_list, msg | 
 |  | 
 |  | 
 |  | 
 | 	def get_print_list(self, sel_list): | 
 | 		width = self.max_name_len | 
 | 		if self.fcn_list: | 
 | 			list = self.fcn_list[:] | 
 | 			msg = "   Ordered by: " + self.sort_type + '\n' | 
 | 		else: | 
 | 			list = self.stats.keys() | 
 | 			msg = "   Random listing order was used\n" | 
 |  | 
 | 		for selection in sel_list: | 
 | 			list,msg = self.eval_print_amount(selection, list, msg) | 
 |  | 
 | 		count = len(list) | 
 |  | 
 | 		if not list: | 
 | 			return 0, list | 
 | 		print msg | 
 | 		if count < len(self.stats): | 
 | 			width = 0 | 
 | 			for func in list: | 
 | 				if  len(func_std_string(func)) > width: | 
 | 					width = len(func_std_string(func)) | 
 | 		return width+2, list | 
 | 		 | 
 | 	def print_stats(self, *amount): | 
 | 		for filename in self.files: | 
 | 			print filename | 
 | 		if self.files: print | 
 | 		indent = "        " | 
 | 		for func in self.top_level.keys(): | 
 | 			print indent, func_get_function_name(func) | 
 | 		 | 
 | 		print  indent, self.total_calls, "function calls", | 
 | 		if self.total_calls != self.prim_calls: | 
 | 			print "(" + `self.prim_calls`, "primitive calls)",  | 
 | 		print "in", fpformat.fix(self.total_tt, 3), "CPU seconds" | 
 | 		print | 
 | 		width, list = self.get_print_list(amount) | 
 | 		if list: | 
 | 			self.print_title() | 
 | 			for func in list: | 
 | 				self.print_line(func) | 
 | 			print  | 
 | 			print | 
 | 		return self | 
 |  | 
 | 			 | 
 | 	def print_callees(self, *amount): | 
 | 		width, list = self.get_print_list(amount) | 
 | 		if list: | 
 | 			self.calc_callees() | 
 |  | 
 | 			self.print_call_heading(width, "called...") | 
 | 			for func in list: | 
 | 				if self.all_callees.has_key(func): | 
 | 					self.print_call_line(width, \ | 
 | 						  func, self.all_callees[func]) | 
 | 				else: | 
 | 					self.print_call_line(width, func, {}) | 
 | 			print | 
 | 			print | 
 | 		return self | 
 |  | 
 | 	def print_callers(self, *amount): | 
 | 		width, list = self.get_print_list(amount) | 
 | 		if list: | 
 | 			self.print_call_heading(width, "was called by...") | 
 | 			for func in list: | 
 | 				cc, nc, tt, ct, callers = self.stats[func] | 
 | 				self.print_call_line(width, func, callers) | 
 | 			print | 
 | 			print | 
 | 		return self | 
 |  | 
 | 	def print_call_heading(self, name_size, column_title): | 
 | 		print string.ljust("Function ", name_size) + column_title | 
 |  | 
 |  | 
 | 	def print_call_line(self, name_size, source, call_dict): | 
 | 		print string.ljust(func_std_string(source), name_size), | 
 | 		if not call_dict: | 
 | 			print "--" | 
 | 			return | 
 | 		clist = call_dict.keys() | 
 | 		clist.sort() | 
 | 		name_size = name_size + 1 | 
 | 		indent = "" | 
 | 		for func in clist: | 
 | 			name = func_std_string(func) | 
 | 			print indent*name_size + name + '(' \ | 
 | 				  + `call_dict[func]`+')', \ | 
 | 				  f8(self.stats[func][3]) | 
 | 			indent = " " | 
 |  | 
 |  | 
 |  | 
 | 	def print_title(self): | 
 | 		print string.rjust('ncalls', 9), | 
 | 		print string.rjust('tottime', 8), | 
 | 		print string.rjust('percall', 8), | 
 | 		print string.rjust('cumtime', 8), | 
 | 		print string.rjust('percall', 8), | 
 | 		print 'filename:lineno(function)' | 
 |  | 
 |  | 
 | 	def print_line(self, func):  # hack : should print percentages | 
 | 		cc, nc, tt, ct, callers = self.stats[func] | 
 | 		c = `nc` | 
 | 		if nc != cc: | 
 | 			c = c + '/' + `cc` | 
 | 		print string.rjust(c, 9), | 
 | 		print f8(tt), | 
 | 		if nc == 0: | 
 | 			print ' '*8, | 
 | 		else: | 
 | 			print f8(tt/nc), | 
 | 		print f8(ct), | 
 | 		if cc == 0: | 
 | 			print ' '*8, | 
 | 		else: | 
 | 			print f8(ct/cc), | 
 | 		print func_std_string(func) | 
 |  | 
 |  | 
 | 	def ignore(self): | 
 | 		pass # has no return value, so use at end of line :-) | 
 |  | 
 |  | 
 | class TupleComp: | 
 | 	"""This class provides a generic function for comparing any two tuples. | 
 | 	Each instance records a list of tuple-indices (from most significant | 
 | 	to least significant), and sort direction (ascending or decending) for | 
 | 	each tuple-index.  The compare functions can then be used as the function | 
 | 	argument to the system sort() function when a list of tuples need to be | 
 | 	sorted in the instances order.""" | 
 |  | 
 | 	def __init__(self, comp_select_list): | 
 | 		self.comp_select_list = comp_select_list | 
 |  | 
 | 	def compare (self, left, right): | 
 | 		for index, direction in self.comp_select_list: | 
 | 			l = left[index] | 
 | 			r = right[index] | 
 | 			if l < r: | 
 | 				return -direction | 
 | 			if l > r: | 
 | 				return direction | 
 | 		return 0 | 
 |  | 
 | 		 | 
 |  | 
 | #************************************************************************** | 
 |  | 
 | def func_strip_path(func_name): | 
 | 	file, line, name = func_name | 
 | 	return os.path.basename(file), line, name | 
 |  | 
 | def func_get_function_name(func): | 
 | 	return func[2] | 
 |  | 
 | def func_std_string(func_name): # match what old profile produced | 
 | 	file, line, name = func_name | 
 | 	return file + ":" + `line` + "(" + name + ")" | 
 |  | 
 | def func_split(func_name): | 
 | 	return func_name | 
 |  | 
 | #************************************************************************** | 
 | # The following functions combine statists for pairs functions. | 
 | # The bulk of the processing involves correctly handling "call" lists, | 
 | # such as callers and callees.  | 
 | #************************************************************************** | 
 |  | 
 | def add_func_stats(target, source): | 
 | 	"""Add together all the stats for two profile entries.""" | 
 | 	cc, nc, tt, ct, callers = source | 
 | 	t_cc, t_nc, t_tt, t_ct, t_callers = target | 
 | 	return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, \ | 
 | 		  add_callers(t_callers, callers)) | 
 |  | 
 |  | 
 | def add_callers(target, source): | 
 | 	"""Combine two caller lists in a single list.""" | 
 | 	new_callers = {} | 
 | 	for func in target.keys(): | 
 | 		new_callers[func] = target[func] | 
 | 	for func in source.keys(): | 
 | 		if new_callers.has_key(func): | 
 | 			new_callers[func] = source[func] + new_callers[func] | 
 | 		else: | 
 | 			new_callers[func] = source[func] | 
 | 	return new_callers | 
 |  | 
 | def count_calls(callers): | 
 | 	"""Sum the caller statistics to get total number of calls received.""" | 
 | 	nc = 0 | 
 | 	for func in callers.keys(): | 
 | 		nc = nc + callers[func] | 
 | 	return nc | 
 |  | 
 | #************************************************************************** | 
 | # The following functions support printing of reports | 
 | #************************************************************************** | 
 |  | 
 | def f8(x): | 
 | 	return string.rjust(fpformat.fix(x, 3), 8) | 
 |  |