2 # Class for printing reports on profiled python code. rev 1.0 4/1/94
4 # Based on prior profile module by Sjoerd Mullender...
5 # which was hacked somewhat by: Guido van Rossum
7 # see jprofile.doc and jprofile.py for more info.
9 # Copyright 1994, by InfoSeek Corporation, all rights reserved.
10 # Written by James Roskind
12 # Permission to use, copy, modify, and distribute this Python software
13 # and its associated documentation for any purpose (subject to the
14 # restriction in the following sentence) without fee is hereby granted,
15 # provided that the above copyright notice appears in all copies, and
16 # that both that copyright notice and this permission notice appear in
17 # supporting documentation, and that the name of InfoSeek not be used in
18 # advertising or publicity pertaining to distribution of the software
19 # without specific, written prior permission. This permission is
20 # explicitly restricted to the copying and modification of the software
21 # to remain in Python, compiled Python, or other languages (such as C)
22 # wherein the modified or derived code is exclusively imported into a
25 # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
26 # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
27 # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY
28 # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
29 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
30 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
31 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
40 #**************************************************************************
41 # Class Stats documentation
42 #**************************************************************************
43 # This class is used for creating reports from data generated by the
44 # Profile class. It is a "friend" of that class, and imports data either
45 # by direct access to members of Profile class, or by reading in a dictionary
46 # that was emitted (via marshal) from the Profile class.
48 # The big change from the previous Profiler (in terms of raw functionality)
49 # is that an "add()" method has been provided to combine Stats from
50 # several distinct profile runs. Both the constructor and the add()
51 # method now take arbitrarilly many file names as arguments.
53 # All the print methods now take an argument that indicats how many lines
54 # to print. If the arg is a floating point number between 0 and 1.0, then
55 # it is taken as a decimal percentage of the availabel lines to be printed
56 # (e.g., .1 means print 10% of all available lines). If it is an integer,
57 # it is taken to mean the number of lines of data that you wish to have
60 # The sort_stats() method now processes some additionaly options (i.e., in
61 # addition to the old -1, 0, 1, or 2). It takes an arbitrary number of quoted
62 # strings to select the sort order. For example sort_stats('time', 'name')
63 # sorts on the major key of "internal function time", and on the minor
64 # key of 'the name of the function'. Look at the two tables in sort_stats()
65 # and get_sort_arg_defs(self) for more examples.
67 # All methods now return "self", so you can string together commands like:
68 # Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
69 # print_stats(5).print_callers(5)
71 #**************************************************************************
75 def __init__(self
, *args
):
82 apply(self
.add
, args
).ignore()
85 self
.all_callees
= None # calc only if needed
94 self
.sort_arg_dict
= {}
98 self
.get_top_level_stats()
102 print "Invalid timing data",
103 if self
.files
: print self
.files
[-1],
107 def load_stats(self
, arg
):
108 if not arg
: self
.stats
= {}
109 elif type(arg
) == type(""):
111 self
.stats
= marshal
.load(f
)
114 file_stats
= os
.stat(arg
)
115 arg
= time
.ctime(file_stats
[8]) + " " + arg
116 except: # in case this is not unix
119 elif hasattr(arg
, 'create_stats'):
121 self
.stats
= arg
.stats
124 raise TypeError, "Cannot create or construct a " \
126 + " object from '" + `arg`
+ "'"
129 def get_top_level_stats(self
):
130 for func
in self
.stats
.keys():
131 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
132 self
.total_calls
= self
.total_calls
+ nc
133 self
.prim_calls
= self
.prim_calls
+ cc
134 self
.total_tt
= self
.total_tt
+ tt
135 if callers
.has_key(("jprofile", 0, "profiler")):
136 self
.top_level
[func
] = None
137 if len(func_std_string(func
)) > self
.max_name_len
:
138 self
.max_name_len
= len(func_std_string(func
))
140 def add(self
, *arg_list
):
141 if not arg_list
: return self
142 if len(arg_list
) > 1: apply(self
.add
, arg_list
[1:])
144 if type(self
) != type(other
) or \
145 self
.__class
__ != other
.__class
__:
147 self
.files
= self
.files
+ other
.files
148 self
.total_calls
= self
.total_calls
+ other
.total_calls
149 self
.prim_calls
= self
.prim_calls
+ other
.prim_calls
150 self
.total_tt
= self
.total_tt
+ other
.total_tt
151 for func
in other
.top_level
.keys():
152 self
.top_level
[func
] = None
154 if self
.max_name_len
< other
.max_name_len
:
155 self
.max_name_len
= other
.max_name_len
159 for func
in other
.stats
.keys():
160 if self
.stats
.has_key(func
):
161 old_func_stat
= self
.stats
[func
]
163 old_func_stat
= (0, 0, 0, 0, {},)
164 self
.stats
[func
] = add_func_stats(old_func_stat
, \
170 # list the tuple indicies and directions for sorting,
171 # along with some printable description
172 sort_arg_dict_default
= {\
173 "calls" : (((1,-1), ), "call count"),\
174 "cumulative": (((3,-1), ), "cumulative time"),\
175 "file" : (((4, 1), ), "file name"),\
176 "line" : (((5, 1), ), "line number"),\
177 "module" : (((4, 1), ), "file name"),\
178 "name" : (((6, 1), ), "function name"),\
179 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"), \
180 "pcalls" : (((0,-1), ), "call count"),\
181 "stdname" : (((7, 1), ), "standard name"),\
182 "time" : (((2,-1), ), "internal time"),\
185 # Expand all abbreviations that are unique
186 def get_sort_arg_defs(self
):
187 if not self
.sort_arg_dict
:
188 self
.sort_arg_dict
= dict = {}
189 std_list
= dict.keys()
191 for word
in self
.sort_arg_dict_default
.keys():
196 if dict.has_key(fragment
):
197 bad_list
[fragment
] = 0
199 dict[fragment
] = self
. \
200 sort_arg_dict_default
[word
]
201 fragment
= fragment
[:-1]
202 for word
in bad_list
.keys():
204 return self
.sort_arg_dict
207 def sort_stats(self
, *field
):
211 if len(field
) == 1 and type(field
[0]) == type(1):
212 # Be compatible with old profiler
213 field
= [ {-1: "stdname", \
216 2: "cumulative" } [ field
[0] ] ]
218 sort_arg_defs
= self
.get_sort_arg_defs()
223 sort_tuple
= sort_tuple
+ sort_arg_defs
[word
][0]
224 self
.sort_type
= self
.sort_type
+ connector
+ \
225 sort_arg_defs
[word
][1]
229 for func
in self
.stats
.keys():
230 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
231 stats_list
.append((cc
, nc
, tt
, ct
) + func_split(func
) \
232 + (func_std_string(func
), func
,) )
234 stats_list
.sort(TupleComp(sort_tuple
).compare
)
236 self
.fcn_list
= fcn_list
= []
237 for tuple in stats_list
:
238 fcn_list
.append(tuple[-1])
242 def reverse_order(self
):
243 if self
.fcn_list
: self
.fcn_list
.reverse()
246 def strip_dirs(self
):
247 oldstats
= self
.stats
248 self
.stats
= newstats
= {}
250 for func
in oldstats
.keys():
251 cc
, nc
, tt
, ct
, callers
= oldstats
[func
]
252 newfunc
= func_strip_path(func
)
253 if len(func_std_string(newfunc
)) > max_name_len
:
254 max_name_len
= len(func_std_string(newfunc
))
256 for func2
in callers
.keys():
257 newcallers
[func_strip_path(func2
)] = \
260 if newstats
.has_key(newfunc
):
261 newstats
[newfunc
] = add_func_stats( \
263 (cc
, nc
, tt
, ct
, newcallers
))
265 newstats
[newfunc
] = (cc
, nc
, tt
, ct
, newcallers
)
266 old_top
= self
.top_level
267 self
.top_level
= new_top
= {}
268 for func
in old_top
.keys():
269 new_top
[func_strip_path(func
)] = None
271 self
.max_name_len
= max_name_len
274 self
.all_callees
= None
279 def calc_callees(self
):
280 if self
.all_callees
: return
281 self
.all_callees
= all_callees
= {}
282 for func
in self
.stats
.keys():
283 if not all_callees
.has_key(func
):
284 all_callees
[func
] = {}
285 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
286 for func2
in callers
.keys():
287 if not all_callees
.has_key(func2
):
288 all_callees
[func2
] = {}
289 all_callees
[func2
][func
] = callers
[func2
]
292 #******************************************************************
293 # The following functions support actual printing of reports
294 #******************************************************************
296 # Optional "amount" is either a line count, or a percentage of lines.
298 def eval_print_amount(self
, sel
, list, msg
):
300 if type(sel
) == type(""):
303 if 0<=regex
.search(sel
, func_std_string(func
)):
304 new_list
.append(func
)
307 if type(sel
) == type(1.0) and 0.0 <= sel
< 1.0:
308 count
= int (count
* sel
+ .5)
309 new_list
= list[:count
]
310 elif type(sel
) == type(1) and 0 <= sel
< count
:
312 new_list
= list[:count
]
313 if len(list) != len(new_list
):
314 msg
= msg
+ " List reduced from " + `
len(list)` \
315 + " to " + `
len(new_list
)`
+ \
316 " due to restriction <" + `sel`
+ ">\n"
322 def get_print_list(self
, sel_list
):
323 width
= self
.max_name_len
325 list = self
.fcn_list
[:]
326 msg
= " Ordered by: " + self
.sort_type
+ '\n'
328 list = self
.stats
.keys()
329 msg
= " Random listing order was used\n"
331 for selection
in sel_list
:
332 list,msg
= self
.eval_print_amount(selection
, list, msg
)
339 if count
< len(self
.stats
):
342 if len(func_std_string(func
)) > width
:
343 width
= len(func_std_string(func
))
346 def print_stats(self
, *amount
):
347 for filename
in self
.files
:
351 for func
in self
.top_level
.keys():
352 print indent
, func_get_function_name(func
)
354 print indent
, self
.total_calls
, "function calls",
355 if self
.total_calls
!= self
.prim_calls
:
356 print "(" + `self
.prim_calls`
, "primitive calls)",
357 print "in", fpformat
.fix(self
.total_tt
, 3), "CPU seconds"
359 width
, list = self
.get_print_list(amount
)
363 self
.print_line(func
)
369 def print_callees(self
, *amount
):
370 width
, list = self
.get_print_list(amount
)
374 self
.print_call_heading(width
, "called...")
376 if self
.all_callees
.has_key(func
):
377 self
.print_call_line(width
, \
378 func
, self
.all_callees
[func
])
380 self
.print_call_line(width
, func
, {})
385 def print_callers(self
, *amount
):
386 width
, list = self
.get_print_list(amount
)
388 self
.print_call_heading(width
, "was called by...")
390 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
391 self
.print_call_line(width
, func
, callers
)
396 def print_call_heading(self
, name_size
, column_title
):
397 print string
.ljust("Function ", name_size
) + column_title
400 def print_call_line(self
, name_size
, source
, call_dict
):
401 print string
.ljust(func_std_string(source
), name_size
),
405 clist
= call_dict
.keys()
407 name_size
= name_size
+ 1
410 name
= func_std_string(func
)
411 print indent
*name_size
+ name
+ '(' \
412 + `call_dict
[func
]`
+')', \
413 f8(self
.stats
[func
][3])
418 def print_title(self
):
419 print string
.rjust('ncalls', 9),
420 print string
.rjust('tottime', 8),
421 print string
.rjust('percall', 8),
422 print string
.rjust('cumtime', 8),
423 print string
.rjust('percall', 8),
424 print 'filename:lineno(function)'
427 def print_line(self
, func
): # hack : should print percentages
428 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
432 print string
.rjust(c
, 9),
443 print func_std_string(func
)
447 pass # has no return value, so use at end of line :-)
450 #**************************************************************************
451 # class TupleComp Documentation
452 #**************************************************************************
453 # This class provides a generic function for comparing any two tuples.
454 # Each instance records a list of tuple-indicies (from most significant
455 # to least significant), and sort direction (ascending or decending) for
456 # each tuple-index. The compare functions can then be used as the function
457 # argument to the system sort() function when a list of tuples need to be
458 # sorted in the instances order.
459 #**************************************************************************
461 def __init__(self
, comp_select_list
):
462 self
.comp_select_list
= comp_select_list
464 def compare (self
, left
, right
):
465 for index
, direction
in self
.comp_select_list
:
476 #**************************************************************************
478 def func_strip_path(func_name
):
479 file, line
, name
= func_name
480 return os
.path
.basename(file), line
, name
482 def func_get_function_name(func
):
485 def func_std_string(func_name
): # match what old profile produced
486 file, line
, name
= func_name
487 return file + ":" + `line`
+ "(" + name
+ ")"
489 def func_split(func_name
):
492 #**************************************************************************
493 # The following functions combine statists for pairs functions.
494 # The bulk of the processing involves correctly handling "call" lists,
495 # such as callers and callees.
496 #**************************************************************************
498 # Add together all the stats for two profile entries
499 def add_func_stats(target
, source
):
500 cc
, nc
, tt
, ct
, callers
= source
501 t_cc
, t_nc
, t_tt
, t_ct
, t_callers
= target
502 return (cc
+t_cc
, nc
+t_nc
, tt
+t_tt
, ct
+t_ct
, \
503 add_callers(t_callers
, callers
))
506 # Combine two caller lists in a single list.
507 def add_callers(target
, source
):
509 for func
in target
.keys():
510 new_callers
[func
] = target
[func
]
511 for func
in source
.keys():
512 if new_callers
.has_key(func
):
513 new_callers
[func
] = source
[func
] + new_callers
[func
]
515 new_callers
[func
] = source
[func
]
518 # Sum the caller statistics to get total number of calls recieved
519 def count_calls(callers
):
521 for func
in callers
.keys():
522 nc
= nc
+ callers
[func
]
525 #**************************************************************************
526 # The following functions support printing of reports
527 #**************************************************************************
530 return string
.rjust(fpformat
.fix(x
, 3), 8)