Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/gprof2dot.py: 15%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Copyright 2008-2023 Jose Fonseca
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17#
19"""Generate a dot graph from the output of several profilers."""
21__author__ = "Jose Fonseca et al"
24import sys
25import math
26import os.path
27import re
28import textwrap
29import argparse
30import xml.parsers.expat
31import collections
32import locale
33import json
34import fnmatch
35import codecs
36import io
37import hashlib
39assert sys.version_info[0] >= 3
42########################################################################
43# Model
46MULTIPLICATION_SIGN = chr(0xd7)
47timeFormat = "%.7g"
50def times(x):
51 return "%u%s" % (x, MULTIPLICATION_SIGN)
53def percentage(p):
54 return "%.02f%%" % (p*100.0,)
56def fmttime(t):
57 return timeFormat % t
59def add(a, b):
60 return a + b
62def fail(a, b):
63 assert False
65# To enhance readability, labels are rounded to the number of decimal
66# places corresponding to the tolerance value.
67def round_difference(difference, tolerance):
68 n = -math.floor(math.log10(tolerance))
69 return round(difference, n)
72def rescale_difference(x, min_val, max_val):
73 return (x - min_val) / (max_val - min_val)
76def min_max_difference(profile1, profile2):
77 f1_events = [f1[TOTAL_TIME_RATIO] for _, f1 in sorted_iteritems(profile1.functions)]
78 f2_events = [f2[TOTAL_TIME_RATIO] for _, f2 in sorted_iteritems(profile2.functions)]
79 differences = []
80 for i in range(len(f1_events)):
81 try:
82 differences.append(abs(f1_events[i] - f2_events[i]) * 100)
83 except IndexError:
84 differences.append(0)
86 return min(differences), max(differences)
89tol = 2 ** -23
91def ratio(numerator, denominator):
92 try:
93 ratio = float(numerator)/float(denominator)
94 except ZeroDivisionError:
95 # 0/0 is undefined, but 1.0 yields more useful results
96 return 1.0
97 if ratio < 0.0:
98 if ratio < -tol:
99 sys.stderr.write('warning: negative ratio (%s/%s)\n' % (numerator, denominator))
100 return 0.0
101 if ratio > 1.0:
102 if ratio > 1.0 + tol:
103 sys.stderr.write('warning: ratio greater than one (%s/%s)\n' % (numerator, denominator))
104 return 1.0
105 return ratio
108class UndefinedEvent(Exception):
109 """Raised when attempting to get an event which is undefined."""
111 def __init__(self, event):
112 Exception.__init__(self)
113 self.event = event
115 def __str__(self):
116 return 'unspecified event %s' % self.event.name
119class Event:
120 """Describe a kind of event, and its basic operations."""
122 def __init__(self, name, null, aggregator, formatter = str):
123 self.name = name
124 self._null = null
125 self._aggregator = aggregator
126 self._formatter = formatter
128 def __repr__(self):
129 return self.name
131 def null(self):
132 return self._null
134 def aggregate(self, val1, val2):
135 """Aggregate two event values."""
136 assert val1 is not None
137 assert val2 is not None
138 return self._aggregator(val1, val2)
140 def format(self, val):
141 """Format an event value."""
142 assert val is not None
143 return self._formatter(val)
146CALLS = Event("Calls", 0, add, times)
147SAMPLES = Event("Samples", 0, add, times)
148SAMPLES2 = Event("Samples", 0, add, times)
150# Count of samples where a given function was either executing or on the stack.
151# This is used to calculate the total time ratio according to the
152# straightforward method described in Mike Dunlavey's answer to
153# stackoverflow.com/questions/1777556/alternatives-to-gprof, item 4 (the myth
154# "that recursion is a tricky confusing issue"), last edited 2012-08-30: it's
155# just the ratio of TOTAL_SAMPLES over the number of samples in the profile.
156#
157# Used only when totalMethod == callstacks
158TOTAL_SAMPLES = Event("Samples", 0, add, times)
160TIME = Event("Time", 0.0, add, lambda x: '(' + fmttime(x) + ')')
161TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')')
162TOTAL_TIME = Event("Total time", 0.0, fail, fmttime)
163TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage)
165labels = {
166 'self-time': TIME,
167 'self-time-percentage': TIME_RATIO,
168 'total-time': TOTAL_TIME,
169 'total-time-percentage': TOTAL_TIME_RATIO,
170}
171defaultLabelNames = ['total-time-percentage', 'self-time-percentage']
173totalMethod = 'callratios'
176class Object:
177 """Base class for all objects in profile which can store events."""
179 def __init__(self, events=None):
180 if events is None:
181 self.events = {}
182 else:
183 self.events = events
185 def __lt__(self, other):
186 return id(self) < id(other)
188 def __contains__(self, event):
189 return event in self.events
191 def __getitem__(self, event):
192 try:
193 return self.events[event]
194 except KeyError:
195 raise UndefinedEvent(event)
197 def __setitem__(self, event, value):
198 if value is None:
199 if event in self.events:
200 del self.events[event]
201 else:
202 self.events[event] = value
205class Call(Object):
206 """A call between functions.
208 There should be at most one call object for every pair of functions.
209 """
211 def __init__(self, callee_id):
212 Object.__init__(self)
213 self.callee_id = callee_id
214 self.ratio = None
215 self.weight = None
218class Function(Object):
219 """A function."""
221 def __init__(self, id, name):
222 Object.__init__(self)
223 self.id = id
224 self.name = name
225 self.module = None
226 self.process = None
227 self.calls = {}
228 self.called = None
229 self.weight = None
230 self.cycle = None
231 self.filename = None
233 def add_call(self, call):
234 if call.callee_id in self.calls:
235 sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id)))
236 self.calls[call.callee_id] = call
238 def get_call(self, callee_id):
239 if not callee_id in self.calls:
240 call = Call(callee_id)
241 call[SAMPLES] = 0
242 call[SAMPLES2] = 0
243 call[CALLS] = 0
244 self.calls[callee_id] = call
245 return self.calls[callee_id]
247 _parenthesis_re = re.compile(r'\([^()]*\)')
248 _angles_re = re.compile(r'<[^<>]*>')
249 _const_re = re.compile(r'\s+const$')
251 def stripped_name(self):
252 """Remove extraneous information from C++ demangled function names."""
254 name = self.name
256 # Strip function parameters from name by recursively removing paired parenthesis
257 while True:
258 name, n = self._parenthesis_re.subn('', name)
259 if not n:
260 break
262 # Strip const qualifier
263 name = self._const_re.sub('', name)
265 # Strip template parameters from name by recursively removing paired angles
266 while True:
267 name, n = self._angles_re.subn('', name)
268 if not n:
269 break
271 return name
273 # TODO: write utility functions
275 def __repr__(self):
276 return self.name
278 def dump(self, sep1=",\n\t", sep2=":=", sep3="\n"):
279 """ Returns as a string all information available in this Function object
280 separators sep1:between entries
281 sep2:between attribute name and value,
282 sep3: inserted at end
283 """
284 return sep1.join(sep2.join([k,str(v)]) for (k,v) in sorted(self.__dict__.items())) + sep3
287class Cycle(Object):
288 """A cycle made from recursive function calls."""
290 def __init__(self):
291 Object.__init__(self)
292 self.functions = set()
294 def add_function(self, function):
295 assert function not in self.functions
296 self.functions.add(function)
297 if function.cycle is not None:
298 for other in function.cycle.functions:
299 if function not in self.functions:
300 self.add_function(other)
301 function.cycle = self
304class Profile(Object):
305 """The whole profile."""
307 def __init__(self):
308 Object.__init__(self)
309 self.functions = {}
310 self.cycles = []
312 def add_function(self, function):
313 if function.id in self.functions:
314 sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id)))
315 self.functions[function.id] = function
317 def add_cycle(self, cycle):
318 self.cycles.append(cycle)
320 def validate(self):
321 """Validate the edges."""
323 for function in self.functions.values():
324 for callee_id in list(function.calls.keys()):
325 assert function.calls[callee_id].callee_id == callee_id
326 if callee_id not in self.functions:
327 sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name))
328 del function.calls[callee_id]
330 def find_cycles(self):
331 """Find cycles using Tarjan's strongly connected components algorithm."""
333 # Apply the Tarjan's algorithm successively until all functions are visited
334 stack = []
335 data = {}
336 order = 0
337 for function in self.functions.values():
338 order = self._tarjan(function, order, stack, data)
339 cycles = []
340 for function in self.functions.values():
341 if function.cycle is not None and function.cycle not in cycles:
342 cycles.append(function.cycle)
343 self.cycles = cycles
344 if 0:
345 for cycle in cycles:
346 sys.stderr.write("Cycle:\n")
347 for member in cycle.functions:
348 sys.stderr.write("\tFunction %s\n" % member.name)
350 def prune_root(self, roots, depth=-1):
351 visited = set()
352 frontier = set([(root_node, depth) for root_node in roots])
353 while len(frontier) > 0:
354 node, node_depth = frontier.pop()
355 visited.add(node)
356 if node_depth == 0:
357 continue
358 f = self.functions[node]
359 newNodes = set(f.calls.keys()) - visited
360 frontier = frontier.union({(new_node, node_depth - 1) for new_node in newNodes})
361 subtreeFunctions = {}
362 for n in visited:
363 f = self.functions[n]
364 newCalls = {}
365 for c in f.calls.keys():
366 if c in visited:
367 newCalls[c] = f.calls[c]
368 f.calls = newCalls
369 subtreeFunctions[n] = f
370 self.functions = subtreeFunctions
372 def prune_leaf(self, leafs, depth=-1):
373 edgesUp = collections.defaultdict(set)
374 for f in self.functions.keys():
375 for n in self.functions[f].calls.keys():
376 edgesUp[n].add(f)
377 # build the tree up
378 visited = set()
379 frontier = set([(leaf_node, depth) for leaf_node in leafs])
380 while len(frontier) > 0:
381 node, node_depth = frontier.pop()
382 visited.add(node)
383 if node_depth == 0:
384 continue
385 newNodes = edgesUp[node] - visited
386 frontier = frontier.union({(new_node, node_depth - 1) for new_node in newNodes})
387 downTree = set(self.functions.keys())
388 upTree = visited
389 path = downTree.intersection(upTree)
390 pathFunctions = {}
391 for n in path:
392 f = self.functions[n]
393 newCalls = {}
394 for c in f.calls.keys():
395 if c in path:
396 newCalls[c] = f.calls[c]
397 f.calls = newCalls
398 pathFunctions[n] = f
399 self.functions = pathFunctions
401 def getFunctionIds(self, funcName):
402 function_names = {v.name: k for (k, v) in self.functions.items()}
403 return [function_names[name] for name in fnmatch.filter(function_names.keys(), funcName)]
405 def getFunctionId(self, funcName):
406 for f in self.functions:
407 if self.functions[f].name == funcName:
408 return f
409 return False
411 def printFunctionIds(self, selector=None, file=sys.stderr):
412 """ Print to file function entries selected by fnmatch.fnmatch like in
413 method getFunctionIds, with following extensions:
414 - selector starts with "%": dump all information available
415 - selector is '+' or '-': select all function entries
416 """
417 if selector is None or selector in ("+", "*"):
418 v = ",\n".join(("%s:\t%s" % (kf,self.functions[kf].name)
419 for kf in self.functions.keys()))
420 else:
421 if selector[0]=="%":
422 selector=selector[1:]
423 function_info={k:v for (k,v)
424 in self.functions.items()
425 if fnmatch.fnmatch(v.name,selector)}
426 v = ",\n".join( ("%s\t({k})\t(%s)::\n\t%s" % (v.name,type(v),v.dump())
427 for (k,v) in function_info.items()
428 ))
430 else:
431 function_names = (v.name for v in self.functions.values())
432 v = ",\n".join( ( nm for nm in fnmatch.filter(function_names,selector )))
434 file.write(v+"\n")
435 file.flush()
437 class _TarjanData:
438 def __init__(self, order):
439 self.order = order
440 self.lowlink = order
441 self.onstack = False
443 def _tarjan(self, function, order, stack, data):
444 """Tarjan's strongly connected components algorithm.
446 See also:
447 - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
448 """
450 try:
451 func_data = data[function.id]
452 return order
453 except KeyError:
454 func_data = self._TarjanData(order)
455 data[function.id] = func_data
456 order += 1
457 pos = len(stack)
458 stack.append(function)
459 func_data.onstack = True
460 for call in function.calls.values():
461 try:
462 callee_data = data[call.callee_id]
463 if callee_data.onstack:
464 func_data.lowlink = min(func_data.lowlink, callee_data.order)
465 except KeyError:
466 callee = self.functions[call.callee_id]
467 order = self._tarjan(callee, order, stack, data)
468 callee_data = data[call.callee_id]
469 func_data.lowlink = min(func_data.lowlink, callee_data.lowlink)
470 if func_data.lowlink == func_data.order:
471 # Strongly connected component found
472 members = stack[pos:]
473 del stack[pos:]
474 if len(members) > 1:
475 cycle = Cycle()
476 for member in members:
477 cycle.add_function(member)
478 data[member.id].onstack = False
479 else:
480 for member in members:
481 data[member.id].onstack = False
482 return order
484 def call_ratios(self, event):
485 # Aggregate for incoming calls
486 cycle_totals = {}
487 for cycle in self.cycles:
488 cycle_totals[cycle] = 0.0
489 function_totals = {}
490 for function in self.functions.values():
491 function_totals[function] = 0.0
493 # Pass 1: function_total gets the sum of call[event] for all
494 # incoming arrows. Same for cycle_total for all arrows
495 # that are coming into the *cycle* but are not part of it.
496 for function in self.functions.values():
497 for call in function.calls.values():
498 if call.callee_id != function.id:
499 callee = self.functions[call.callee_id]
500 if event in call.events:
501 function_totals[callee] += call[event]
502 if callee.cycle is not None and callee.cycle is not function.cycle:
503 cycle_totals[callee.cycle] += call[event]
504 else:
505 sys.stderr.write("call_ratios: No data for " + function.name + " call to " + callee.name + "\n")
507 # Pass 2: Compute the ratios. Each call[event] is scaled by the
508 # function_total of the callee. Calls into cycles use the
509 # cycle_total, but not calls within cycles.
510 for function in self.functions.values():
511 for call in function.calls.values():
512 assert call.ratio is None
513 if call.callee_id != function.id:
514 callee = self.functions[call.callee_id]
515 if event in call.events:
516 if callee.cycle is not None and callee.cycle is not function.cycle:
517 total = cycle_totals[callee.cycle]
518 else:
519 total = function_totals[callee]
520 call.ratio = ratio(call[event], total)
521 else:
522 # Warnings here would only repeat those issued above.
523 call.ratio = 0.0
525 def integrate(self, outevent, inevent):
526 """Propagate function time ratio along the function calls.
528 Must be called after finding the cycles.
530 See also:
531 - http://citeseer.ist.psu.edu/graham82gprof.html
532 """
534 # Sanity checking
535 assert outevent not in self
536 for function in self.functions.values():
537 assert outevent not in function
538 assert inevent in function
539 for call in function.calls.values():
540 assert outevent not in call
541 if call.callee_id != function.id:
542 assert call.ratio is not None
544 # Aggregate the input for each cycle
545 for cycle in self.cycles:
546 total = inevent.null()
547 for function in self.functions.values():
548 total = inevent.aggregate(total, function[inevent])
549 self[inevent] = total
551 # Integrate along the edges
552 total = inevent.null()
553 for function in self.functions.values():
554 total = inevent.aggregate(total, function[inevent])
555 self._integrate_function(function, outevent, inevent)
556 self[outevent] = total
558 def _integrate_function(self, function, outevent, inevent):
559 if function.cycle is not None:
560 return self._integrate_cycle(function.cycle, outevent, inevent)
561 else:
562 if outevent not in function:
563 total = function[inevent]
564 for call in function.calls.values():
565 if call.callee_id != function.id:
566 total += self._integrate_call(call, outevent, inevent)
567 function[outevent] = total
568 return function[outevent]
570 def _integrate_call(self, call, outevent, inevent):
571 assert outevent not in call
572 assert call.ratio is not None
573 callee = self.functions[call.callee_id]
574 subtotal = call.ratio *self._integrate_function(callee, outevent, inevent)
575 call[outevent] = subtotal
576 return subtotal
578 def _integrate_cycle(self, cycle, outevent, inevent):
579 if outevent not in cycle:
581 # Compute the outevent for the whole cycle
582 total = inevent.null()
583 for member in cycle.functions:
584 subtotal = member[inevent]
585 for call in member.calls.values():
586 callee = self.functions[call.callee_id]
587 if callee.cycle is not cycle:
588 subtotal += self._integrate_call(call, outevent, inevent)
589 total += subtotal
590 cycle[outevent] = total
592 # Compute the time propagated to callers of this cycle
593 callees = {}
594 for function in self.functions.values():
595 if function.cycle is not cycle:
596 for call in function.calls.values():
597 callee = self.functions[call.callee_id]
598 if callee.cycle is cycle:
599 try:
600 callees[callee] += call.ratio
601 except KeyError:
602 callees[callee] = call.ratio
604 for member in cycle.functions:
605 member[outevent] = outevent.null()
607 for callee, call_ratio in callees.items():
608 ranks = {}
609 call_ratios = {}
610 partials = {}
611 self._rank_cycle_function(cycle, callee, ranks)
612 self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set())
613 partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent)
615 # Ensure `partial == max(partials.values())`, but with round-off tolerance
616 max_partial = max(partials.values())
617 assert abs(partial - max_partial) <= 1e-7*max_partial
619 assert abs(call_ratio*total - partial) <= 0.001*call_ratio*total
621 return cycle[outevent]
623 def _rank_cycle_function(self, cycle, function, ranks):
624 """Dijkstra's shortest paths algorithm.
626 See also:
627 - http://en.wikipedia.org/wiki/Dijkstra's_algorithm
628 """
630 import heapq
631 Q = []
632 Qd = {}
633 p = {}
634 visited = set([function])
636 ranks[function] = 0
637 for call in function.calls.values():
638 if call.callee_id != function.id:
639 callee = self.functions[call.callee_id]
640 if callee.cycle is cycle:
641 ranks[callee] = 1
642 item = [ranks[callee], function, callee]
643 heapq.heappush(Q, item)
644 Qd[callee] = item
646 while Q:
647 cost, parent, member = heapq.heappop(Q)
648 if member not in visited:
649 p[member]= parent
650 visited.add(member)
651 for call in member.calls.values():
652 if call.callee_id != member.id:
653 callee = self.functions[call.callee_id]
654 if callee.cycle is cycle:
655 member_rank = ranks[member]
656 rank = ranks.get(callee)
657 if rank is not None:
658 if rank > 1 + member_rank:
659 rank = 1 + member_rank
660 ranks[callee] = rank
661 Qd_callee = Qd[callee]
662 Qd_callee[0] = rank
663 Qd_callee[1] = member
664 heapq._siftdown(Q, 0, Q.index(Qd_callee))
665 else:
666 rank = 1 + member_rank
667 ranks[callee] = rank
668 item = [rank, member, callee]
669 heapq.heappush(Q, item)
670 Qd[callee] = item
672 def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited):
673 if function not in visited:
674 visited.add(function)
675 for call in function.calls.values():
676 if call.callee_id != function.id:
677 callee = self.functions[call.callee_id]
678 if callee.cycle is cycle:
679 if ranks[callee] > ranks[function]:
680 call_ratios[callee] = call_ratios.get(callee, 0.0) + call.ratio
681 self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited)
683 def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent):
684 if function not in partials:
685 partial = partial_ratio*function[inevent]
686 for call in function.calls.values():
687 if call.callee_id != function.id:
688 callee = self.functions[call.callee_id]
689 if callee.cycle is not cycle:
690 assert outevent in call
691 partial += partial_ratio*call[outevent]
692 else:
693 if ranks[callee] > ranks[function]:
694 callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent)
695 call_ratio = ratio(call.ratio, call_ratios[callee])
696 call_partial = call_ratio*callee_partial
697 try:
698 call[outevent] += call_partial
699 except UndefinedEvent:
700 call[outevent] = call_partial
701 partial += call_partial
702 partials[function] = partial
703 try:
704 function[outevent] += partial
705 except UndefinedEvent:
706 function[outevent] = partial
707 return partials[function]
709 def aggregate(self, event):
710 """Aggregate an event for the whole profile."""
712 total = event.null()
713 for function in self.functions.values():
714 try:
715 total = event.aggregate(total, function[event])
716 except UndefinedEvent:
717 return
718 self[event] = total
720 def ratio(self, outevent, inevent):
721 assert outevent not in self
722 assert inevent in self
723 for function in self.functions.values():
724 assert outevent not in function
725 assert inevent in function
726 function[outevent] = ratio(function[inevent], self[inevent])
727 for call in function.calls.values():
728 assert outevent not in call
729 if inevent in call:
730 call[outevent] = ratio(call[inevent], self[inevent])
731 self[outevent] = 1.0
733 def prune(self, node_thres, edge_thres, paths, color_nodes_by_selftime):
734 """Prune the profile"""
736 # compute the prune ratios
737 for function in self.functions.values():
738 try:
739 function.weight = function[TOTAL_TIME_RATIO]
740 except UndefinedEvent:
741 pass
743 for call in function.calls.values():
744 callee = self.functions[call.callee_id]
746 if TOTAL_TIME_RATIO in call:
747 # handle exact cases first
748 call.weight = call[TOTAL_TIME_RATIO]
749 else:
750 try:
751 # make a safe estimate
752 call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO])
753 except UndefinedEvent:
754 pass
756 # prune the nodes
757 for function_id in list(self.functions.keys()):
758 function = self.functions[function_id]
759 if function.weight is not None:
760 if function.weight < node_thres:
761 del self.functions[function_id]
763 # prune file paths
764 for function_id in list(self.functions.keys()):
765 function = self.functions[function_id]
766 if paths and function.filename and not any(function.filename.startswith(path) for path in paths):
767 del self.functions[function_id]
768 elif paths and function.module and not any((function.module.find(path)>-1) for path in paths):
769 del self.functions[function_id]
771 # prune the edges
772 for function in self.functions.values():
773 for callee_id in list(function.calls.keys()):
774 call = function.calls[callee_id]
775 if callee_id not in self.functions or call.weight is not None and call.weight < edge_thres:
776 del function.calls[callee_id]
778 if color_nodes_by_selftime:
779 weights = []
780 for function in self.functions.values():
781 try:
782 weights.append(function[TIME_RATIO])
783 except UndefinedEvent:
784 pass
785 max_ratio = max(weights or [1])
787 # apply rescaled weights for coloriung
788 for function in self.functions.values():
789 try:
790 function.weight = function[TIME_RATIO] / max_ratio
791 except (ZeroDivisionError, UndefinedEvent):
792 pass
794 def dump(self):
795 for function in self.functions.values():
796 sys.stderr.write('Function %s:\n' % (function.name,))
797 self._dump_events(function.events)
798 for call in function.calls.values():
799 callee = self.functions[call.callee_id]
800 sys.stderr.write(' Call %s:\n' % (callee.name,))
801 self._dump_events(call.events)
802 for cycle in self.cycles:
803 sys.stderr.write('Cycle:\n')
804 self._dump_events(cycle.events)
805 for function in cycle.functions:
806 sys.stderr.write(' Function %s\n' % (function.name,))
808 def _dump_events(self, events):
809 for event, value in events.items():
810 sys.stderr.write(' %s: %s\n' % (event.name, event.format(value)))
814########################################################################
815# Parsers
818class Struct:
819 """Masquerade a dictionary with a structure-like behavior."""
821 def __init__(self, attrs = None):
822 if attrs is None:
823 attrs = {}
824 self.__dict__['_attrs'] = attrs
826 def __getattr__(self, name):
827 try:
828 return self._attrs[name]
829 except KeyError:
830 raise AttributeError(name)
832 def __setattr__(self, name, value):
833 self._attrs[name] = value
835 def __str__(self):
836 return str(self._attrs)
838 def __repr__(self):
839 return repr(self._attrs)
842class ParseError(Exception):
843 """Raised when parsing to signal mismatches."""
845 def __init__(self, msg, line):
846 Exception.__init__(self)
847 self.msg = msg
848 # TODO: store more source line information
849 self.line = line
851 def __str__(self):
852 return '%s: %r' % (self.msg, self.line)
855class Parser:
856 """Parser interface."""
858 stdinInput = True
859 multipleInput = False
861 def __init__(self):
862 pass
864 def parse(self):
865 raise NotImplementedError
868class JsonParser(Parser):
869 """Parser for a custom JSON representation of profile data.
871 See schema.json for details.
872 """
875 def __init__(self, stream):
876 Parser.__init__(self)
877 self.stream = stream
879 def parse(self):
881 obj = json.load(self.stream)
883 assert obj['version'] == 0
885 profile = Profile()
886 profile[SAMPLES] = 0
888 fns = obj['functions']
890 for functionIndex in range(len(fns)):
891 fn = fns[functionIndex]
892 function = Function(functionIndex, fn['name'])
893 try:
894 function.module = fn['module']
895 except KeyError:
896 pass
897 try:
898 function.process = fn['process']
899 except KeyError:
900 pass
901 function[SAMPLES] = 0
902 function.called = 0
903 profile.add_function(function)
905 for event in obj['events']:
906 callchain = []
908 for functionIndex in event['callchain']:
909 function = profile.functions[functionIndex]
910 callchain.append(function)
912 # increment the call count of the first in the callchain
913 function = profile.functions[event['callchain'][0]]
914 function.called = function.called + 1
916 cost = event['cost'][0]
918 callee = callchain[0]
919 callee[SAMPLES] += cost
920 profile[SAMPLES] += cost
922 for caller in callchain[1:]:
923 try:
924 call = caller.calls[callee.id]
925 except KeyError:
926 call = Call(callee.id)
927 call[SAMPLES2] = cost
928 caller.add_call(call)
929 else:
930 call[SAMPLES2] += cost
932 callee = caller
934 if False:
935 profile.dump()
937 # compute derived data
938 profile.validate()
939 profile.find_cycles()
940 profile.ratio(TIME_RATIO, SAMPLES)
941 profile.call_ratios(SAMPLES2)
942 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
944 return profile
947class LineParser(Parser):
948 """Base class for parsers that read line-based formats."""
950 def __init__(self, stream):
951 Parser.__init__(self)
952 self._stream = stream
953 self.__line = None
954 self.__eof = False
955 self.line_no = 0
957 def readline(self):
958 line = self._stream.readline()
959 if not line:
960 self.__line = ''
961 self.__eof = True
962 else:
963 self.line_no += 1
964 line = line.rstrip('\r\n')
965 self.__line = line
967 def lookahead(self):
968 assert self.__line is not None
969 return self.__line
971 def consume(self):
972 assert self.__line is not None
973 line = self.__line
974 self.readline()
975 return line
977 def eof(self):
978 assert self.__line is not None
979 return self.__eof
982XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF = range(4)
985class XmlToken:
987 def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
988 assert type in (XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF)
989 self.type = type
990 self.name_or_data = name_or_data
991 self.attrs = attrs
992 self.line = line
993 self.column = column
995 def __str__(self):
996 if self.type == XML_ELEMENT_START:
997 return '<' + self.name_or_data + ' ...>'
998 if self.type == XML_ELEMENT_END:
999 return '</' + self.name_or_data + '>'
1000 if self.type == XML_CHARACTER_DATA:
1001 return self.name_or_data
1002 if self.type == XML_EOF:
1003 return 'end of file'
1004 assert 0
1007class XmlTokenizer:
1008 """Expat based XML tokenizer."""
1010 def __init__(self, fp, skip_ws = True):
1011 self.fp = fp
1012 self.tokens = []
1013 self.index = 0
1014 self.final = False
1015 self.skip_ws = skip_ws
1017 self.character_pos = 0, 0
1018 self.character_data = ''
1020 self.parser = xml.parsers.expat.ParserCreate()
1021 self.parser.StartElementHandler = self.handle_element_start
1022 self.parser.EndElementHandler = self.handle_element_end
1023 self.parser.CharacterDataHandler = self.handle_character_data
1025 def handle_element_start(self, name, attributes):
1026 self.finish_character_data()
1027 line, column = self.pos()
1028 token = XmlToken(XML_ELEMENT_START, name, attributes, line, column)
1029 self.tokens.append(token)
1031 def handle_element_end(self, name):
1032 self.finish_character_data()
1033 line, column = self.pos()
1034 token = XmlToken(XML_ELEMENT_END, name, None, line, column)
1035 self.tokens.append(token)
1037 def handle_character_data(self, data):
1038 if not self.character_data:
1039 self.character_pos = self.pos()
1040 self.character_data += data
1042 def finish_character_data(self):
1043 if self.character_data:
1044 if not self.skip_ws or not self.character_data.isspace():
1045 line, column = self.character_pos
1046 token = XmlToken(XML_CHARACTER_DATA, self.character_data, None, line, column)
1047 self.tokens.append(token)
1048 self.character_data = ''
1050 def next(self):
1051 size = 16*1024
1052 while self.index >= len(self.tokens) and not self.final:
1053 self.tokens = []
1054 self.index = 0
1055 data = self.fp.read(size)
1056 self.final = len(data) < size
1057 self.parser.Parse(data, self.final)
1058 if self.index >= len(self.tokens):
1059 line, column = self.pos()
1060 token = XmlToken(XML_EOF, None, None, line, column)
1061 else:
1062 token = self.tokens[self.index]
1063 self.index += 1
1064 return token
1066 def pos(self):
1067 return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
1070class XmlTokenMismatch(Exception):
1072 def __init__(self, expected, found):
1073 Exception.__init__(self)
1074 self.expected = expected
1075 self.found = found
1077 def __str__(self):
1078 return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
1081class XmlParser(Parser):
1082 """Base XML document parser."""
1084 def __init__(self, fp):
1085 Parser.__init__(self)
1086 self.tokenizer = XmlTokenizer(fp)
1087 self.consume()
1089 def consume(self):
1090 self.token = self.tokenizer.next()
1092 def match_element_start(self, name):
1093 return self.token.type == XML_ELEMENT_START and self.token.name_or_data == name
1095 def match_element_end(self, name):
1096 return self.token.type == XML_ELEMENT_END and self.token.name_or_data == name
1098 def element_start(self, name):
1099 while self.token.type == XML_CHARACTER_DATA:
1100 self.consume()
1101 if self.token.type != XML_ELEMENT_START:
1102 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
1103 if self.token.name_or_data != name:
1104 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
1105 attrs = self.token.attrs
1106 self.consume()
1107 return attrs
1109 def element_end(self, name):
1110 while self.token.type == XML_CHARACTER_DATA:
1111 self.consume()
1112 if self.token.type != XML_ELEMENT_END:
1113 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
1114 if self.token.name_or_data != name:
1115 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
1116 self.consume()
1118 def character_data(self, strip = True):
1119 data = ''
1120 while self.token.type == XML_CHARACTER_DATA:
1121 data += self.token.name_or_data
1122 self.consume()
1123 if strip:
1124 data = data.strip()
1125 return data
1128class GprofParser(Parser):
1129 """Parser for GNU gprof output.
1131 See also:
1132 - Chapter "Interpreting gprof's Output" from the GNU gprof manual
1133 http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph
1134 - File "cg_print.c" from the GNU gprof source code
1135 http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src
1136 """
1138 def __init__(self, fp):
1139 Parser.__init__(self)
1140 self.fp = fp
1141 self.functions = {}
1142 self.cycles = {}
1144 def readline(self):
1145 line = self.fp.readline()
1146 if not line:
1147 sys.stderr.write('error: unexpected end of file\n')
1148 sys.exit(1)
1149 line = line.rstrip('\r\n')
1150 return line
1152 _int_re = re.compile(r'^\d+$')
1153 _float_re = re.compile(r'^\d+\.\d+$')
1155 def translate(self, mo):
1156 """Extract a structure from a match object, while translating the types in the process."""
1157 attrs = {}
1158 groupdict = mo.groupdict()
1159 for name, value in groupdict.items():
1160 if value is None:
1161 value = None
1162 elif self._int_re.match(value):
1163 value = int(value)
1164 elif self._float_re.match(value):
1165 value = float(value)
1166 attrs[name] = (value)
1167 return Struct(attrs)
1169 _cg_header_re = re.compile(
1170 # original gprof header
1171 r'^\s+called/total\s+parents\s*$|' +
1172 r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' +
1173 r'^\s+called/total\s+children\s*$|' +
1174 # GNU gprof header
1175 r'^index\s+%\s+(time\s+)?self\s+children\s+called\s+name\s*$'
1176 )
1178 _cg_ignore_re = re.compile(
1179 # spontaneous
1180 r'^\s+<spontaneous>\s*$|'
1181 # internal calls (such as "mcount")
1182 r'^.*\((\d+)\)$'
1183 )
1185 _cg_primary_re = re.compile(
1186 r'^\[(?P<index>\d+)\]?' +
1187 r'\s+(?P<percentage_time>\d+\.\d+)' +
1188 r'\s+(?P<self>\d+\.\d+)' +
1189 r'\s+(?P<descendants>\d+\.\d+)' +
1190 r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
1191 r'\s+(?P<name>\S.*?)' +
1192 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1193 r'\s\[(\d+)\]$'
1194 )
1196 _cg_parent_re = re.compile(
1197 r'^\s+(?P<self>\d+\.\d+)?' +
1198 r'\s+(?P<descendants>\d+\.\d+)?' +
1199 r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' +
1200 r'\s+(?P<name>\S.*?)' +
1201 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1202 r'\s\[(?P<index>\d+)\]$'
1203 )
1205 _cg_child_re = _cg_parent_re
1207 _cg_cycle_header_re = re.compile(
1208 r'^\[(?P<index>\d+)\]?' +
1209 r'\s+(?P<percentage_time>\d+\.\d+)' +
1210 r'\s+(?P<self>\d+\.\d+)' +
1211 r'\s+(?P<descendants>\d+\.\d+)' +
1212 r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
1213 r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' +
1214 r'\s\[(\d+)\]$'
1215 )
1217 _cg_cycle_member_re = re.compile(
1218 r'^\s+(?P<self>\d+\.\d+)?' +
1219 r'\s+(?P<descendants>\d+\.\d+)?' +
1220 r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' +
1221 r'\s+(?P<name>\S.*?)' +
1222 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1223 r'\s\[(?P<index>\d+)\]$'
1224 )
1226 _cg_sep_re = re.compile(r'^--+$')
1228 def parse_function_entry(self, lines):
1229 parents = []
1230 children = []
1232 while True:
1233 if not lines:
1234 sys.stderr.write('warning: unexpected end of entry\n')
1235 line = lines.pop(0)
1236 if line.startswith('['):
1237 break
1239 # read function parent line
1240 mo = self._cg_parent_re.match(line)
1241 if not mo:
1242 if self._cg_ignore_re.match(line):
1243 continue
1244 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
1245 else:
1246 parent = self.translate(mo)
1247 parents.append(parent)
1249 # read primary line
1250 mo = self._cg_primary_re.match(line)
1251 if not mo:
1252 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
1253 return
1254 else:
1255 function = self.translate(mo)
1257 while lines:
1258 line = lines.pop(0)
1260 # read function subroutine line
1261 mo = self._cg_child_re.match(line)
1262 if not mo:
1263 if self._cg_ignore_re.match(line):
1264 continue
1265 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
1266 else:
1267 child = self.translate(mo)
1268 children.append(child)
1270 function.parents = parents
1271 function.children = children
1273 self.functions[function.index] = function
1275 def parse_cycle_entry(self, lines):
1277 # read cycle header line
1278 line = lines[0]
1279 mo = self._cg_cycle_header_re.match(line)
1280 if not mo:
1281 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
1282 return
1283 cycle = self.translate(mo)
1285 # read cycle member lines
1286 cycle.functions = []
1287 for line in lines[1:]:
1288 mo = self._cg_cycle_member_re.match(line)
1289 if not mo:
1290 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
1291 continue
1292 call = self.translate(mo)
1293 cycle.functions.append(call)
1295 self.cycles[cycle.cycle] = cycle
1297 def parse_cg_entry(self, lines):
1298 if lines[0].startswith("["):
1299 self.parse_cycle_entry(lines)
1300 else:
1301 self.parse_function_entry(lines)
1303 def parse_cg(self):
1304 """Parse the call graph."""
1306 # skip call graph header
1307 while not self._cg_header_re.match(self.readline()):
1308 pass
1309 line = self.readline()
1310 while self._cg_header_re.match(line):
1311 line = self.readline()
1313 # process call graph entries
1314 entry_lines = []
1315 while line != '\014': # form feed
1316 if line and not line.isspace():
1317 if self._cg_sep_re.match(line):
1318 self.parse_cg_entry(entry_lines)
1319 entry_lines = []
1320 else:
1321 entry_lines.append(line)
1322 line = self.readline()
1324 def parse(self):
1325 self.parse_cg()
1326 self.fp.close()
1328 profile = Profile()
1329 profile[TIME] = 0.0
1331 cycles = {}
1332 for index in self.cycles:
1333 cycles[index] = Cycle()
1335 for entry in self.functions.values():
1336 # populate the function
1337 function = Function(entry.index, entry.name)
1338 function[TIME] = entry.self
1339 if entry.called is not None:
1340 function.called = entry.called
1341 if entry.called_self is not None:
1342 call = Call(entry.index)
1343 call[CALLS] = entry.called_self
1344 function.called += entry.called_self
1346 # populate the function calls
1347 for child in entry.children:
1348 call = Call(child.index)
1350 assert child.called is not None
1351 call[CALLS] = child.called
1353 if child.index not in self.functions:
1354 # NOTE: functions that were never called but were discovered by gprof's
1355 # static call graph analysis dont have a call graph entry so we need
1356 # to add them here
1357 missing = Function(child.index, child.name)
1358 function[TIME] = 0.0
1359 function.called = 0
1360 profile.add_function(missing)
1362 function.add_call(call)
1364 profile.add_function(function)
1366 if entry.cycle is not None:
1367 try:
1368 cycle = cycles[entry.cycle]
1369 except KeyError:
1370 sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle)
1371 cycle = Cycle()
1372 cycles[entry.cycle] = cycle
1373 cycle.add_function(function)
1375 profile[TIME] = profile[TIME] + function[TIME]
1377 for cycle in cycles.values():
1378 profile.add_cycle(cycle)
1380 # Compute derived events
1381 profile.validate()
1382 profile.ratio(TIME_RATIO, TIME)
1383 profile.call_ratios(CALLS)
1384 profile.integrate(TOTAL_TIME, TIME)
1385 profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
1387 return profile
1390# Clone&hack of GprofParser for VTune Amplifier XE 2013 gprof-cc output.
1391# Tested only with AXE 2013 for Windows.
1392# - Use total times as reported by AXE.
1393# - In the absence of call counts, call ratios are faked from the relative
1394# proportions of total time. This affects only the weighting of the calls.
1395# - Different header, separator, and end marker.
1396# - Extra whitespace after function names.
1397# - You get a full entry for <spontaneous>, which does not have parents.
1398# - Cycles do have parents. These are saved but unused (as they are
1399# for functions).
1400# - Disambiguated "unrecognized call graph entry" error messages.
1401# Notes:
1402# - Total time of functions as reported by AXE passes the val3 test.
1403# - CPU Time:Children in the input is sometimes a negative number. This
1404# value goes to the variable descendants, which is unused.
1405# - The format of gprof-cc reports is unaffected by the use of
1406# -knob enable-call-counts=true (no call counts, ever), or
1407# -show-as=samples (results are quoted in seconds regardless).
1408class AXEParser(Parser):
1409 "Parser for VTune Amplifier XE 2013 gprof-cc report output."
1411 def __init__(self, fp):
1412 Parser.__init__(self)
1413 self.fp = fp
1414 self.functions = {}
1415 self.cycles = {}
1417 def readline(self):
1418 line = self.fp.readline()
1419 if not line:
1420 sys.stderr.write('error: unexpected end of file\n')
1421 sys.exit(1)
1422 line = line.rstrip('\r\n')
1423 return line
1425 _int_re = re.compile(r'^\d+$')
1426 _float_re = re.compile(r'^\d+\.\d+$')
1428 def translate(self, mo):
1429 """Extract a structure from a match object, while translating the types in the process."""
1430 attrs = {}
1431 groupdict = mo.groupdict()
1432 for name, value in groupdict.items():
1433 if value is None:
1434 value = None
1435 elif self._int_re.match(value):
1436 value = int(value)
1437 elif self._float_re.match(value):
1438 value = float(value)
1439 attrs[name] = (value)
1440 return Struct(attrs)
1442 _cg_header_re = re.compile(
1443 '^Index |'
1444 '^-----+ '
1445 )
1447 _cg_footer_re = re.compile(r'^Index\s+Function\s*$')
1449 _cg_primary_re = re.compile(
1450 r'^\[(?P<index>\d+)\]?' +
1451 r'\s+(?P<percentage_time>\d+\.\d+)' +
1452 r'\s+(?P<self>\d+\.\d+)' +
1453 r'\s+(?P<descendants>\d+\.\d+)' +
1454 r'\s+(?P<name>\S.*?)' +
1455 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1456 r'\s+\[(\d+)\]' +
1457 r'\s*$'
1458 )
1460 _cg_parent_re = re.compile(
1461 r'^\s+(?P<self>\d+\.\d+)?' +
1462 r'\s+(?P<descendants>\d+\.\d+)?' +
1463 r'\s+(?P<name>\S.*?)' +
1464 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1465 r'(?:\s+\[(?P<index>\d+)\]\s*)?' +
1466 r'\s*$'
1467 )
1469 _cg_child_re = _cg_parent_re
1471 _cg_cycle_header_re = re.compile(
1472 r'^\[(?P<index>\d+)\]?' +
1473 r'\s+(?P<percentage_time>\d+\.\d+)' +
1474 r'\s+(?P<self>\d+\.\d+)' +
1475 r'\s+(?P<descendants>\d+\.\d+)' +
1476 r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' +
1477 r'\s+\[(\d+)\]' +
1478 r'\s*$'
1479 )
1481 _cg_cycle_member_re = re.compile(
1482 r'^\s+(?P<self>\d+\.\d+)?' +
1483 r'\s+(?P<descendants>\d+\.\d+)?' +
1484 r'\s+(?P<name>\S.*?)' +
1485 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
1486 r'\s+\[(?P<index>\d+)\]' +
1487 r'\s*$'
1488 )
1490 def parse_function_entry(self, lines):
1491 parents = []
1492 children = []
1494 while True:
1495 if not lines:
1496 sys.stderr.write('warning: unexpected end of entry\n')
1497 return
1498 line = lines.pop(0)
1499 if line.startswith('['):
1500 break
1502 # read function parent line
1503 mo = self._cg_parent_re.match(line)
1504 if not mo:
1505 sys.stderr.write('warning: unrecognized call graph entry (1): %r\n' % line)
1506 else:
1507 parent = self.translate(mo)
1508 if parent.name != '<spontaneous>':
1509 parents.append(parent)
1511 # read primary line
1512 mo = self._cg_primary_re.match(line)
1513 if not mo:
1514 sys.stderr.write('warning: unrecognized call graph entry (2): %r\n' % line)
1515 return
1516 else:
1517 function = self.translate(mo)
1519 while lines:
1520 line = lines.pop(0)
1522 # read function subroutine line
1523 mo = self._cg_child_re.match(line)
1524 if not mo:
1525 sys.stderr.write('warning: unrecognized call graph entry (3): %r\n' % line)
1526 else:
1527 child = self.translate(mo)
1528 if child.name != '<spontaneous>':
1529 children.append(child)
1531 if function.name != '<spontaneous>':
1532 function.parents = parents
1533 function.children = children
1535 self.functions[function.index] = function
1537 def parse_cycle_entry(self, lines):
1539 # Process the parents that were not there in gprof format.
1540 parents = []
1541 while True:
1542 if not lines:
1543 sys.stderr.write('warning: unexpected end of cycle entry\n')
1544 return
1545 line = lines.pop(0)
1546 if line.startswith('['):
1547 break
1548 mo = self._cg_parent_re.match(line)
1549 if not mo:
1550 sys.stderr.write('warning: unrecognized call graph entry (6): %r\n' % line)
1551 else:
1552 parent = self.translate(mo)
1553 if parent.name != '<spontaneous>':
1554 parents.append(parent)
1556 # read cycle header line
1557 mo = self._cg_cycle_header_re.match(line)
1558 if not mo:
1559 sys.stderr.write('warning: unrecognized call graph entry (4): %r\n' % line)
1560 return
1561 cycle = self.translate(mo)
1563 # read cycle member lines
1564 cycle.functions = []
1565 for line in lines[1:]:
1566 mo = self._cg_cycle_member_re.match(line)
1567 if not mo:
1568 sys.stderr.write('warning: unrecognized call graph entry (5): %r\n' % line)
1569 continue
1570 call = self.translate(mo)
1571 cycle.functions.append(call)
1573 cycle.parents = parents
1574 self.cycles[cycle.cycle] = cycle
1576 def parse_cg_entry(self, lines):
1577 if any("as a whole" in linelooper for linelooper in lines):
1578 self.parse_cycle_entry(lines)
1579 else:
1580 self.parse_function_entry(lines)
1582 def parse_cg(self):
1583 """Parse the call graph."""
1585 # skip call graph header
1586 line = self.readline()
1587 while self._cg_header_re.match(line):
1588 line = self.readline()
1590 # process call graph entries
1591 entry_lines = []
1592 # An EOF in readline terminates the program without returning.
1593 while not self._cg_footer_re.match(line):
1594 if line.isspace():
1595 self.parse_cg_entry(entry_lines)
1596 entry_lines = []
1597 else:
1598 entry_lines.append(line)
1599 line = self.readline()
1601 def parse(self):
1602 sys.stderr.write('warning: for axe format, edge weights are unreliable estimates derived from function total times.\n')
1603 self.parse_cg()
1604 self.fp.close()
1606 profile = Profile()
1607 profile[TIME] = 0.0
1609 cycles = {}
1610 for index in self.cycles:
1611 cycles[index] = Cycle()
1613 for entry in self.functions.values():
1614 # populate the function
1615 function = Function(entry.index, entry.name)
1616 function[TIME] = entry.self
1617 function[TOTAL_TIME_RATIO] = entry.percentage_time / 100.0
1619 # populate the function calls
1620 for child in entry.children:
1621 call = Call(child.index)
1622 # The following bogus value affects only the weighting of
1623 # the calls.
1624 call[TOTAL_TIME_RATIO] = function[TOTAL_TIME_RATIO]
1626 if child.index not in self.functions:
1627 # NOTE: functions that were never called but were discovered by gprof's
1628 # static call graph analysis dont have a call graph entry so we need
1629 # to add them here
1630 # FIXME: Is this applicable?
1631 missing = Function(child.index, child.name)
1632 function[TIME] = 0.0
1633 profile.add_function(missing)
1635 function.add_call(call)
1637 profile.add_function(function)
1639 if entry.cycle is not None:
1640 try:
1641 cycle = cycles[entry.cycle]
1642 except KeyError:
1643 sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle)
1644 cycle = Cycle()
1645 cycles[entry.cycle] = cycle
1646 cycle.add_function(function)
1648 profile[TIME] = profile[TIME] + function[TIME]
1650 for cycle in cycles.values():
1651 profile.add_cycle(cycle)
1653 # Compute derived events.
1654 profile.validate()
1655 profile.ratio(TIME_RATIO, TIME)
1656 # Lacking call counts, fake call ratios based on total times.
1657 profile.call_ratios(TOTAL_TIME_RATIO)
1658 # The TOTAL_TIME_RATIO of functions is already set. Propagate that
1659 # total time to the calls. (TOTAL_TIME is neither set nor used.)
1660 for function in profile.functions.values():
1661 for call in function.calls.values():
1662 if call.ratio is not None:
1663 callee = profile.functions[call.callee_id]
1664 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
1666 return profile
1669class CallgrindParser(LineParser):
1670 """Parser for valgrind's callgrind tool.
1672 See also:
1673 - https://valgrind.org/docs/manual/cl-format.html
1674 """
1676 _call_re = re.compile(r'^calls=\s*(\d+)\s+((\d+|\+\d+|-\d+|\*)\s+)+$')
1678 def __init__(self, infile):
1679 LineParser.__init__(self, infile)
1681 # Textual positions
1682 self.position_ids = {}
1683 self.positions = {}
1685 # Numeric positions
1686 self.num_positions = 1
1687 self.cost_positions = ['line']
1688 self.last_positions = [0]
1690 # Events
1691 self.num_events = 0
1692 self.cost_events = []
1694 self.profile = Profile()
1695 self.profile[SAMPLES] = 0
1697 def parse(self):
1698 # read lookahead
1699 self.readline()
1701 self.parse_key('version')
1702 self.parse_key('creator')
1703 while self.parse_part():
1704 pass
1705 if not self.eof():
1706 sys.stderr.write('warning: line %u: unexpected line\n' % self.line_no)
1707 sys.stderr.write('%s\n' % self.lookahead())
1709 # compute derived data
1710 self.profile.validate()
1711 self.profile.find_cycles()
1712 self.profile.ratio(TIME_RATIO, SAMPLES)
1713 self.profile.call_ratios(SAMPLES2)
1714 self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
1716 return self.profile
1718 def parse_part(self):
1719 if not self.parse_header_line():
1720 return False
1721 while self.parse_header_line():
1722 pass
1723 if not self.parse_body_line():
1724 return False
1725 while self.parse_body_line():
1726 pass
1727 return True
1729 def parse_header_line(self):
1730 return \
1731 self.parse_empty() or \
1732 self.parse_comment() or \
1733 self.parse_part_detail() or \
1734 self.parse_description() or \
1735 self.parse_event_specification() or \
1736 self.parse_cost_line_def() or \
1737 self.parse_cost_summary()
1739 _detail_keys = set(('cmd', 'pid', 'thread', 'part'))
1741 def parse_part_detail(self):
1742 return self.parse_keys(self._detail_keys)
1744 def parse_description(self):
1745 return self.parse_key('desc') is not None
1747 def parse_event_specification(self):
1748 event = self.parse_key('event')
1749 if event is None:
1750 return False
1751 return True
1753 def parse_cost_line_def(self):
1754 pair = self.parse_keys(('events', 'positions'))
1755 if pair is None:
1756 return False
1757 key, value = pair
1758 items = value.split()
1759 if key == 'events':
1760 self.num_events = len(items)
1761 self.cost_events = items
1762 if key == 'positions':
1763 self.num_positions = len(items)
1764 self.cost_positions = items
1765 self.last_positions = [0]*self.num_positions
1766 return True
1768 def parse_cost_summary(self):
1769 pair = self.parse_keys(('summary', 'totals'))
1770 if pair is None:
1771 return False
1772 return True
1774 def parse_body_line(self):
1775 return \
1776 self.parse_empty() or \
1777 self.parse_comment() or \
1778 self.parse_cost_line() or \
1779 self.parse_position_spec() or \
1780 self.parse_association_spec()
1782 __subpos_re = r'(0x[0-9a-fA-F]+|\d+|\+\d+|-\d+|\*)'
1783 _cost_re = re.compile(r'^' +
1784 __subpos_re + r'( +' + __subpos_re + r')*' +
1785 r'( +\d+)*' +
1786 '$')
1788 def parse_cost_line(self, calls=None):
1789 line = self.lookahead().rstrip()
1790 mo = self._cost_re.match(line)
1791 if not mo:
1792 return False
1794 function = self.get_function()
1796 if calls is None:
1797 # Unlike other aspects, call object (cob) is relative not to the
1798 # last call object, but to the caller's object (ob), so try to
1799 # update it when processing a functions cost line
1800 try:
1801 self.positions['cob'] = self.positions['ob']
1802 except KeyError:
1803 pass
1805 values = line.split()
1806 assert len(values) <= self.num_positions + self.num_events
1808 positions = values[0 : self.num_positions]
1809 events = values[self.num_positions : ]
1810 events += ['0']*(self.num_events - len(events))
1812 for i in range(self.num_positions):
1813 position = positions[i]
1814 if position == '*':
1815 position = self.last_positions[i]
1816 elif position[0] in '-+':
1817 position = self.last_positions[i] + int(position)
1818 elif position.startswith('0x'):
1819 position = int(position, 16)
1820 else:
1821 position = int(position)
1822 self.last_positions[i] = position
1824 events = [float(event) for event in events]
1826 if calls is None:
1827 function[SAMPLES] += events[0]
1828 self.profile[SAMPLES] += events[0]
1829 else:
1830 callee = self.get_callee()
1831 callee.called += calls
1833 try:
1834 call = function.calls[callee.id]
1835 except KeyError:
1836 call = Call(callee.id)
1837 call[CALLS] = calls
1838 call[SAMPLES2] = events[0]
1839 function.add_call(call)
1840 else:
1841 call[CALLS] += calls
1842 call[SAMPLES2] += events[0]
1844 self.consume()
1845 return True
1847 def parse_association_spec(self):
1848 line = self.lookahead()
1849 if not line.startswith('calls='):
1850 return False
1852 _, values = line.split('=', 1)
1853 values = values.strip().split()
1854 calls = int(values[0])
1855 call_position = values[1:]
1856 self.consume()
1858 self.parse_cost_line(calls)
1860 return True
1862 _position_re = re.compile(r'^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?')
1864 _position_table_map = {
1865 'ob': 'ob',
1866 'fl': 'fl',
1867 'fi': 'fl',
1868 'fe': 'fl',
1869 'fn': 'fn',
1870 'cob': 'ob',
1871 'cfl': 'fl',
1872 'cfi': 'fl',
1873 'cfe': 'fl',
1874 'cfn': 'fn',
1875 'jfi': 'fl',
1876 }
1878 _position_map = {
1879 'ob': 'ob',
1880 'fl': 'fl',
1881 'fi': 'fl',
1882 'fe': 'fl',
1883 'fn': 'fn',
1884 'cob': 'cob',
1885 'cfl': 'cfl',
1886 'cfi': 'cfl',
1887 'cfe': 'cfl',
1888 'cfn': 'cfn',
1889 'jfi': 'jfi',
1890 }
1892 def parse_position_spec(self):
1893 line = self.lookahead()
1895 if line.startswith('jump=') or line.startswith('jcnd='):
1896 self.consume()
1897 return True
1899 mo = self._position_re.match(line)
1900 if not mo:
1901 return False
1903 position, id, name = mo.groups()
1904 if id:
1905 table = self._position_table_map[position]
1906 if name:
1907 self.position_ids[(table, id)] = name
1908 else:
1909 name = self.position_ids.get((table, id), '')
1910 self.positions[self._position_map[position]] = name
1912 self.consume()
1913 return True
1915 def parse_empty(self):
1916 if self.eof():
1917 return False
1918 line = self.lookahead()
1919 if line.strip():
1920 return False
1921 self.consume()
1922 return True
1924 def parse_comment(self):
1925 line = self.lookahead()
1926 if not line.startswith('#'):
1927 return False
1928 self.consume()
1929 return True
1931 _key_re = re.compile(r'^(\w+):')
1933 def parse_key(self, key):
1934 pair = self.parse_keys((key,))
1935 if not pair:
1936 return None
1937 key, value = pair
1938 return value
1940 def parse_keys(self, keys):
1941 line = self.lookahead()
1942 mo = self._key_re.match(line)
1943 if not mo:
1944 return None
1945 key, value = line.split(':', 1)
1946 if key not in keys:
1947 return None
1948 value = value.strip()
1949 self.consume()
1950 return key, value
1952 def make_function(self, module, filename, name):
1953 # FIXME: module and filename are not being tracked reliably
1954 #id = '|'.join((module, filename, name))
1955 id = name
1956 try:
1957 function = self.profile.functions[id]
1958 except KeyError:
1959 function = Function(id, name)
1960 if module:
1961 function.module = os.path.basename(module)
1962 function[SAMPLES] = 0
1963 function.called = 0
1964 self.profile.add_function(function)
1965 return function
1967 def get_function(self):
1968 module = self.positions.get('ob', '')
1969 filename = self.positions.get('fl', '')
1970 function = self.positions.get('fn', '')
1971 return self.make_function(module, filename, function)
1973 def get_callee(self):
1974 module = self.positions.get('cob', '')
1975 filename = self.positions.get('cfi', '')
1976 function = self.positions.get('cfn', '')
1977 return self.make_function(module, filename, function)
1979 def readline(self):
1980 # Override LineParser.readline to ignore comment lines
1981 while True:
1982 LineParser.readline(self)
1983 if self.eof() or not self.lookahead().startswith('#'):
1984 break
1987class PerfParser(LineParser):
1988 """Parser for linux perf callgraph output.
1990 It expects output generated with
1992 perf record -g
1993 perf script | gprof2dot.py --format=perf
1994 """
1996 def __init__(self, infile):
1997 LineParser.__init__(self, infile)
1998 self.profile = Profile()
2000 def readline(self):
2001 # Override LineParser.readline to ignore comment lines
2002 while True:
2003 LineParser.readline(self)
2004 if self.eof() or not self.lookahead().startswith('#'):
2005 break
2007 def parse(self):
2008 # read lookahead
2009 self.readline()
2011 profile = self.profile
2012 profile[SAMPLES] = 0
2013 while not self.eof():
2014 self.parse_event()
2016 # compute derived data
2017 profile.validate()
2018 profile.find_cycles()
2019 profile.ratio(TIME_RATIO, SAMPLES)
2020 profile.call_ratios(SAMPLES2)
2021 if totalMethod == "callratios":
2022 # Heuristic approach. TOTAL_SAMPLES is unused.
2023 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2024 elif totalMethod == "callstacks":
2025 # Use the actual call chains for functions.
2026 profile[TOTAL_SAMPLES] = profile[SAMPLES]
2027 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES)
2028 # Then propagate that total time to the calls.
2029 for function in profile.functions.values():
2030 for call in function.calls.values():
2031 if call.ratio is not None:
2032 callee = profile.functions[call.callee_id]
2033 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
2034 else:
2035 assert False
2037 return profile
2039 def parse_event(self):
2040 if self.eof():
2041 return
2043 line = self.consume()
2044 assert line
2046 callchain = self.parse_callchain()
2047 if not callchain:
2048 return
2050 callee = callchain[0]
2051 callee[SAMPLES] += 1
2052 self.profile[SAMPLES] += 1
2054 for caller in callchain[1:]:
2055 try:
2056 call = caller.calls[callee.id]
2057 except KeyError:
2058 call = Call(callee.id)
2059 call[SAMPLES2] = 1
2060 caller.add_call(call)
2061 else:
2062 call[SAMPLES2] += 1
2064 callee = caller
2066 # Increment TOTAL_SAMPLES only once on each function.
2067 stack = set(callchain)
2068 for function in stack:
2069 function[TOTAL_SAMPLES] += 1
2071 def parse_callchain(self):
2072 callchain = []
2073 while self.lookahead():
2074 function = self.parse_call()
2075 if function is None:
2076 break
2077 callchain.append(function)
2078 if self.lookahead() == '':
2079 self.consume()
2080 return callchain
2082 call_re = re.compile(r'^\s+(?P<address>[0-9a-fA-F]+)\s+(?P<symbol>.*)\s+\((?P<module>.*)\)$')
2083 addr2_re = re.compile(r'\+0x[0-9a-fA-F]+$')
2085 def parse_call(self):
2086 line = self.consume()
2087 mo = self.call_re.match(line)
2088 assert mo
2089 if not mo:
2090 return None
2092 function_name = mo.group('symbol')
2094 # If present, amputate program counter from function name.
2095 if function_name:
2096 function_name = re.sub(self.addr2_re, '', function_name)
2098 if not function_name or function_name == '[unknown]':
2099 function_name = mo.group('address')
2101 module = mo.group('module')
2103 function_id = function_name + ':' + module
2105 try:
2106 function = self.profile.functions[function_id]
2107 except KeyError:
2108 function = Function(function_id, function_name)
2109 function.module = os.path.basename(module)
2110 function[SAMPLES] = 0
2111 function[TOTAL_SAMPLES] = 0
2112 self.profile.add_function(function)
2114 return function
2117class HProfParser(LineParser):
2118 """Parser for java hprof output
2120 See also:
2121 - http://java.sun.com/developer/technicalArticles/Programming/HPROF.html
2122 """
2124 trace_re = re.compile(r'\t(.*)\((.*):(.*)\)')
2125 trace_id_re = re.compile(r'^TRACE (\d+):$')
2127 def __init__(self, infile):
2128 LineParser.__init__(self, infile)
2129 self.traces = {}
2130 self.samples = {}
2132 def parse(self):
2133 # read lookahead
2134 self.readline()
2136 while not self.lookahead().startswith('------'): self.consume()
2137 while not self.lookahead().startswith('TRACE '): self.consume()
2139 self.parse_traces()
2141 while not self.lookahead().startswith('CPU'):
2142 self.consume()
2144 self.parse_samples()
2146 # populate the profile
2147 profile = Profile()
2148 profile[SAMPLES] = 0
2150 functions = {}
2152 # build up callgraph
2153 for id, trace in self.traces.items():
2154 if not id in self.samples: continue
2155 mtime = self.samples[id][0]
2156 last = None
2158 for func, file, line in trace:
2159 if not func in functions:
2160 function = Function(func, func)
2161 function[SAMPLES] = 0
2162 profile.add_function(function)
2163 functions[func] = function
2165 function = functions[func]
2166 # allocate time to the deepest method in the trace
2167 if not last:
2168 function[SAMPLES] += mtime
2169 profile[SAMPLES] += mtime
2170 else:
2171 c = function.get_call(last)
2172 c[SAMPLES2] += mtime
2174 last = func
2176 # compute derived data
2177 profile.validate()
2178 profile.find_cycles()
2179 profile.ratio(TIME_RATIO, SAMPLES)
2180 profile.call_ratios(SAMPLES2)
2181 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2183 return profile
2185 def parse_traces(self):
2186 while self.lookahead().startswith('TRACE '):
2187 self.parse_trace()
2189 def parse_trace(self):
2190 l = self.consume()
2191 mo = self.trace_id_re.match(l)
2192 tid = mo.group(1)
2193 last = None
2194 trace = []
2196 while self.lookahead().startswith('\t'):
2197 l = self.consume()
2198 match = self.trace_re.search(l)
2199 if not match:
2200 #sys.stderr.write('Invalid line: %s\n' % l)
2201 break
2202 else:
2203 function_name, file, line = match.groups()
2204 trace += [(function_name, file, line)]
2206 self.traces[int(tid)] = trace
2208 def parse_samples(self):
2209 self.consume()
2210 self.consume()
2212 while not self.lookahead().startswith('CPU'):
2213 rank, percent_self, percent_accum, count, traceid, method = self.lookahead().split()
2214 self.samples[int(traceid)] = (int(count), method)
2215 self.consume()
2218class SysprofParser(XmlParser):
2220 def __init__(self, stream):
2221 XmlParser.__init__(self, stream)
2223 def parse(self):
2224 objects = {}
2225 nodes = {}
2227 self.element_start('profile')
2228 while self.token.type == XML_ELEMENT_START:
2229 if self.token.name_or_data == 'objects':
2230 assert not objects
2231 objects = self.parse_items('objects')
2232 elif self.token.name_or_data == 'nodes':
2233 assert not nodes
2234 nodes = self.parse_items('nodes')
2235 else:
2236 self.parse_value(self.token.name_or_data)
2237 self.element_end('profile')
2239 return self.build_profile(objects, nodes)
2241 def parse_items(self, name):
2242 assert name[-1] == 's'
2243 items = {}
2244 self.element_start(name)
2245 while self.token.type == XML_ELEMENT_START:
2246 id, values = self.parse_item(name[:-1])
2247 assert id not in items
2248 items[id] = values
2249 self.element_end(name)
2250 return items
2252 def parse_item(self, name):
2253 attrs = self.element_start(name)
2254 id = int(attrs['id'])
2255 values = self.parse_values()
2256 self.element_end(name)
2257 return id, values
2259 def parse_values(self):
2260 values = {}
2261 while self.token.type == XML_ELEMENT_START:
2262 name = self.token.name_or_data
2263 value = self.parse_value(name)
2264 assert name not in values
2265 values[name] = value
2266 return values
2268 def parse_value(self, tag):
2269 self.element_start(tag)
2270 value = self.character_data()
2271 self.element_end(tag)
2272 if value.isdigit():
2273 return int(value)
2274 if value.startswith('"') and value.endswith('"'):
2275 return value[1:-1]
2276 return value
2278 def build_profile(self, objects, nodes):
2279 profile = Profile()
2281 profile[SAMPLES] = 0
2282 for id, object in objects.items():
2283 # Ignore fake objects (process names, modules, "Everything", "kernel", etc.)
2284 if object['self'] == 0:
2285 continue
2287 function = Function(id, object['name'])
2288 function[SAMPLES] = object['self']
2289 profile.add_function(function)
2290 profile[SAMPLES] += function[SAMPLES]
2292 for id, node in nodes.items():
2293 # Ignore fake calls
2294 if node['self'] == 0:
2295 continue
2297 # Find a non-ignored parent
2298 parent_id = node['parent']
2299 while parent_id != 0:
2300 parent = nodes[parent_id]
2301 caller_id = parent['object']
2302 if objects[caller_id]['self'] != 0:
2303 break
2304 parent_id = parent['parent']
2305 if parent_id == 0:
2306 continue
2308 callee_id = node['object']
2310 assert objects[caller_id]['self']
2311 assert objects[callee_id]['self']
2313 function = profile.functions[caller_id]
2315 samples = node['self']
2316 try:
2317 call = function.calls[callee_id]
2318 except KeyError:
2319 call = Call(callee_id)
2320 call[SAMPLES2] = samples
2321 function.add_call(call)
2322 else:
2323 call[SAMPLES2] += samples
2325 # Compute derived events
2326 profile.validate()
2327 profile.find_cycles()
2328 profile.ratio(TIME_RATIO, SAMPLES)
2329 profile.call_ratios(SAMPLES2)
2330 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2332 return profile
2335class XPerfParser(Parser):
2336 """Parser for CSVs generated by XPerf, from Microsoft Windows Performance Tools.
2337 """
2339 def __init__(self, stream):
2340 Parser.__init__(self)
2341 self.stream = stream
2342 self.profile = Profile()
2343 self.profile[SAMPLES] = 0
2344 self.column = {}
2346 def parse(self):
2347 import csv
2348 reader = csv.reader(
2349 self.stream,
2350 delimiter = ',',
2351 quotechar = None,
2352 escapechar = None,
2353 doublequote = False,
2354 skipinitialspace = True,
2355 lineterminator = '\r\n',
2356 quoting = csv.QUOTE_NONE)
2357 header = True
2358 for row in reader:
2359 if header:
2360 self.parse_header(row)
2361 header = False
2362 else:
2363 self.parse_row(row)
2365 # compute derived data
2366 self.profile.validate()
2367 self.profile.find_cycles()
2368 self.profile.ratio(TIME_RATIO, SAMPLES)
2369 self.profile.call_ratios(SAMPLES2)
2370 self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2372 return self.profile
2374 def parse_header(self, row):
2375 for column in range(len(row)):
2376 name = row[column]
2377 assert name not in self.column
2378 self.column[name] = column
2380 def parse_row(self, row):
2381 fields = {}
2382 for name, column in self.column.items():
2383 value = row[column]
2384 for factory in int, float:
2385 try:
2386 value = factory(value)
2387 except ValueError:
2388 pass
2389 else:
2390 break
2391 fields[name] = value
2393 process = fields['Process Name']
2394 symbol = fields['Module'] + '!' + fields['Function']
2395 weight = fields['Weight']
2396 count = fields['Count']
2398 if process == 'Idle':
2399 return
2401 function = self.get_function(process, symbol)
2402 function[SAMPLES] += weight * count
2403 self.profile[SAMPLES] += weight * count
2405 stack = fields['Stack']
2406 if stack != '?':
2407 stack = stack.split('/')
2408 assert stack[0] == '[Root]'
2409 if stack[-1] != symbol:
2410 # XXX: some cases the sampled function does not appear in the stack
2411 stack.append(symbol)
2412 caller = None
2413 for symbol in stack[1:]:
2414 callee = self.get_function(process, symbol)
2415 if caller is not None:
2416 try:
2417 call = caller.calls[callee.id]
2418 except KeyError:
2419 call = Call(callee.id)
2420 call[SAMPLES2] = count
2421 caller.add_call(call)
2422 else:
2423 call[SAMPLES2] += count
2424 caller = callee
2426 def get_function(self, process, symbol):
2427 function_id = process + '!' + symbol
2429 try:
2430 function = self.profile.functions[function_id]
2431 except KeyError:
2432 module, name = symbol.split('!', 1)
2433 function = Function(function_id, name)
2434 function.process = process
2435 function.module = module
2436 function[SAMPLES] = 0
2437 self.profile.add_function(function)
2439 return function
2442class SleepyParser(Parser):
2443 """Parser for GNU gprof output.
2445 See also:
2446 - http://www.codersnotes.com/sleepy/
2447 - http://sleepygraph.sourceforge.net/
2448 """
2450 stdinInput = False
2452 def __init__(self, filename):
2453 Parser.__init__(self)
2455 from zipfile import ZipFile
2457 self.database = ZipFile(filename)
2459 self.symbols = {}
2460 self.calls = {}
2462 self.profile = Profile()
2464 _symbol_re = re.compile(
2465 r'^(?P<id>\w+)' +
2466 r'\s+"(?P<module>[^"]*)"' +
2467 r'\s+"(?P<procname>[^"]*)"' +
2468 r'\s+"(?P<sourcefile>[^"]*)"' +
2469 r'\s+(?P<sourceline>\d+)$'
2470 )
2472 def openEntry(self, name):
2473 # Some versions of verysleepy use lowercase filenames
2474 for database_name in self.database.namelist():
2475 if name.lower() == database_name.lower():
2476 name = database_name
2477 break
2479 return self.database.open(name, 'r')
2481 def parse_symbols(self):
2482 for line in self.openEntry('Symbols.txt'):
2483 line = line.decode('UTF-8').rstrip('\r\n')
2485 mo = self._symbol_re.match(line)
2486 if mo:
2487 symbol_id, module, procname, sourcefile, sourceline = mo.groups()
2489 function_id = ':'.join([module, procname])
2491 try:
2492 function = self.profile.functions[function_id]
2493 except KeyError:
2494 function = Function(function_id, procname)
2495 function.module = module
2496 function[SAMPLES] = 0
2497 self.profile.add_function(function)
2499 self.symbols[symbol_id] = function
2501 def parse_callstacks(self):
2502 for line in self.openEntry('Callstacks.txt'):
2503 line = line.decode('UTF-8').rstrip('\r\n')
2505 fields = line.split()
2506 samples = float(fields[0])
2507 callstack = fields[1:]
2509 callstack = [self.symbols[symbol_id] for symbol_id in callstack]
2511 callee = callstack[0]
2513 callee[SAMPLES] += samples
2514 self.profile[SAMPLES] += samples
2516 for caller in callstack[1:]:
2517 try:
2518 call = caller.calls[callee.id]
2519 except KeyError:
2520 call = Call(callee.id)
2521 call[SAMPLES2] = samples
2522 caller.add_call(call)
2523 else:
2524 call[SAMPLES2] += samples
2526 callee = caller
2528 def parse(self):
2529 profile = self.profile
2530 profile[SAMPLES] = 0
2532 self.parse_symbols()
2533 self.parse_callstacks()
2535 # Compute derived events
2536 profile.validate()
2537 profile.find_cycles()
2538 profile.ratio(TIME_RATIO, SAMPLES)
2539 profile.call_ratios(SAMPLES2)
2540 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2542 return profile
2545class PstatsParser:
2546 """Parser python profiling statistics saved with te pstats module."""
2548 stdinInput = False
2549 multipleInput = True
2551 def __init__(self, *filename):
2552 import pstats
2553 try:
2554 self.stats = pstats.Stats(*filename)
2555 except ValueError:
2556 sys.stderr.write('error: failed to load %s, maybe they are generated by different python version?\n' % ', '.join(filename))
2557 sys.exit(1)
2558 self.profile = Profile()
2559 self.function_ids = {}
2561 def get_function_name(self, key):
2562 filename, line, name = key
2563 module = os.path.splitext(filename)[0]
2564 module = os.path.basename(module)
2565 return "%s:%d:%s" % (module, line, name)
2567 def get_function(self, key):
2568 try:
2569 id = self.function_ids[key]
2570 except KeyError:
2571 id = len(self.function_ids)
2572 name = self.get_function_name(key)
2573 function = Function(id, name)
2574 function.filename = key[0]
2575 self.profile.functions[id] = function
2576 self.function_ids[key] = id
2577 else:
2578 function = self.profile.functions[id]
2579 return function
2581 def parse(self):
2582 self.profile[TIME] = 0.0
2583 self.profile[TOTAL_TIME] = self.stats.total_tt
2584 for fn, (cc, nc, tt, ct, callers) in self.stats.stats.items():
2585 callee = self.get_function(fn)
2586 callee.called = nc
2587 callee[TOTAL_TIME] = ct
2588 callee[TIME] = tt
2589 self.profile[TIME] += tt
2590 self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct)
2591 for fn, value in callers.items():
2592 caller = self.get_function(fn)
2593 call = Call(callee.id)
2594 if isinstance(value, tuple):
2595 for i in range(0, len(value), 4):
2596 nc, cc, tt, ct = value[i:i+4]
2597 if CALLS in call:
2598 call[CALLS] += cc
2599 else:
2600 call[CALLS] = cc
2602 if TOTAL_TIME in call:
2603 call[TOTAL_TIME] += ct
2604 else:
2605 call[TOTAL_TIME] = ct
2607 else:
2608 call[CALLS] = value
2609 call[TOTAL_TIME] = ratio(value, nc)*ct
2611 caller.add_call(call)
2613 if False:
2614 self.stats.print_stats()
2615 self.stats.print_callees()
2617 # Compute derived events
2618 self.profile.validate()
2619 self.profile.ratio(TIME_RATIO, TIME)
2620 self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
2622 return self.profile
2624class DtraceParser(LineParser):
2625 """Parser for linux perf callgraph output.
2627 It expects output generated with
2629 # Refer to https://github.com/brendangregg/FlameGraph#dtrace
2630 # 60 seconds of user-level stacks, including time spent in-kernel, for PID 12345 at 97 Hertz
2631 sudo dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks
2633 # The dtrace output
2634 gprof2dot.py -f dtrace out.user_stacks
2636 # Notice: sometimes, the dtrace outputs format may be latin-1, and gprof2dot will fail to parse it.
2637 # To solve this problem, you should use iconv to convert to UTF-8 explicitly.
2638 # TODO: add an encoding flag to tell gprof2dot how to decode the profile file.
2639 iconv -f ISO-8859-1 -t UTF-8 out.user_stacks | gprof2dot.py -f dtrace
2640 """
2642 def __init__(self, infile):
2643 LineParser.__init__(self, infile)
2644 self.profile = Profile()
2646 def readline(self):
2647 # Override LineParser.readline to ignore comment lines
2648 while True:
2649 LineParser.readline(self)
2650 if self.eof():
2651 break
2653 line = self.lookahead().strip()
2654 if line.startswith('CPU'):
2655 # The format likes:
2656 # CPU ID FUNCTION:NAME
2657 # 1 29684 :tick-60s
2658 # Skip next line
2659 LineParser.readline(self)
2660 elif not line == '':
2661 break
2664 def parse(self):
2665 # read lookahead
2666 self.readline()
2668 profile = self.profile
2669 profile[SAMPLES] = 0
2670 while not self.eof():
2671 self.parse_event()
2673 # compute derived data
2674 profile.validate()
2675 profile.find_cycles()
2676 profile.ratio(TIME_RATIO, SAMPLES)
2677 profile.call_ratios(SAMPLES2)
2678 if totalMethod == "callratios":
2679 # Heuristic approach. TOTAL_SAMPLES is unused.
2680 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2681 elif totalMethod == "callstacks":
2682 # Use the actual call chains for functions.
2683 profile[TOTAL_SAMPLES] = profile[SAMPLES]
2684 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES)
2685 # Then propagate that total time to the calls.
2686 for function in profile.functions.values():
2687 for call in function.calls.values():
2688 if call.ratio is not None:
2689 callee = profile.functions[call.callee_id]
2690 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
2691 else:
2692 assert False
2694 return profile
2696 def parse_event(self):
2697 if self.eof():
2698 return
2700 callchain, count = self.parse_callchain()
2701 if not callchain:
2702 return
2704 callee = callchain[0]
2705 callee[SAMPLES] += count
2706 self.profile[SAMPLES] += count
2708 for caller in callchain[1:]:
2709 try:
2710 call = caller.calls[callee.id]
2711 except KeyError:
2712 call = Call(callee.id)
2713 call[SAMPLES2] = count
2714 caller.add_call(call)
2715 else:
2716 call[SAMPLES2] += count
2718 callee = caller
2720 # Increment TOTAL_SAMPLES only once on each function.
2721 stack = set(callchain)
2722 for function in stack:
2723 function[TOTAL_SAMPLES] += count
2726 def parse_callchain(self):
2727 callchain = []
2728 count = 0
2729 while self.lookahead():
2730 function, count = self.parse_call()
2731 if function is None:
2732 break
2733 callchain.append(function)
2734 return callchain, count
2736 call_re = re.compile(r'^\s+(?P<module>.*)`(?P<symbol>.*)')
2737 addr2_re = re.compile(r'\+0x[0-9a-fA-F]+$')
2739 def parse_call(self):
2740 line = self.consume()
2741 mo = self.call_re.match(line)
2742 if not mo:
2743 # The line must be the stack count
2744 return None, int(line.strip())
2746 function_name = mo.group('symbol')
2748 # If present, amputate program counter from function name.
2749 if function_name:
2750 function_name = re.sub(self.addr2_re, '', function_name)
2752 # if not function_name or function_name == '[unknown]':
2753 # function_name = mo.group('address')
2755 module = mo.group('module')
2757 function_id = function_name + ':' + module
2759 try:
2760 function = self.profile.functions[function_id]
2761 except KeyError:
2762 function = Function(function_id, function_name)
2763 function.module = os.path.basename(module)
2764 function[SAMPLES] = 0
2765 function[TOTAL_SAMPLES] = 0
2766 self.profile.add_function(function)
2768 return function, None
2771class CollapseParser(LineParser):
2772 """Parser for the output of stackcollapse
2774 (from https://github.com/brendangregg/FlameGraph)
2775 """
2777 def __init__(self, infile):
2778 LineParser.__init__(self, infile)
2779 self.profile = Profile()
2781 def parse(self):
2782 profile = self.profile
2783 profile[SAMPLES] = 0
2785 self.readline()
2786 while not self.eof():
2787 self.parse_event()
2789 # compute derived data
2790 profile.validate()
2791 profile.find_cycles()
2792 profile.ratio(TIME_RATIO, SAMPLES)
2793 profile.call_ratios(SAMPLES2)
2794 if totalMethod == "callratios":
2795 # Heuristic approach. TOTAL_SAMPLES is unused.
2796 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
2797 elif totalMethod == "callstacks":
2798 # Use the actual call chains for functions.
2799 profile[TOTAL_SAMPLES] = profile[SAMPLES]
2800 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES)
2801 # Then propagate that total time to the calls.
2802 for function in compat_itervalues(profile.functions):
2803 for call in compat_itervalues(function.calls):
2804 if call.ratio is not None:
2805 callee = profile.functions[call.callee_id]
2806 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
2807 else:
2808 assert False
2810 return profile
2812 def parse_event(self):
2813 line = self.consume()
2815 stack, count = line.rsplit(' ',maxsplit=1)
2816 count=int(count)
2817 self.profile[SAMPLES] += count
2819 calls = stack.split(';')
2820 functions = [self._make_function(call) for call in calls]
2822 functions[-1][SAMPLES] += count
2824 # TOTAL_SAMPLES excludes loops
2825 for func in set(functions):
2826 func[TOTAL_SAMPLES] += count
2828 # add call data
2829 callee = functions[-1]
2830 for caller in reversed(functions[:-1]):
2831 call = caller.calls.get(callee.id)
2832 if call is None:
2833 call = Call(callee.id)
2834 call[SAMPLES2] = 0
2835 caller.add_call(call)
2836 call[SAMPLES2] += count
2837 callee = caller
2839 call_re = re.compile(r'^(?P<func>[^ ]+) \((?P<file>.*):(?P<line>[0-9]+)\)$')
2841 def _make_function(self, call):
2842 """turn a call str into a Function
2844 takes a call site, as found between semicolons in the input, and returns
2845 a Function definition corresponding to that call site.
2846 """
2847 mo = self.call_re.match(call)
2848 if mo:
2849 name, module, line = mo.groups()
2850 func_id = "%s:%s" % (module, name)
2851 else:
2852 name = func_id = call
2853 module = None
2855 func = self.profile.functions.get(func_id)
2856 if func is None:
2857 func = Function(func_id, name)
2858 func.module = module
2859 func[SAMPLES] = 0
2860 func[TOTAL_SAMPLES] = 0
2861 self.profile.add_function(func)
2862 return func
2865formats = {
2866 "axe": AXEParser,
2867 "callgrind": CallgrindParser,
2868 "collapse": CollapseParser,
2869 "hprof": HProfParser,
2870 "json": JsonParser,
2871 "perf": PerfParser,
2872 "prof": GprofParser,
2873 "pstats": PstatsParser,
2874 "sleepy": SleepyParser,
2875 "sysprof": SysprofParser,
2876 "xperf": XPerfParser,
2877 "dtrace": DtraceParser,
2878}
2881########################################################################
2882# Output
2885class Theme:
2887 def __init__(self,
2888 bgcolor = (0.0, 0.0, 1.0),
2889 mincolor = (0.0, 0.0, 0.0),
2890 maxcolor = (0.0, 0.0, 1.0),
2891 fontname = "Arial",
2892 fontcolor = "white",
2893 nodestyle = "filled",
2894 minfontsize = 10.0,
2895 maxfontsize = 10.0,
2896 minpenwidth = 0.5,
2897 maxpenwidth = 4.0,
2898 gamma = 2.2,
2899 skew = 1.0):
2900 self.bgcolor = bgcolor
2901 self.mincolor = mincolor
2902 self.maxcolor = maxcolor
2903 self.fontname = fontname
2904 self.fontcolor = fontcolor
2905 self.nodestyle = nodestyle
2906 self.minfontsize = minfontsize
2907 self.maxfontsize = maxfontsize
2908 self.minpenwidth = minpenwidth
2909 self.maxpenwidth = maxpenwidth
2910 self.gamma = gamma
2911 self.skew = skew
2913 def graph_bgcolor(self):
2914 return self.hsl_to_rgb(*self.bgcolor)
2916 def graph_fontname(self):
2917 return self.fontname
2919 def graph_fontcolor(self):
2920 return self.fontcolor
2922 def node_bgcolor(self, weight):
2923 return self.color(weight)
2925 def node_fgcolor(self, weight):
2926 if self.nodestyle == "filled":
2927 return self.graph_bgcolor()
2928 else:
2929 return self.color(weight)
2931 def node_fontsize(self, weight):
2932 return self.fontsize(weight)
2934 def node_style(self):
2935 return self.nodestyle
2937 def edge_color(self, weight):
2938 return self.color(weight)
2940 def edge_fontsize(self, weight):
2941 return self.fontsize(weight)
2943 def edge_penwidth(self, weight):
2944 return max(weight*self.maxpenwidth, self.minpenwidth)
2946 def edge_arrowsize(self, weight):
2947 return 0.5 * math.sqrt(self.edge_penwidth(weight))
2949 def fontsize(self, weight):
2950 return max(weight**2 * self.maxfontsize, self.minfontsize)
2952 def color(self, weight):
2953 weight = min(max(weight, 0.0), 1.0)
2955 hmin, smin, lmin = self.mincolor
2956 hmax, smax, lmax = self.maxcolor
2958 if self.skew < 0:
2959 raise ValueError("Skew must be greater than 0")
2960 elif self.skew == 1.0:
2961 h = hmin + weight*(hmax - hmin)
2962 s = smin + weight*(smax - smin)
2963 l = lmin + weight*(lmax - lmin)
2964 else:
2965 base = self.skew
2966 h = hmin + ((hmax-hmin)*(-1.0 + (base ** weight)) / (base - 1.0))
2967 s = smin + ((smax-smin)*(-1.0 + (base ** weight)) / (base - 1.0))
2968 l = lmin + ((lmax-lmin)*(-1.0 + (base ** weight)) / (base - 1.0))
2970 return self.hsl_to_rgb(h, s, l)
2972 def hsl_to_rgb(self, h, s, l):
2973 """Convert a color from HSL color-model to RGB.
2975 See also:
2976 - http://www.w3.org/TR/css3-color/#hsl-color
2977 """
2979 h = h % 1.0
2980 s = min(max(s, 0.0), 1.0)
2981 l = min(max(l, 0.0), 1.0)
2983 if l <= 0.5:
2984 m2 = l*(s + 1.0)
2985 else:
2986 m2 = l + s - l*s
2987 m1 = l*2.0 - m2
2988 r = self._hue_to_rgb(m1, m2, h + 1.0/3.0)
2989 g = self._hue_to_rgb(m1, m2, h)
2990 b = self._hue_to_rgb(m1, m2, h - 1.0/3.0)
2992 # Apply gamma correction
2993 r **= self.gamma
2994 g **= self.gamma
2995 b **= self.gamma
2997 return (r, g, b)
2999 def _hue_to_rgb(self, m1, m2, h):
3000 if h < 0.0:
3001 h += 1.0
3002 elif h > 1.0:
3003 h -= 1.0
3004 if h*6 < 1.0:
3005 return m1 + (m2 - m1)*h*6.0
3006 elif h*2 < 1.0:
3007 return m2
3008 elif h*3 < 2.0:
3009 return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0
3010 else:
3011 return m1
3014TEMPERATURE_COLORMAP = Theme(
3015 mincolor = (2.0/3.0, 0.80, 0.25), # dark blue
3016 maxcolor = (0.0, 1.0, 0.5), # satured red
3017 gamma = 1.0
3018)
3020PINK_COLORMAP = Theme(
3021 mincolor = (0.0, 1.0, 0.90), # pink
3022 maxcolor = (0.0, 1.0, 0.5), # satured red
3023)
3025GRAY_COLORMAP = Theme(
3026 mincolor = (0.0, 0.0, 0.85), # light gray
3027 maxcolor = (0.0, 0.0, 0.0), # black
3028)
3030BW_COLORMAP = Theme(
3031 minfontsize = 8.0,
3032 maxfontsize = 24.0,
3033 mincolor = (0.0, 0.0, 0.0), # black
3034 maxcolor = (0.0, 0.0, 0.0), # black
3035 minpenwidth = 0.1,
3036 maxpenwidth = 8.0,
3037)
3039PRINT_COLORMAP = Theme(
3040 minfontsize = 18.0,
3041 maxfontsize = 30.0,
3042 fontcolor = "black",
3043 nodestyle = "solid",
3044 mincolor = (0.0, 0.0, 0.0), # black
3045 maxcolor = (0.0, 0.0, 0.0), # black
3046 minpenwidth = 0.1,
3047 maxpenwidth = 8.0,
3048)
3051themes = {
3052 "color": TEMPERATURE_COLORMAP,
3053 "pink": PINK_COLORMAP,
3054 "gray": GRAY_COLORMAP,
3055 "bw": BW_COLORMAP,
3056 "print": PRINT_COLORMAP,
3057}
3060def sorted_iteritems(d):
3061 # Used mostly for result reproducibility (while testing.)
3062 keys = list(d.keys())
3063 keys.sort()
3064 for key in keys:
3065 value = d[key]
3066 yield key, value
3069class DotWriter:
3070 """Writer for the DOT language.
3072 See also:
3073 - "The DOT Language" specification
3074 http://www.graphviz.org/doc/info/lang.html
3075 """
3077 strip = False
3078 wrap = False
3080 def __init__(self, fp):
3081 self.fp = fp
3083 def wrap_function_name(self, name):
3084 """Split the function name on multiple lines."""
3086 if len(name) > 32:
3087 ratio = 2.0/3.0
3088 height = max(int(len(name)/(1.0 - ratio) + 0.5), 1)
3089 width = max(len(name)/height, 32)
3090 # TODO: break lines in symbols
3091 name = textwrap.fill(name, width, break_long_words=False)
3093 # Take away spaces
3094 name = name.replace(", ", ",")
3095 name = name.replace("> >", ">>")
3096 name = name.replace("> >", ">>") # catch consecutive
3098 return name
3100 show_function_events = [TOTAL_TIME_RATIO, TIME_RATIO]
3101 show_edge_events = [TOTAL_TIME_RATIO, CALLS]
3103 def graphs_compare(self, profile1, profile2, theme, options):
3104 self.begin_graph()
3106 fontname = theme.graph_fontname()
3107 fontcolor = theme.graph_fontcolor()
3108 nodestyle = theme.node_style()
3110 tolerance, only_slower, only_faster, color_by_difference = (
3111 options.tolerance, options.only_slower, options.only_faster, options.color_by_difference)
3112 self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
3113 self.attr('node', fontname=fontname, style=nodestyle, fontcolor=fontcolor, width=0, height=0)
3114 self.attr('edge', fontname=fontname)
3116 functions2 = {function.name: function for _, function in sorted_iteritems(profile2.functions)}
3118 for _, function1 in sorted_iteritems(profile1.functions):
3119 labels = []
3121 name = function1.name
3122 try:
3123 function2 = functions2[name]
3124 if self.wrap:
3125 name = self.wrap_function_name(name)
3126 if color_by_difference:
3127 min_diff, max_diff = min_max_difference(profile1, profile2)
3128 labels.append(name)
3129 weight_difference = 0
3130 shape = 'box'
3131 orientation = '0'
3132 for event in self.show_function_events:
3133 if event in function1.events:
3134 event1 = function1[event]
3135 event2 = function2[event]
3137 difference = abs(event1 - event2) * 100
3139 if event == TOTAL_TIME_RATIO:
3140 weight_difference = difference
3141 if difference >= tolerance:
3142 if event2 > event1 and not only_faster:
3143 shape = 'cds'
3144 label = (f'{event.format(event1)} +'
3145 f' {round_difference(difference, tolerance)}%')
3146 elif event2 < event1 and not only_slower:
3147 orientation = "90"
3148 shape = 'cds'
3149 label = (f'{event.format(event1)} - '
3150 f'{round_difference(difference, tolerance)}%')
3151 else:
3152 # protection to not color by difference if we choose to show only_faster/only_slower
3153 weight_difference = 0
3154 label = event.format(function1[event])
3155 else:
3156 weight_difference = 0
3157 label = event.format(function1[event])
3158 else:
3159 if difference >= tolerance:
3160 if event2 > event1:
3161 label = (f'{event.format(event1)} +'
3162 f' {round_difference(difference, tolerance)}%')
3163 elif event2 < event1:
3164 label = (f'{event.format(event1)} - '
3165 f'{round_difference(difference, tolerance)}%')
3166 else:
3167 label = event.format(function1[event])
3169 labels.append(label)
3170 if function1.called is not None:
3171 labels.append(f"{function1.called} {MULTIPLICATION_SIGN}/ {function2.called} {MULTIPLICATION_SIGN}")
3173 except KeyError:
3174 shape = 'box'
3175 orientation = '0'
3176 weight_difference = 0
3177 if function1.process is not None:
3178 labels.append(function1.process)
3179 if function1.module is not None:
3180 labels.append(function1.module)
3182 if self.strip:
3183 function_name = function1.stripped_name()
3184 else:
3185 function_name = function1.name
3186 if color_by_difference:
3187 min_diff, max_diff = 0, 0
3189 # dot can't parse quoted strings longer than YY_BUF_SIZE, which
3190 # defaults to 16K. But some annotated C++ functions (e.g., boost,
3191 # https://github.com/jrfonseca/gprof2dot/issues/30) can exceed that
3192 MAX_FUNCTION_NAME = 4096
3193 if len(function_name) >= MAX_FUNCTION_NAME:
3194 sys.stderr.write('warning: truncating function name with %u chars (%s)\n' % (len(function_name), function_name[:32] + '...'))
3195 function_name = function_name[:MAX_FUNCTION_NAME - 1] + chr(0x2026)
3197 if self.wrap:
3198 function_name = self.wrap_function_name(function_name)
3199 labels.append(function_name)
3201 for event in self.show_function_events:
3202 if event in function1.events:
3203 label = event.format(function1[event])
3204 labels.append(label)
3205 if function1.called is not None:
3206 labels.append("%u%s" % (function1.called, MULTIPLICATION_SIGN))
3208 if color_by_difference and weight_difference:
3209 # min and max is calculated whe color_by_difference is true
3210 weight = rescale_difference(weight_difference, min_diff, max_diff)
3212 elif function1.weight is not None and not color_by_difference:
3213 weight = function1.weight
3214 else:
3215 weight = 0.0
3217 label = '\n'.join(labels)
3219 self.node(function1.id,
3220 label=label,
3221 orientation=orientation,
3222 color=self.color(theme.node_bgcolor(weight)),
3223 shape=shape,
3224 fontcolor=self.color(theme.node_fgcolor(weight)),
3225 fontsize="%f" % theme.node_fontsize(weight),
3226 tooltip=function1.filename,
3227 )
3229 calls2 = {call.callee_id: call for _, call in sorted_iteritems(function2.calls)}
3230 functions_by_id1 = {function.id: function for _, function in sorted_iteritems(profile1.functions)}
3232 for _, call1 in sorted_iteritems(function1.calls):
3233 labels = []
3234 try:
3235 # if profiles do not have identical setups, callee_id will not be identical either
3236 call_id1 = call1.callee_id
3237 call_name = functions_by_id1[call_id1].name
3238 call_id2 = functions2[call_name].id
3239 call2 = calls2[call_id2]
3240 for event in self.show_edge_events:
3241 if event in call1.events:
3242 label = f'{event.format(call1[event])} / {event.format(call2[event])}'
3243 labels.append(label)
3244 except KeyError:
3245 for event in self.show_edge_events:
3246 if event in call1.events:
3247 label = f'{event.format(call1[event])}'
3248 labels.append(label)
3250 weight = 0 if color_by_difference else call1.weight
3251 label = '\n'.join(labels)
3252 self.edge(function1.id, call1.callee_id,
3253 label=label,
3254 color=self.color(theme.edge_color(weight)),
3255 fontcolor=self.color(theme.edge_color(weight)),
3256 fontsize="%.2f" % theme.edge_fontsize(weight),
3257 penwidth="%.2f" % theme.edge_penwidth(weight),
3258 labeldistance="%.2f" % theme.edge_penwidth(weight),
3259 arrowsize="%.2f" % theme.edge_arrowsize(weight),
3260 )
3261 self.end_graph()
3263 def graph(self, profile, theme):
3264 self.begin_graph()
3266 fontname = theme.graph_fontname()
3267 fontcolor = theme.graph_fontcolor()
3268 nodestyle = theme.node_style()
3270 self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
3271 self.attr('node', fontname=fontname, shape="box", style=nodestyle, fontcolor=fontcolor, width=0, height=0)
3272 self.attr('edge', fontname=fontname)
3274 for _, function in sorted_iteritems(profile.functions):
3275 labels = []
3276 if function.process is not None:
3277 labels.append(function.process)
3278 if function.module is not None:
3279 labels.append(function.module)
3281 if self.strip:
3282 function_name = function.stripped_name()
3283 else:
3284 function_name = function.name
3286 # dot can't parse quoted strings longer than YY_BUF_SIZE, which
3287 # defaults to 16K. But some annotated C++ functions (e.g., boost,
3288 # https://github.com/jrfonseca/gprof2dot/issues/30) can exceed that
3289 MAX_FUNCTION_NAME = 4096
3290 if len(function_name) >= MAX_FUNCTION_NAME:
3291 sys.stderr.write('warning: truncating function name with %u chars (%s)\n' % (len(function_name), function_name[:32] + '...'))
3292 function_name = function_name[:MAX_FUNCTION_NAME - 1] + chr(0x2026)
3294 if self.wrap:
3295 function_name = self.wrap_function_name(function_name)
3296 labels.append(function_name)
3298 for event in self.show_function_events:
3299 if event in function.events:
3300 label = event.format(function[event])
3301 labels.append(label)
3302 if function.called is not None:
3303 labels.append("%u%s" % (function.called, MULTIPLICATION_SIGN))
3305 if function.weight is not None:
3306 weight = function.weight
3307 else:
3308 weight = 0.0
3310 label = '\n'.join(labels)
3311 self.node(function.id,
3312 label = label,
3313 color = self.color(theme.node_bgcolor(weight)),
3314 fontcolor = self.color(theme.node_fgcolor(weight)),
3315 fontsize = "%.2f" % theme.node_fontsize(weight),
3316 tooltip = function.filename,
3317 )
3319 for _, call in sorted_iteritems(function.calls):
3320 callee = profile.functions[call.callee_id]
3322 labels = []
3323 for event in self.show_edge_events:
3324 if event in call.events:
3325 label = event.format(call[event])
3326 labels.append(label)
3328 if call.weight is not None:
3329 weight = call.weight
3330 elif callee.weight is not None:
3331 weight = callee.weight
3332 else:
3333 weight = 0.0
3335 label = '\n'.join(labels)
3337 self.edge(function.id, call.callee_id,
3338 label = label,
3339 color = self.color(theme.edge_color(weight)),
3340 fontcolor = self.color(theme.edge_color(weight)),
3341 fontsize = "%.2f" % theme.edge_fontsize(weight),
3342 penwidth = "%.2f" % theme.edge_penwidth(weight),
3343 labeldistance = "%.2f" % theme.edge_penwidth(weight),
3344 arrowsize = "%.2f" % theme.edge_arrowsize(weight),
3345 )
3347 self.end_graph()
3349 def begin_graph(self):
3350 self.write('digraph {\n')
3351 # Work-around graphviz bug[1]: unnamed graphs have "%3" tooltip in SVG
3352 # output. The bug was fixed upstream, but graphviz shipped in recent
3353 # Linux distros (for example, Ubuntu 24.04) still has the bug.
3354 # [1] https://gitlab.com/graphviz/graphviz/-/issues/1376
3355 self.write('\ttooltip=" "\n')
3357 def end_graph(self):
3358 self.write('}\n')
3360 def attr(self, what, **attrs):
3361 self.write("\t")
3362 self.write(what)
3363 self.attr_list(attrs)
3364 self.write(";\n")
3366 def node(self, node, **attrs):
3367 self.write("\t")
3368 self.node_id(node)
3369 self.attr_list(attrs)
3370 self.write(";\n")
3372 def edge(self, src, dst, **attrs):
3373 self.write("\t")
3374 self.node_id(src)
3375 self.write(" -> ")
3376 self.node_id(dst)
3377 self.attr_list(attrs)
3378 self.write(";\n")
3380 def attr_list(self, attrs):
3381 if not attrs:
3382 return
3383 self.write(' [')
3384 first = True
3385 for name, value in sorted_iteritems(attrs):
3386 if value is None:
3387 continue
3388 if first:
3389 first = False
3390 else:
3391 self.write(", ")
3392 assert isinstance(name, str)
3393 assert name.isidentifier()
3394 self.write(name)
3395 self.write('=')
3396 self.id(value)
3397 self.write(']')
3399 def node_id(self, id):
3400 # Node IDs need to be unique (can't be truncated) but dot doesn't allow
3401 # IDs longer than 16384 characters, so use an hash instead for the huge
3402 # C++ symbols that can arise, as seen in
3403 # https://github.com/jrfonseca/gprof2dot/issues/99
3404 if isinstance(id, str) and len(id) > 1024:
3405 id = '_' + hashlib.sha1(id.encode('utf-8'), usedforsecurity=False).hexdigest()
3406 self.id(id)
3408 def id(self, id):
3409 if isinstance(id, (int, float)):
3410 s = str(id)
3411 elif isinstance(id, str):
3412 if id.isalnum() and not id.startswith('0x'):
3413 s = id
3414 else:
3415 s = self.escape(id)
3416 else:
3417 raise TypeError
3418 self.write(s)
3420 def color(self, rgb):
3421 r, g, b = rgb
3423 def float2int(f):
3424 if f <= 0.0:
3425 return 0
3426 if f >= 1.0:
3427 return 255
3428 return int(255.0*f + 0.5)
3430 return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)])
3432 def escape(self, s):
3433 s = s.replace('\\', r'\\')
3434 s = s.replace('\n', r'\n')
3435 s = s.replace('\t', r'\t')
3436 s = s.replace('"', r'\"')
3437 return '"' + s + '"'
3439 def write(self, s):
3440 self.fp.write(s)
3444########################################################################
3445# Main program
3448def naturalJoin(values):
3449 if len(values) >= 2:
3450 return ', '.join(values[:-1]) + ' or ' + values[-1]
3452 else:
3453 return ''.join(values)
3456def main(argv=sys.argv[1:]):
3457 """Main program."""
3459 global totalMethod, timeFormat
3461 formatNames = list(formats.keys())
3462 formatNames.sort()
3464 themeNames = list(themes.keys())
3465 themeNames.sort()
3467 labelNames = list(labels.keys())
3468 labelNames.sort()
3470 argparser = argparse.ArgumentParser(
3471 usage="\n %(prog)s [options] [INPUT] ...")
3472 argparser.add_argument(
3473 '-o', '--output', metavar='OUTPUT',
3474 dest="output",
3475 help="output filename [stdout]")
3476 argparser.add_argument(
3477 '-n', '--node-thres', metavar='PERCENTAGE',
3478 type=float, dest="node_thres", default=0.5,
3479 help="eliminate nodes below this threshold [default: %(default)s]")
3480 argparser.add_argument(
3481 '-e', '--edge-thres', metavar='PERCENTAGE',
3482 type=float, dest="edge_thres", default=0.1,
3483 help="eliminate edges below this threshold [default: %(default)s]")
3484 argparser.add_argument(
3485 '-f', '--format',
3486 choices=formatNames,
3487 dest="format", default="prof",
3488 help="profile format: %s [default: %%(default)s]" % naturalJoin(formatNames))
3489 argparser.add_argument(
3490 '--total',
3491 choices=('callratios', 'callstacks'),
3492 dest="totalMethod", default=totalMethod,
3493 help="preferred method of calculating total time: callratios or callstacks (currently affects only perf format) [default: %(default)s]")
3494 argparser.add_argument(
3495 '-c', '--colormap',
3496 choices=themeNames,
3497 dest="theme", default="color",
3498 help="color map: %s [default: %%(default)s]" % naturalJoin(themeNames))
3499 argparser.add_argument(
3500 '-s', '--strip',
3501 action="store_true",
3502 dest="strip", default=False,
3503 help="strip function parameters, template parameters, and const modifiers from demangled C++ function names")
3504 argparser.add_argument(
3505 '--color-nodes-by-selftime',
3506 action="store_true",
3507 dest="color_nodes_by_selftime", default=False,
3508 help="color nodes by self time, rather than by total time (sum of self and descendants)")
3509 argparser.add_argument(
3510 '--colour-nodes-by-selftime',
3511 action="store_true",
3512 dest="color_nodes_by_selftime",
3513 help=argparse.SUPPRESS)
3514 argparser.add_argument(
3515 '-w', '--wrap',
3516 action="store_true",
3517 dest="wrap", default=False,
3518 help="wrap function names")
3519 argparser.add_argument(
3520 '--show-samples',
3521 action="store_true",
3522 dest="show_samples", default=False,
3523 help="show function samples")
3524 argparser.add_argument(
3525 '--time-format',
3526 default=timeFormat,
3527 help="format to use for showing time values [default: %(default)s]")
3528 argparser.add_argument(
3529 '--node-label', metavar='MEASURE',
3530 choices=labelNames,
3531 action='append',
3532 dest='node_labels',
3533 help="measurements to on show the node (can be specified multiple times): %s [default: %s]" % (
3534 naturalJoin(labelNames), ', '.join(defaultLabelNames)))
3535 # add option to show information on available entries ()
3536 argparser.add_argument(
3537 '--list-functions',
3538 dest="list_functions", default=None,
3539 help="""\
3540list functions available for selection in -z or -l, requires selector argument
3541( use '+' to select all).
3542Recall that the selector argument is used with Unix/Bash globbing/pattern matching,
3543and that entries are formatted '<pkg>:<linenum>:<function>'. When argument starts
3544with '%%', a dump of all available information is performed for selected entries,
3545 after removal of leading '%%'.
3546""")
3547 # add option to create subtree or show paths
3548 argparser.add_argument(
3549 '-z', '--root',
3550 dest="root", default="",
3551 help="prune call graph to show only descendants of specified root function")
3552 argparser.add_argument(
3553 '-l', '--leaf',
3554 dest="leaf", default="",
3555 help="prune call graph to show only ancestors of specified leaf function")
3556 argparser.add_argument(
3557 '--depth',
3558 type=int,
3559 dest="depth", default=-1,
3560 help="prune call graph to show only descendants or ancestors until specified depth")
3561 # add a new option to control skew of the colorization curve
3562 argparser.add_argument(
3563 '--skew',
3564 type=float, dest="theme_skew", default=1.0,
3565 help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Values > 1.0 give less variety to lower percentages")
3566 # add option for filtering by file path
3567 argparser.add_argument(
3568 '-p', '--path', action="append",
3569 dest="filter_paths",
3570 help="filter all modules not in a specified path")
3571 argparser.add_argument(
3572 '--compare',
3573 action="store_true",
3574 dest="compare", default=False,
3575 help="compare two graphs with almost identical structure. With this option two files should be provided."
3576 "gprof2dot.py [options] --compare [file1] [file2] ...")
3577 argparser.add_argument(
3578 '--compare-tolerance',
3579 type=float, dest="tolerance", default=0.001,
3580 help="tolerance threshold for node difference (default=0.001%%)."
3581 "If the difference is below this value the nodes are considered identical.")
3582 argparser.add_argument(
3583 '--compare-only-slower',
3584 action="store_true",
3585 dest="only_slower", default=False,
3586 help="display comparison only for function which are slower in second graph.")
3587 argparser.add_argument(
3588 '--compare-only-faster',
3589 action="store_true",
3590 dest="only_faster", default=False,
3591 help="display comparison only for function which are faster in second graph.")
3592 argparser.add_argument(
3593 '--compare-color-by-difference',
3594 action="store_true",
3595 dest="color_by_difference", default=False,
3596 help="color nodes based on the value of the difference. "
3597 "Nodes with the largest differences represent the hot spots.")
3598 argparser.add_argument('input', nargs='*', metavar='INPUT', help='input stats')
3599 options = argparser.parse_args(argv)
3600 args = options.input
3602 if len(args) > 1 and options.format != 'pstats' and not options.compare:
3603 argparser.error('incorrect number of arguments')
3605 try:
3606 theme = themes[options.theme]
3607 except KeyError:
3608 argparser.error('invalid colormap \'%s\'' % options.theme)
3610 # set skew on the theme now that it has been picked.
3611 if options.theme_skew:
3612 theme.skew = options.theme_skew
3614 totalMethod = options.totalMethod
3615 timeFormat = options.time_format
3617 try:
3618 Format = formats[options.format]
3619 except KeyError:
3620 argparser.error('invalid format \'%s\'' % options.format)
3622 if Format.stdinInput:
3623 if not args:
3624 fp = sys.stdin
3625 parser = Format(fp)
3626 elif options.compare:
3627 fp1 = open(args[0], 'rt', encoding='UTF-8')
3628 fp2 = open(args[1], 'rt', encoding='UTF-8')
3629 parser1 = Format(fp1)
3630 parser2 = Format(fp2)
3631 else:
3632 fp = open(args[0], 'rb')
3633 bom = fp.read(2)
3634 if bom == codecs.BOM_UTF16_LE:
3635 # Default on Windows PowerShell (https://github.com/jrfonseca/gprof2dot/issues/88)
3636 encoding = 'utf-16le'
3637 else:
3638 encoding = 'utf-8'
3639 fp.seek(0)
3640 fp = io.TextIOWrapper(fp, encoding=encoding)
3641 parser = Format(fp)
3642 elif Format.multipleInput:
3643 if not args:
3644 argparser.error('at least a file must be specified for %s input' % options.format)
3645 if options.compare:
3646 parser1 = Format(args[-2])
3647 parser2 = Format(args[-1])
3648 else:
3649 parser = Format(*args)
3650 else:
3651 if len(args) != 1:
3652 argparser.error('exactly one file must be specified for %s input' % options.format)
3653 parser = Format(args[0])
3655 if options.compare:
3656 profile1 = parser1.parse()
3657 profile2 = parser2.parse()
3658 else:
3659 profile = parser.parse()
3661 if options.output is None:
3662 output = open(sys.stdout.fileno(), mode='wt', encoding='UTF-8', closefd=False)
3663 else:
3664 output = open(options.output, 'wt', encoding='UTF-8')
3666 dot = DotWriter(output)
3667 dot.strip = options.strip
3668 dot.wrap = options.wrap
3670 labelNames = options.node_labels or defaultLabelNames
3671 dot.show_function_events = [labels[l] for l in labelNames]
3672 if options.show_samples:
3673 dot.show_function_events.append(SAMPLES)
3675 if options.compare:
3676 profile1.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths,
3677 options.color_nodes_by_selftime)
3678 profile2.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths,
3679 options.color_nodes_by_selftime)
3681 if options.root:
3682 profile1.prune_root(profile1.getFunctionIds(options.root), options.depth)
3683 profile2.prune_root(profile2.getFunctionIds(options.root), options.depth)
3684 else:
3685 profile.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths,
3686 options.color_nodes_by_selftime)
3687 if options.root:
3688 rootIds = profile.getFunctionIds(options.root)
3689 if not rootIds:
3690 sys.stderr.write('root node ' + options.root + ' not found (might already be pruned : try -e0 -n0 flags)\n')
3691 sys.exit(1)
3692 profile.prune_root(rootIds, options.depth)
3694 if options.list_functions:
3695 profile.printFunctionIds(selector=options.list_functions)
3696 sys.exit(0)
3698 if options.leaf:
3699 leafIds = profile.getFunctionIds(options.leaf)
3700 if not leafIds:
3701 sys.stderr.write('leaf node ' + options.leaf + ' not found (maybe already pruned : try -e0 -n0 flags)\n')
3702 sys.exit(1)
3703 profile.prune_leaf(leafIds, options.depth)
3705 if options.compare:
3706 dot.graphs_compare(profile1, profile2, theme, options)
3707 else:
3708 dot.graph(profile, theme)
3711if __name__ == '__main__':
3712 main()