gprof2dot.py 99 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2008-2014 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. #
  18. """Generate a dot graph from the output of several profilers."""
  19. __author__ = "Jose Fonseca et al"
  20. import sys
  21. import math
  22. import os.path
  23. import re
  24. import textwrap
  25. import optparse
  26. import xml.parsers.expat
  27. import collections
  28. import locale
  29. import json
  30. # Python 2.x/3.x compatibility
  31. if sys.version_info[0] >= 3:
  32. PYTHON_3 = True
  33. def compat_iteritems(x): return x.items() # No iteritems() in Python 3
  34. def compat_itervalues(x): return x.values() # No itervalues() in Python 3
  35. def compat_keys(x): return list(x.keys()) # keys() is a generator in Python 3
  36. basestring = str # No class basestring in Python 3
  37. unichr = chr # No unichr in Python 3
  38. xrange = range # No xrange in Python 3
  39. else:
  40. PYTHON_3 = False
  41. def compat_iteritems(x): return x.iteritems()
  42. def compat_itervalues(x): return x.itervalues()
  43. def compat_keys(x): return x.keys()
  44. try:
  45. # Debugging helper module
  46. import debug
  47. except ImportError:
  48. pass
  49. ########################################################################
  50. # Model
  51. MULTIPLICATION_SIGN = unichr(0xd7)
  52. def times(x):
  53. return "%u%s" % (x, MULTIPLICATION_SIGN)
  54. def percentage(p):
  55. return "%.02f%%" % (p*100.0,)
  56. def add(a, b):
  57. return a + b
  58. def fail(a, b):
  59. assert False
  60. tol = 2 ** -23
  61. def ratio(numerator, denominator):
  62. try:
  63. ratio = float(numerator)/float(denominator)
  64. except ZeroDivisionError:
  65. # 0/0 is undefined, but 1.0 yields more useful results
  66. return 1.0
  67. if ratio < 0.0:
  68. if ratio < -tol:
  69. sys.stderr.write('warning: negative ratio (%s/%s)\n' % (numerator, denominator))
  70. return 0.0
  71. if ratio > 1.0:
  72. if ratio > 1.0 + tol:
  73. sys.stderr.write('warning: ratio greater than one (%s/%s)\n' % (numerator, denominator))
  74. return 1.0
  75. return ratio
  76. class UndefinedEvent(Exception):
  77. """Raised when attempting to get an event which is undefined."""
  78. def __init__(self, event):
  79. Exception.__init__(self)
  80. self.event = event
  81. def __str__(self):
  82. return 'unspecified event %s' % self.event.name
  83. class Event(object):
  84. """Describe a kind of event, and its basic operations."""
  85. def __init__(self, name, null, aggregator, formatter = str):
  86. self.name = name
  87. self._null = null
  88. self._aggregator = aggregator
  89. self._formatter = formatter
  90. def __eq__(self, other):
  91. return self is other
  92. def __hash__(self):
  93. return id(self)
  94. def null(self):
  95. return self._null
  96. def aggregate(self, val1, val2):
  97. """Aggregate two event values."""
  98. assert val1 is not None
  99. assert val2 is not None
  100. return self._aggregator(val1, val2)
  101. def format(self, val):
  102. """Format an event value."""
  103. assert val is not None
  104. return self._formatter(val)
  105. CALLS = Event("Calls", 0, add, times)
  106. SAMPLES = Event("Samples", 0, add, times)
  107. SAMPLES2 = Event("Samples", 0, add, times)
  108. # Count of samples where a given function was either executing or on the stack.
  109. # This is used to calculate the total time ratio according to the
  110. # straightforward method described in Mike Dunlavey's answer to
  111. # stackoverflow.com/questions/1777556/alternatives-to-gprof, item 4 (the myth
  112. # "that recursion is a tricky confusing issue"), last edited 2012-08-30: it's
  113. # just the ratio of TOTAL_SAMPLES over the number of samples in the profile.
  114. #
  115. # Used only when totalMethod == callstacks
  116. TOTAL_SAMPLES = Event("Samples", 0, add, times)
  117. TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')')
  118. TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')')
  119. TOTAL_TIME = Event("Total time", 0.0, fail)
  120. TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage)
  121. totalMethod = 'callratios'
  122. class Object(object):
  123. """Base class for all objects in profile which can store events."""
  124. def __init__(self, events=None):
  125. if events is None:
  126. self.events = {}
  127. else:
  128. self.events = events
  129. def __hash__(self):
  130. return id(self)
  131. def __eq__(self, other):
  132. return self is other
  133. def __contains__(self, event):
  134. return event in self.events
  135. def __getitem__(self, event):
  136. try:
  137. return self.events[event]
  138. except KeyError:
  139. raise UndefinedEvent(event)
  140. def __setitem__(self, event, value):
  141. if value is None:
  142. if event in self.events:
  143. del self.events[event]
  144. else:
  145. self.events[event] = value
  146. class Call(Object):
  147. """A call between functions.
  148. There should be at most one call object for every pair of functions.
  149. """
  150. def __init__(self, callee_id):
  151. Object.__init__(self)
  152. self.callee_id = callee_id
  153. self.ratio = None
  154. self.weight = None
  155. class Function(Object):
  156. """A function."""
  157. def __init__(self, id, name):
  158. Object.__init__(self)
  159. self.id = id
  160. self.name = name
  161. self.module = None
  162. self.process = None
  163. self.calls = {}
  164. self.called = None
  165. self.weight = None
  166. self.cycle = None
  167. def add_call(self, call):
  168. if call.callee_id in self.calls:
  169. sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id)))
  170. self.calls[call.callee_id] = call
  171. def get_call(self, callee_id):
  172. if not callee_id in self.calls:
  173. call = Call(callee_id)
  174. call[SAMPLES] = 0
  175. call[SAMPLES2] = 0
  176. call[CALLS] = 0
  177. self.calls[callee_id] = call
  178. return self.calls[callee_id]
  179. _parenthesis_re = re.compile(r'\([^()]*\)')
  180. _angles_re = re.compile(r'<[^<>]*>')
  181. _const_re = re.compile(r'\s+const$')
  182. def stripped_name(self):
  183. """Remove extraneous information from C++ demangled function names."""
  184. name = self.name
  185. # Strip function parameters from name by recursively removing paired parenthesis
  186. while True:
  187. name, n = self._parenthesis_re.subn('', name)
  188. if not n:
  189. break
  190. # Strip const qualifier
  191. name = self._const_re.sub('', name)
  192. # Strip template parameters from name by recursively removing paired angles
  193. while True:
  194. name, n = self._angles_re.subn('', name)
  195. if not n:
  196. break
  197. return name
  198. # TODO: write utility functions
  199. def __repr__(self):
  200. return self.name
  201. class Cycle(Object):
  202. """A cycle made from recursive function calls."""
  203. def __init__(self):
  204. Object.__init__(self)
  205. self.functions = set()
  206. def add_function(self, function):
  207. assert function not in self.functions
  208. self.functions.add(function)
  209. if function.cycle is not None:
  210. for other in function.cycle.functions:
  211. if function not in self.functions:
  212. self.add_function(other)
  213. function.cycle = self
  214. class Profile(Object):
  215. """The whole profile."""
  216. def __init__(self):
  217. Object.__init__(self)
  218. self.functions = {}
  219. self.cycles = []
  220. def add_function(self, function):
  221. if function.id in self.functions:
  222. sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id)))
  223. self.functions[function.id] = function
  224. def add_cycle(self, cycle):
  225. self.cycles.append(cycle)
  226. def validate(self):
  227. """Validate the edges."""
  228. for function in compat_itervalues(self.functions):
  229. for callee_id in compat_keys(function.calls):
  230. assert function.calls[callee_id].callee_id == callee_id
  231. if callee_id not in self.functions:
  232. sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name))
  233. del function.calls[callee_id]
  234. def find_cycles(self):
  235. """Find cycles using Tarjan's strongly connected components algorithm."""
  236. # Apply the Tarjan's algorithm successively until all functions are visited
  237. visited = set()
  238. for function in compat_itervalues(self.functions):
  239. if function not in visited:
  240. self._tarjan(function, 0, [], {}, {}, visited)
  241. cycles = []
  242. for function in compat_itervalues(self.functions):
  243. if function.cycle is not None and function.cycle not in cycles:
  244. cycles.append(function.cycle)
  245. self.cycles = cycles
  246. if 0:
  247. for cycle in cycles:
  248. sys.stderr.write("Cycle:\n")
  249. for member in cycle.functions:
  250. sys.stderr.write("\tFunction %s\n" % member.name)
  251. def prune_root(self, root):
  252. visited = set()
  253. frontier = set([root])
  254. while len(frontier) > 0:
  255. node = frontier.pop()
  256. visited.add(node)
  257. f = self.functions[node]
  258. newNodes = f.calls.keys()
  259. frontier = frontier.union(set(newNodes) - visited)
  260. subtreeFunctions = {}
  261. for n in visited:
  262. subtreeFunctions[n] = self.functions[n]
  263. self.functions = subtreeFunctions
  264. def prune_leaf(self, leaf):
  265. edgesUp = collections.defaultdict(set)
  266. for f in self.functions.keys():
  267. for n in self.functions[f].calls.keys():
  268. edgesUp[n].add(f)
  269. # build the tree up
  270. visited = set()
  271. frontier = set([leaf])
  272. while len(frontier) > 0:
  273. node = frontier.pop()
  274. visited.add(node)
  275. frontier = frontier.union(edgesUp[node] - visited)
  276. downTree = set(self.functions.keys())
  277. upTree = visited
  278. path = downTree.intersection(upTree)
  279. pathFunctions = {}
  280. for n in path:
  281. f = self.functions[n]
  282. newCalls = {}
  283. for c in f.calls.keys():
  284. if c in path:
  285. newCalls[c] = f.calls[c]
  286. f.calls = newCalls
  287. pathFunctions[n] = f
  288. self.functions = pathFunctions
  289. def getFunctionId(self, funcName):
  290. for f in self.functions:
  291. if self.functions[f].name == funcName:
  292. return f
  293. return False
  294. def _tarjan(self, function, order, stack, orders, lowlinks, visited):
  295. """Tarjan's strongly connected components algorithm.
  296. See also:
  297. - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
  298. """
  299. visited.add(function)
  300. orders[function] = order
  301. lowlinks[function] = order
  302. order += 1
  303. pos = len(stack)
  304. stack.append(function)
  305. for call in compat_itervalues(function.calls):
  306. callee = self.functions[call.callee_id]
  307. # TODO: use a set to optimize lookup
  308. if callee not in orders:
  309. order = self._tarjan(callee, order, stack, orders, lowlinks, visited)
  310. lowlinks[function] = min(lowlinks[function], lowlinks[callee])
  311. elif callee in stack:
  312. lowlinks[function] = min(lowlinks[function], orders[callee])
  313. if lowlinks[function] == orders[function]:
  314. # Strongly connected component found
  315. members = stack[pos:]
  316. del stack[pos:]
  317. if len(members) > 1:
  318. cycle = Cycle()
  319. for member in members:
  320. cycle.add_function(member)
  321. return order
  322. def call_ratios(self, event):
  323. # Aggregate for incoming calls
  324. cycle_totals = {}
  325. for cycle in self.cycles:
  326. cycle_totals[cycle] = 0.0
  327. function_totals = {}
  328. for function in compat_itervalues(self.functions):
  329. function_totals[function] = 0.0
  330. # Pass 1: function_total gets the sum of call[event] for all
  331. # incoming arrows. Same for cycle_total for all arrows
  332. # that are coming into the *cycle* but are not part of it.
  333. for function in compat_itervalues(self.functions):
  334. for call in compat_itervalues(function.calls):
  335. if call.callee_id != function.id:
  336. callee = self.functions[call.callee_id]
  337. if event in call.events:
  338. function_totals[callee] += call[event]
  339. if callee.cycle is not None and callee.cycle is not function.cycle:
  340. cycle_totals[callee.cycle] += call[event]
  341. else:
  342. sys.stderr.write("call_ratios: No data for " + function.name + " call to " + callee.name + "\n")
  343. # Pass 2: Compute the ratios. Each call[event] is scaled by the
  344. # function_total of the callee. Calls into cycles use the
  345. # cycle_total, but not calls within cycles.
  346. for function in compat_itervalues(self.functions):
  347. for call in compat_itervalues(function.calls):
  348. assert call.ratio is None
  349. if call.callee_id != function.id:
  350. callee = self.functions[call.callee_id]
  351. if event in call.events:
  352. if callee.cycle is not None and callee.cycle is not function.cycle:
  353. total = cycle_totals[callee.cycle]
  354. else:
  355. total = function_totals[callee]
  356. call.ratio = ratio(call[event], total)
  357. else:
  358. # Warnings here would only repeat those issued above.
  359. call.ratio = 0.0
  360. def integrate(self, outevent, inevent):
  361. """Propagate function time ratio along the function calls.
  362. Must be called after finding the cycles.
  363. See also:
  364. - http://citeseer.ist.psu.edu/graham82gprof.html
  365. """
  366. # Sanity checking
  367. assert outevent not in self
  368. for function in compat_itervalues(self.functions):
  369. assert outevent not in function
  370. assert inevent in function
  371. for call in compat_itervalues(function.calls):
  372. assert outevent not in call
  373. if call.callee_id != function.id:
  374. assert call.ratio is not None
  375. # Aggregate the input for each cycle
  376. for cycle in self.cycles:
  377. total = inevent.null()
  378. for function in compat_itervalues(self.functions):
  379. total = inevent.aggregate(total, function[inevent])
  380. self[inevent] = total
  381. # Integrate along the edges
  382. total = inevent.null()
  383. for function in compat_itervalues(self.functions):
  384. total = inevent.aggregate(total, function[inevent])
  385. self._integrate_function(function, outevent, inevent)
  386. self[outevent] = total
  387. def _integrate_function(self, function, outevent, inevent):
  388. if function.cycle is not None:
  389. return self._integrate_cycle(function.cycle, outevent, inevent)
  390. else:
  391. if outevent not in function:
  392. total = function[inevent]
  393. for call in compat_itervalues(function.calls):
  394. if call.callee_id != function.id:
  395. total += self._integrate_call(call, outevent, inevent)
  396. function[outevent] = total
  397. return function[outevent]
  398. def _integrate_call(self, call, outevent, inevent):
  399. assert outevent not in call
  400. assert call.ratio is not None
  401. callee = self.functions[call.callee_id]
  402. subtotal = call.ratio *self._integrate_function(callee, outevent, inevent)
  403. call[outevent] = subtotal
  404. return subtotal
  405. def _integrate_cycle(self, cycle, outevent, inevent):
  406. if outevent not in cycle:
  407. # Compute the outevent for the whole cycle
  408. total = inevent.null()
  409. for member in cycle.functions:
  410. subtotal = member[inevent]
  411. for call in compat_itervalues(member.calls):
  412. callee = self.functions[call.callee_id]
  413. if callee.cycle is not cycle:
  414. subtotal += self._integrate_call(call, outevent, inevent)
  415. total += subtotal
  416. cycle[outevent] = total
  417. # Compute the time propagated to callers of this cycle
  418. callees = {}
  419. for function in compat_itervalues(self.functions):
  420. if function.cycle is not cycle:
  421. for call in compat_itervalues(function.calls):
  422. callee = self.functions[call.callee_id]
  423. if callee.cycle is cycle:
  424. try:
  425. callees[callee] += call.ratio
  426. except KeyError:
  427. callees[callee] = call.ratio
  428. for member in cycle.functions:
  429. member[outevent] = outevent.null()
  430. for callee, call_ratio in compat_iteritems(callees):
  431. ranks = {}
  432. call_ratios = {}
  433. partials = {}
  434. self._rank_cycle_function(cycle, callee, 0, ranks)
  435. self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set())
  436. partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent)
  437. assert partial == max(partials.values())
  438. assert abs(call_ratio*total - partial) <= 0.001*call_ratio*total
  439. return cycle[outevent]
  440. def _rank_cycle_function(self, cycle, function, rank, ranks):
  441. if function not in ranks or ranks[function] > rank:
  442. ranks[function] = rank
  443. for call in compat_itervalues(function.calls):
  444. if call.callee_id != function.id:
  445. callee = self.functions[call.callee_id]
  446. if callee.cycle is cycle:
  447. self._rank_cycle_function(cycle, callee, rank + 1, ranks)
  448. def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited):
  449. if function not in visited:
  450. visited.add(function)
  451. for call in compat_itervalues(function.calls):
  452. if call.callee_id != function.id:
  453. callee = self.functions[call.callee_id]
  454. if callee.cycle is cycle:
  455. if ranks[callee] > ranks[function]:
  456. call_ratios[callee] = call_ratios.get(callee, 0.0) + call.ratio
  457. self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited)
  458. def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent):
  459. if function not in partials:
  460. partial = partial_ratio*function[inevent]
  461. for call in compat_itervalues(function.calls):
  462. if call.callee_id != function.id:
  463. callee = self.functions[call.callee_id]
  464. if callee.cycle is not cycle:
  465. assert outevent in call
  466. partial += partial_ratio*call[outevent]
  467. else:
  468. if ranks[callee] > ranks[function]:
  469. callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent)
  470. call_ratio = ratio(call.ratio, call_ratios[callee])
  471. call_partial = call_ratio*callee_partial
  472. try:
  473. call[outevent] += call_partial
  474. except UndefinedEvent:
  475. call[outevent] = call_partial
  476. partial += call_partial
  477. partials[function] = partial
  478. try:
  479. function[outevent] += partial
  480. except UndefinedEvent:
  481. function[outevent] = partial
  482. return partials[function]
  483. def aggregate(self, event):
  484. """Aggregate an event for the whole profile."""
  485. total = event.null()
  486. for function in compat_itervalues(self.functions):
  487. try:
  488. total = event.aggregate(total, function[event])
  489. except UndefinedEvent:
  490. return
  491. self[event] = total
  492. def ratio(self, outevent, inevent):
  493. assert outevent not in self
  494. assert inevent in self
  495. for function in compat_itervalues(self.functions):
  496. assert outevent not in function
  497. assert inevent in function
  498. function[outevent] = ratio(function[inevent], self[inevent])
  499. for call in compat_itervalues(function.calls):
  500. assert outevent not in call
  501. if inevent in call:
  502. call[outevent] = ratio(call[inevent], self[inevent])
  503. self[outevent] = 1.0
  504. def prune(self, node_thres, edge_thres):
  505. """Prune the profile"""
  506. # compute the prune ratios
  507. for function in compat_itervalues(self.functions):
  508. try:
  509. function.weight = function[TOTAL_TIME_RATIO]
  510. except UndefinedEvent:
  511. pass
  512. for call in compat_itervalues(function.calls):
  513. callee = self.functions[call.callee_id]
  514. if TOTAL_TIME_RATIO in call:
  515. # handle exact cases first
  516. call.weight = call[TOTAL_TIME_RATIO]
  517. else:
  518. try:
  519. # make a safe estimate
  520. call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO])
  521. except UndefinedEvent:
  522. pass
  523. # prune the nodes
  524. for function_id in compat_keys(self.functions):
  525. function = self.functions[function_id]
  526. if function.weight is not None:
  527. if function.weight < node_thres:
  528. del self.functions[function_id]
  529. # prune the egdes
  530. for function in compat_itervalues(self.functions):
  531. for callee_id in compat_keys(function.calls):
  532. call = function.calls[callee_id]
  533. if callee_id not in self.functions or call.weight is not None and call.weight < edge_thres:
  534. del function.calls[callee_id]
  535. def dump(self):
  536. for function in compat_itervalues(self.functions):
  537. sys.stderr.write('Function %s:\n' % (function.name,))
  538. self._dump_events(function.events)
  539. for call in compat_itervalues(function.calls):
  540. callee = self.functions[call.callee_id]
  541. sys.stderr.write(' Call %s:\n' % (callee.name,))
  542. self._dump_events(call.events)
  543. for cycle in self.cycles:
  544. sys.stderr.write('Cycle:\n')
  545. self._dump_events(cycle.events)
  546. for function in cycle.functions:
  547. sys.stderr.write(' Function %s\n' % (function.name,))
  548. def _dump_events(self, events):
  549. for event, value in compat_iteritems(events):
  550. sys.stderr.write(' %s: %s\n' % (event.name, event.format(value)))
  551. ########################################################################
  552. # Parsers
  553. class Struct:
  554. """Masquerade a dictionary with a structure-like behavior."""
  555. def __init__(self, attrs = None):
  556. if attrs is None:
  557. attrs = {}
  558. self.__dict__['_attrs'] = attrs
  559. def __getattr__(self, name):
  560. try:
  561. return self._attrs[name]
  562. except KeyError:
  563. raise AttributeError(name)
  564. def __setattr__(self, name, value):
  565. self._attrs[name] = value
  566. def __str__(self):
  567. return str(self._attrs)
  568. def __repr__(self):
  569. return repr(self._attrs)
  570. class ParseError(Exception):
  571. """Raised when parsing to signal mismatches."""
  572. def __init__(self, msg, line):
  573. Exception.__init__(self)
  574. self.msg = msg
  575. # TODO: store more source line information
  576. self.line = line
  577. def __str__(self):
  578. return '%s: %r' % (self.msg, self.line)
  579. class Parser:
  580. """Parser interface."""
  581. stdinInput = True
  582. multipleInput = False
  583. def __init__(self):
  584. pass
  585. def parse(self):
  586. raise NotImplementedError
  587. class JsonParser(Parser):
  588. """Parser for a custom JSON representation of profile data.
  589. See schema.json for details.
  590. """
  591. def __init__(self, stream):
  592. Parser.__init__(self)
  593. self.stream = stream
  594. def parse(self):
  595. obj = json.load(self.stream)
  596. assert obj['version'] == 0
  597. profile = Profile()
  598. profile[SAMPLES] = 0
  599. fns = obj['functions']
  600. for functionIndex in range(len(fns)):
  601. fn = fns[functionIndex]
  602. function = Function(functionIndex, fn['name'])
  603. try:
  604. function.module = fn['module']
  605. except KeyError:
  606. pass
  607. try:
  608. function.process = fn['process']
  609. except KeyError:
  610. pass
  611. function[SAMPLES] = 0
  612. profile.add_function(function)
  613. for event in obj['events']:
  614. callchain = []
  615. for functionIndex in event['callchain']:
  616. function = profile.functions[functionIndex]
  617. callchain.append(function)
  618. cost = event['cost'][0]
  619. callee = callchain[0]
  620. callee[SAMPLES] += cost
  621. profile[SAMPLES] += cost
  622. for caller in callchain[1:]:
  623. try:
  624. call = caller.calls[callee.id]
  625. except KeyError:
  626. call = Call(callee.id)
  627. call[SAMPLES2] = cost
  628. caller.add_call(call)
  629. else:
  630. call[SAMPLES2] += cost
  631. callee = caller
  632. if False:
  633. profile.dump()
  634. # compute derived data
  635. profile.validate()
  636. profile.find_cycles()
  637. profile.ratio(TIME_RATIO, SAMPLES)
  638. profile.call_ratios(SAMPLES2)
  639. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  640. return profile
  641. class LineParser(Parser):
  642. """Base class for parsers that read line-based formats."""
  643. def __init__(self, stream):
  644. Parser.__init__(self)
  645. self._stream = stream
  646. self.__line = None
  647. self.__eof = False
  648. self.line_no = 0
  649. def readline(self):
  650. line = self._stream.readline()
  651. if not line:
  652. self.__line = ''
  653. self.__eof = True
  654. else:
  655. self.line_no += 1
  656. line = line.rstrip('\r\n')
  657. if not PYTHON_3:
  658. encoding = self._stream.encoding
  659. if encoding is None:
  660. encoding = locale.getpreferredencoding()
  661. line = line.decode(encoding)
  662. self.__line = line
  663. def lookahead(self):
  664. assert self.__line is not None
  665. return self.__line
  666. def consume(self):
  667. assert self.__line is not None
  668. line = self.__line
  669. self.readline()
  670. return line
  671. def eof(self):
  672. assert self.__line is not None
  673. return self.__eof
  674. XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF = range(4)
  675. class XmlToken:
  676. def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
  677. assert type in (XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF)
  678. self.type = type
  679. self.name_or_data = name_or_data
  680. self.attrs = attrs
  681. self.line = line
  682. self.column = column
  683. def __str__(self):
  684. if self.type == XML_ELEMENT_START:
  685. return '<' + self.name_or_data + ' ...>'
  686. if self.type == XML_ELEMENT_END:
  687. return '</' + self.name_or_data + '>'
  688. if self.type == XML_CHARACTER_DATA:
  689. return self.name_or_data
  690. if self.type == XML_EOF:
  691. return 'end of file'
  692. assert 0
  693. class XmlTokenizer:
  694. """Expat based XML tokenizer."""
  695. def __init__(self, fp, skip_ws = True):
  696. self.fp = fp
  697. self.tokens = []
  698. self.index = 0
  699. self.final = False
  700. self.skip_ws = skip_ws
  701. self.character_pos = 0, 0
  702. self.character_data = ''
  703. self.parser = xml.parsers.expat.ParserCreate()
  704. self.parser.StartElementHandler = self.handle_element_start
  705. self.parser.EndElementHandler = self.handle_element_end
  706. self.parser.CharacterDataHandler = self.handle_character_data
  707. def handle_element_start(self, name, attributes):
  708. self.finish_character_data()
  709. line, column = self.pos()
  710. token = XmlToken(XML_ELEMENT_START, name, attributes, line, column)
  711. self.tokens.append(token)
  712. def handle_element_end(self, name):
  713. self.finish_character_data()
  714. line, column = self.pos()
  715. token = XmlToken(XML_ELEMENT_END, name, None, line, column)
  716. self.tokens.append(token)
  717. def handle_character_data(self, data):
  718. if not self.character_data:
  719. self.character_pos = self.pos()
  720. self.character_data += data
  721. def finish_character_data(self):
  722. if self.character_data:
  723. if not self.skip_ws or not self.character_data.isspace():
  724. line, column = self.character_pos
  725. token = XmlToken(XML_CHARACTER_DATA, self.character_data, None, line, column)
  726. self.tokens.append(token)
  727. self.character_data = ''
  728. def next(self):
  729. size = 16*1024
  730. while self.index >= len(self.tokens) and not self.final:
  731. self.tokens = []
  732. self.index = 0
  733. data = self.fp.read(size)
  734. self.final = len(data) < size
  735. self.parser.Parse(data, self.final)
  736. if self.index >= len(self.tokens):
  737. line, column = self.pos()
  738. token = XmlToken(XML_EOF, None, None, line, column)
  739. else:
  740. token = self.tokens[self.index]
  741. self.index += 1
  742. return token
  743. def pos(self):
  744. return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
  745. class XmlTokenMismatch(Exception):
  746. def __init__(self, expected, found):
  747. Exception.__init__(self)
  748. self.expected = expected
  749. self.found = found
  750. def __str__(self):
  751. return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
  752. class XmlParser(Parser):
  753. """Base XML document parser."""
  754. def __init__(self, fp):
  755. Parser.__init__(self)
  756. self.tokenizer = XmlTokenizer(fp)
  757. self.consume()
  758. def consume(self):
  759. self.token = self.tokenizer.next()
  760. def match_element_start(self, name):
  761. return self.token.type == XML_ELEMENT_START and self.token.name_or_data == name
  762. def match_element_end(self, name):
  763. return self.token.type == XML_ELEMENT_END and self.token.name_or_data == name
  764. def element_start(self, name):
  765. while self.token.type == XML_CHARACTER_DATA:
  766. self.consume()
  767. if self.token.type != XML_ELEMENT_START:
  768. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
  769. if self.token.name_or_data != name:
  770. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
  771. attrs = self.token.attrs
  772. self.consume()
  773. return attrs
  774. def element_end(self, name):
  775. while self.token.type == XML_CHARACTER_DATA:
  776. self.consume()
  777. if self.token.type != XML_ELEMENT_END:
  778. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
  779. if self.token.name_or_data != name:
  780. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
  781. self.consume()
  782. def character_data(self, strip = True):
  783. data = ''
  784. while self.token.type == XML_CHARACTER_DATA:
  785. data += self.token.name_or_data
  786. self.consume()
  787. if strip:
  788. data = data.strip()
  789. return data
  790. class GprofParser(Parser):
  791. """Parser for GNU gprof output.
  792. See also:
  793. - Chapter "Interpreting gprof's Output" from the GNU gprof manual
  794. http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph
  795. - File "cg_print.c" from the GNU gprof source code
  796. http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src
  797. """
  798. def __init__(self, fp):
  799. Parser.__init__(self)
  800. self.fp = fp
  801. self.functions = {}
  802. self.cycles = {}
  803. def readline(self):
  804. line = self.fp.readline()
  805. if not line:
  806. sys.stderr.write('error: unexpected end of file\n')
  807. sys.exit(1)
  808. line = line.rstrip('\r\n')
  809. return line
  810. _int_re = re.compile(r'^\d+$')
  811. _float_re = re.compile(r'^\d+\.\d+$')
  812. def translate(self, mo):
  813. """Extract a structure from a match object, while translating the types in the process."""
  814. attrs = {}
  815. groupdict = mo.groupdict()
  816. for name, value in compat_iteritems(groupdict):
  817. if value is None:
  818. value = None
  819. elif self._int_re.match(value):
  820. value = int(value)
  821. elif self._float_re.match(value):
  822. value = float(value)
  823. attrs[name] = (value)
  824. return Struct(attrs)
  825. _cg_header_re = re.compile(
  826. # original gprof header
  827. r'^\s+called/total\s+parents\s*$|' +
  828. r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' +
  829. r'^\s+called/total\s+children\s*$|' +
  830. # GNU gprof header
  831. r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$'
  832. )
  833. _cg_ignore_re = re.compile(
  834. # spontaneous
  835. r'^\s+<spontaneous>\s*$|'
  836. # internal calls (such as "mcount")
  837. r'^.*\((\d+)\)$'
  838. )
  839. _cg_primary_re = re.compile(
  840. r'^\[(?P<index>\d+)\]?' +
  841. r'\s+(?P<percentage_time>\d+\.\d+)' +
  842. r'\s+(?P<self>\d+\.\d+)' +
  843. r'\s+(?P<descendants>\d+\.\d+)' +
  844. r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
  845. r'\s+(?P<name>\S.*?)' +
  846. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  847. r'\s\[(\d+)\]$'
  848. )
  849. _cg_parent_re = re.compile(
  850. r'^\s+(?P<self>\d+\.\d+)?' +
  851. r'\s+(?P<descendants>\d+\.\d+)?' +
  852. r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' +
  853. r'\s+(?P<name>\S.*?)' +
  854. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  855. r'\s\[(?P<index>\d+)\]$'
  856. )
  857. _cg_child_re = _cg_parent_re
  858. _cg_cycle_header_re = re.compile(
  859. r'^\[(?P<index>\d+)\]?' +
  860. r'\s+(?P<percentage_time>\d+\.\d+)' +
  861. r'\s+(?P<self>\d+\.\d+)' +
  862. r'\s+(?P<descendants>\d+\.\d+)' +
  863. r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
  864. r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' +
  865. r'\s\[(\d+)\]$'
  866. )
  867. _cg_cycle_member_re = re.compile(
  868. r'^\s+(?P<self>\d+\.\d+)?' +
  869. r'\s+(?P<descendants>\d+\.\d+)?' +
  870. r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' +
  871. r'\s+(?P<name>\S.*?)' +
  872. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  873. r'\s\[(?P<index>\d+)\]$'
  874. )
  875. _cg_sep_re = re.compile(r'^--+$')
  876. def parse_function_entry(self, lines):
  877. parents = []
  878. children = []
  879. while True:
  880. if not lines:
  881. sys.stderr.write('warning: unexpected end of entry\n')
  882. line = lines.pop(0)
  883. if line.startswith('['):
  884. break
  885. # read function parent line
  886. mo = self._cg_parent_re.match(line)
  887. if not mo:
  888. if self._cg_ignore_re.match(line):
  889. continue
  890. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  891. else:
  892. parent = self.translate(mo)
  893. parents.append(parent)
  894. # read primary line
  895. mo = self._cg_primary_re.match(line)
  896. if not mo:
  897. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  898. return
  899. else:
  900. function = self.translate(mo)
  901. while lines:
  902. line = lines.pop(0)
  903. # read function subroutine line
  904. mo = self._cg_child_re.match(line)
  905. if not mo:
  906. if self._cg_ignore_re.match(line):
  907. continue
  908. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  909. else:
  910. child = self.translate(mo)
  911. children.append(child)
  912. function.parents = parents
  913. function.children = children
  914. self.functions[function.index] = function
  915. def parse_cycle_entry(self, lines):
  916. # read cycle header line
  917. line = lines[0]
  918. mo = self._cg_cycle_header_re.match(line)
  919. if not mo:
  920. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  921. return
  922. cycle = self.translate(mo)
  923. # read cycle member lines
  924. cycle.functions = []
  925. for line in lines[1:]:
  926. mo = self._cg_cycle_member_re.match(line)
  927. if not mo:
  928. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  929. continue
  930. call = self.translate(mo)
  931. cycle.functions.append(call)
  932. self.cycles[cycle.cycle] = cycle
  933. def parse_cg_entry(self, lines):
  934. if lines[0].startswith("["):
  935. self.parse_cycle_entry(lines)
  936. else:
  937. self.parse_function_entry(lines)
  938. def parse_cg(self):
  939. """Parse the call graph."""
  940. # skip call graph header
  941. while not self._cg_header_re.match(self.readline()):
  942. pass
  943. line = self.readline()
  944. while self._cg_header_re.match(line):
  945. line = self.readline()
  946. # process call graph entries
  947. entry_lines = []
  948. while line != '\014': # form feed
  949. if line and not line.isspace():
  950. if self._cg_sep_re.match(line):
  951. self.parse_cg_entry(entry_lines)
  952. entry_lines = []
  953. else:
  954. entry_lines.append(line)
  955. line = self.readline()
  956. def parse(self):
  957. self.parse_cg()
  958. self.fp.close()
  959. profile = Profile()
  960. profile[TIME] = 0.0
  961. cycles = {}
  962. for index in self.cycles:
  963. cycles[index] = Cycle()
  964. for entry in compat_itervalues(self.functions):
  965. # populate the function
  966. function = Function(entry.index, entry.name)
  967. function[TIME] = entry.self
  968. if entry.called is not None:
  969. function.called = entry.called
  970. if entry.called_self is not None:
  971. call = Call(entry.index)
  972. call[CALLS] = entry.called_self
  973. function.called += entry.called_self
  974. # populate the function calls
  975. for child in entry.children:
  976. call = Call(child.index)
  977. assert child.called is not None
  978. call[CALLS] = child.called
  979. if child.index not in self.functions:
  980. # NOTE: functions that were never called but were discovered by gprof's
  981. # static call graph analysis dont have a call graph entry so we need
  982. # to add them here
  983. missing = Function(child.index, child.name)
  984. function[TIME] = 0.0
  985. function.called = 0
  986. profile.add_function(missing)
  987. function.add_call(call)
  988. profile.add_function(function)
  989. if entry.cycle is not None:
  990. try:
  991. cycle = cycles[entry.cycle]
  992. except KeyError:
  993. sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle)
  994. cycle = Cycle()
  995. cycles[entry.cycle] = cycle
  996. cycle.add_function(function)
  997. profile[TIME] = profile[TIME] + function[TIME]
  998. for cycle in compat_itervalues(cycles):
  999. profile.add_cycle(cycle)
  1000. # Compute derived events
  1001. profile.validate()
  1002. profile.ratio(TIME_RATIO, TIME)
  1003. profile.call_ratios(CALLS)
  1004. profile.integrate(TOTAL_TIME, TIME)
  1005. profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
  1006. return profile
  1007. # Clone&hack of GprofParser for VTune Amplifier XE 2013 gprof-cc output.
  1008. # Tested only with AXE 2013 for Windows.
  1009. # - Use total times as reported by AXE.
  1010. # - In the absence of call counts, call ratios are faked from the relative
  1011. # proportions of total time. This affects only the weighting of the calls.
  1012. # - Different header, separator, and end marker.
  1013. # - Extra whitespace after function names.
  1014. # - You get a full entry for <spontaneous>, which does not have parents.
  1015. # - Cycles do have parents. These are saved but unused (as they are
  1016. # for functions).
  1017. # - Disambiguated "unrecognized call graph entry" error messages.
  1018. # Notes:
  1019. # - Total time of functions as reported by AXE passes the val3 test.
  1020. # - CPU Time:Children in the input is sometimes a negative number. This
  1021. # value goes to the variable descendants, which is unused.
  1022. # - The format of gprof-cc reports is unaffected by the use of
  1023. # -knob enable-call-counts=true (no call counts, ever), or
  1024. # -show-as=samples (results are quoted in seconds regardless).
  1025. class AXEParser(Parser):
  1026. "Parser for VTune Amplifier XE 2013 gprof-cc report output."
  1027. def __init__(self, fp):
  1028. Parser.__init__(self)
  1029. self.fp = fp
  1030. self.functions = {}
  1031. self.cycles = {}
  1032. def readline(self):
  1033. line = self.fp.readline()
  1034. if not line:
  1035. sys.stderr.write('error: unexpected end of file\n')
  1036. sys.exit(1)
  1037. line = line.rstrip('\r\n')
  1038. return line
  1039. _int_re = re.compile(r'^\d+$')
  1040. _float_re = re.compile(r'^\d+\.\d+$')
  1041. def translate(self, mo):
  1042. """Extract a structure from a match object, while translating the types in the process."""
  1043. attrs = {}
  1044. groupdict = mo.groupdict()
  1045. for name, value in compat_iteritems(groupdict):
  1046. if value is None:
  1047. value = None
  1048. elif self._int_re.match(value):
  1049. value = int(value)
  1050. elif self._float_re.match(value):
  1051. value = float(value)
  1052. attrs[name] = (value)
  1053. return Struct(attrs)
  1054. _cg_header_re = re.compile(
  1055. '^Index |'
  1056. '^-----+ '
  1057. )
  1058. _cg_footer_re = re.compile(r'^Index\s+Function\s*$')
  1059. _cg_primary_re = re.compile(
  1060. r'^\[(?P<index>\d+)\]?' +
  1061. r'\s+(?P<percentage_time>\d+\.\d+)' +
  1062. r'\s+(?P<self>\d+\.\d+)' +
  1063. r'\s+(?P<descendants>\d+\.\d+)' +
  1064. r'\s+(?P<name>\S.*?)' +
  1065. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  1066. r'\s+\[(\d+)\]$'
  1067. )
  1068. _cg_parent_re = re.compile(
  1069. r'^\s+(?P<self>\d+\.\d+)?' +
  1070. r'\s+(?P<descendants>\d+\.\d+)?' +
  1071. r'\s+(?P<name>\S.*?)' +
  1072. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  1073. r'\s+\[(?P<index>\d+)\]$'
  1074. )
  1075. _cg_child_re = _cg_parent_re
  1076. _cg_cycle_header_re = re.compile(
  1077. r'^\[(?P<index>\d+)\]?' +
  1078. r'\s+(?P<percentage_time>\d+\.\d+)' +
  1079. r'\s+(?P<self>\d+\.\d+)' +
  1080. r'\s+(?P<descendants>\d+\.\d+)' +
  1081. r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' +
  1082. r'\s+\[(\d+)\]$'
  1083. )
  1084. _cg_cycle_member_re = re.compile(
  1085. r'^\s+(?P<self>\d+\.\d+)?' +
  1086. r'\s+(?P<descendants>\d+\.\d+)?' +
  1087. r'\s+(?P<name>\S.*?)' +
  1088. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  1089. r'\s+\[(?P<index>\d+)\]$'
  1090. )
  1091. def parse_function_entry(self, lines):
  1092. parents = []
  1093. children = []
  1094. while True:
  1095. if not lines:
  1096. sys.stderr.write('warning: unexpected end of entry\n')
  1097. return
  1098. line = lines.pop(0)
  1099. if line.startswith('['):
  1100. break
  1101. # read function parent line
  1102. mo = self._cg_parent_re.match(line)
  1103. if not mo:
  1104. sys.stderr.write('warning: unrecognized call graph entry (1): %r\n' % line)
  1105. else:
  1106. parent = self.translate(mo)
  1107. if parent.name != '<spontaneous>':
  1108. parents.append(parent)
  1109. # read primary line
  1110. mo = self._cg_primary_re.match(line)
  1111. if not mo:
  1112. sys.stderr.write('warning: unrecognized call graph entry (2): %r\n' % line)
  1113. return
  1114. else:
  1115. function = self.translate(mo)
  1116. while lines:
  1117. line = lines.pop(0)
  1118. # read function subroutine line
  1119. mo = self._cg_child_re.match(line)
  1120. if not mo:
  1121. sys.stderr.write('warning: unrecognized call graph entry (3): %r\n' % line)
  1122. else:
  1123. child = self.translate(mo)
  1124. if child.name != '<spontaneous>':
  1125. children.append(child)
  1126. if function.name != '<spontaneous>':
  1127. function.parents = parents
  1128. function.children = children
  1129. self.functions[function.index] = function
  1130. def parse_cycle_entry(self, lines):
  1131. # Process the parents that were not there in gprof format.
  1132. parents = []
  1133. while True:
  1134. if not lines:
  1135. sys.stderr.write('warning: unexpected end of cycle entry\n')
  1136. return
  1137. line = lines.pop(0)
  1138. if line.startswith('['):
  1139. break
  1140. mo = self._cg_parent_re.match(line)
  1141. if not mo:
  1142. sys.stderr.write('warning: unrecognized call graph entry (6): %r\n' % line)
  1143. else:
  1144. parent = self.translate(mo)
  1145. if parent.name != '<spontaneous>':
  1146. parents.append(parent)
  1147. # read cycle header line
  1148. mo = self._cg_cycle_header_re.match(line)
  1149. if not mo:
  1150. sys.stderr.write('warning: unrecognized call graph entry (4): %r\n' % line)
  1151. return
  1152. cycle = self.translate(mo)
  1153. # read cycle member lines
  1154. cycle.functions = []
  1155. for line in lines[1:]:
  1156. mo = self._cg_cycle_member_re.match(line)
  1157. if not mo:
  1158. sys.stderr.write('warning: unrecognized call graph entry (5): %r\n' % line)
  1159. continue
  1160. call = self.translate(mo)
  1161. cycle.functions.append(call)
  1162. cycle.parents = parents
  1163. self.cycles[cycle.cycle] = cycle
  1164. def parse_cg_entry(self, lines):
  1165. if any("as a whole" in linelooper for linelooper in lines):
  1166. self.parse_cycle_entry(lines)
  1167. else:
  1168. self.parse_function_entry(lines)
  1169. def parse_cg(self):
  1170. """Parse the call graph."""
  1171. # skip call graph header
  1172. line = self.readline()
  1173. while self._cg_header_re.match(line):
  1174. line = self.readline()
  1175. # process call graph entries
  1176. entry_lines = []
  1177. # An EOF in readline terminates the program without returning.
  1178. while not self._cg_footer_re.match(line):
  1179. if line.isspace():
  1180. self.parse_cg_entry(entry_lines)
  1181. entry_lines = []
  1182. else:
  1183. entry_lines.append(line)
  1184. line = self.readline()
  1185. def parse(self):
  1186. sys.stderr.write('warning: for axe format, edge weights are unreliable estimates derived from\nfunction total times.\n')
  1187. self.parse_cg()
  1188. self.fp.close()
  1189. profile = Profile()
  1190. profile[TIME] = 0.0
  1191. cycles = {}
  1192. for index in self.cycles:
  1193. cycles[index] = Cycle()
  1194. for entry in compat_itervalues(self.functions):
  1195. # populate the function
  1196. function = Function(entry.index, entry.name)
  1197. function[TIME] = entry.self
  1198. function[TOTAL_TIME_RATIO] = entry.percentage_time / 100.0
  1199. # populate the function calls
  1200. for child in entry.children:
  1201. call = Call(child.index)
  1202. # The following bogus value affects only the weighting of
  1203. # the calls.
  1204. call[TOTAL_TIME_RATIO] = function[TOTAL_TIME_RATIO]
  1205. if child.index not in self.functions:
  1206. # NOTE: functions that were never called but were discovered by gprof's
  1207. # static call graph analysis dont have a call graph entry so we need
  1208. # to add them here
  1209. # FIXME: Is this applicable?
  1210. missing = Function(child.index, child.name)
  1211. function[TIME] = 0.0
  1212. profile.add_function(missing)
  1213. function.add_call(call)
  1214. profile.add_function(function)
  1215. if entry.cycle is not None:
  1216. try:
  1217. cycle = cycles[entry.cycle]
  1218. except KeyError:
  1219. sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle)
  1220. cycle = Cycle()
  1221. cycles[entry.cycle] = cycle
  1222. cycle.add_function(function)
  1223. profile[TIME] = profile[TIME] + function[TIME]
  1224. for cycle in compat_itervalues(cycles):
  1225. profile.add_cycle(cycle)
  1226. # Compute derived events.
  1227. profile.validate()
  1228. profile.ratio(TIME_RATIO, TIME)
  1229. # Lacking call counts, fake call ratios based on total times.
  1230. profile.call_ratios(TOTAL_TIME_RATIO)
  1231. # The TOTAL_TIME_RATIO of functions is already set. Propagate that
  1232. # total time to the calls. (TOTAL_TIME is neither set nor used.)
  1233. for function in compat_itervalues(profile.functions):
  1234. for call in compat_itervalues(function.calls):
  1235. if call.ratio is not None:
  1236. callee = profile.functions[call.callee_id]
  1237. call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
  1238. return profile
  1239. class CallgrindParser(LineParser):
  1240. """Parser for valgrind's callgrind tool.
  1241. See also:
  1242. - http://valgrind.org/docs/manual/cl-format.html
  1243. """
  1244. _call_re = re.compile(r'^calls=\s*(\d+)\s+((\d+|\+\d+|-\d+|\*)\s+)+$')
  1245. def __init__(self, infile):
  1246. LineParser.__init__(self, infile)
  1247. # Textual positions
  1248. self.position_ids = {}
  1249. self.positions = {}
  1250. # Numeric positions
  1251. self.num_positions = 1
  1252. self.cost_positions = ['line']
  1253. self.last_positions = [0]
  1254. # Events
  1255. self.num_events = 0
  1256. self.cost_events = []
  1257. self.profile = Profile()
  1258. self.profile[SAMPLES] = 0
  1259. def parse(self):
  1260. # read lookahead
  1261. self.readline()
  1262. self.parse_key('version')
  1263. self.parse_key('creator')
  1264. while self.parse_part():
  1265. pass
  1266. if not self.eof():
  1267. sys.stderr.write('warning: line %u: unexpected line\n' % self.line_no)
  1268. sys.stderr.write('%s\n' % self.lookahead())
  1269. # compute derived data
  1270. self.profile.validate()
  1271. self.profile.find_cycles()
  1272. self.profile.ratio(TIME_RATIO, SAMPLES)
  1273. self.profile.call_ratios(SAMPLES2)
  1274. self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1275. return self.profile
  1276. def parse_part(self):
  1277. if not self.parse_header_line():
  1278. return False
  1279. while self.parse_header_line():
  1280. pass
  1281. if not self.parse_body_line():
  1282. return False
  1283. while self.parse_body_line():
  1284. pass
  1285. return True
  1286. def parse_header_line(self):
  1287. return \
  1288. self.parse_empty() or \
  1289. self.parse_comment() or \
  1290. self.parse_part_detail() or \
  1291. self.parse_description() or \
  1292. self.parse_event_specification() or \
  1293. self.parse_cost_line_def() or \
  1294. self.parse_cost_summary()
  1295. _detail_keys = set(('cmd', 'pid', 'thread', 'part'))
  1296. def parse_part_detail(self):
  1297. return self.parse_keys(self._detail_keys)
  1298. def parse_description(self):
  1299. return self.parse_key('desc') is not None
  1300. def parse_event_specification(self):
  1301. event = self.parse_key('event')
  1302. if event is None:
  1303. return False
  1304. return True
  1305. def parse_cost_line_def(self):
  1306. pair = self.parse_keys(('events', 'positions'))
  1307. if pair is None:
  1308. return False
  1309. key, value = pair
  1310. items = value.split()
  1311. if key == 'events':
  1312. self.num_events = len(items)
  1313. self.cost_events = items
  1314. if key == 'positions':
  1315. self.num_positions = len(items)
  1316. self.cost_positions = items
  1317. self.last_positions = [0]*self.num_positions
  1318. return True
  1319. def parse_cost_summary(self):
  1320. pair = self.parse_keys(('summary', 'totals'))
  1321. if pair is None:
  1322. return False
  1323. return True
  1324. def parse_body_line(self):
  1325. return \
  1326. self.parse_empty() or \
  1327. self.parse_comment() or \
  1328. self.parse_cost_line() or \
  1329. self.parse_position_spec() or \
  1330. self.parse_association_spec()
  1331. __subpos_re = r'(0x[0-9a-fA-F]+|\d+|\+\d+|-\d+|\*)'
  1332. _cost_re = re.compile(r'^' +
  1333. __subpos_re + r'( +' + __subpos_re + r')*' +
  1334. r'( +\d+)*' +
  1335. '$')
  1336. def parse_cost_line(self, calls=None):
  1337. line = self.lookahead().rstrip()
  1338. mo = self._cost_re.match(line)
  1339. if not mo:
  1340. return False
  1341. function = self.get_function()
  1342. if calls is None:
  1343. # Unlike other aspects, call object (cob) is relative not to the
  1344. # last call object, but to the caller's object (ob), so try to
  1345. # update it when processing a functions cost line
  1346. try:
  1347. self.positions['cob'] = self.positions['ob']
  1348. except KeyError:
  1349. pass
  1350. values = line.split()
  1351. assert len(values) <= self.num_positions + self.num_events
  1352. positions = values[0 : self.num_positions]
  1353. events = values[self.num_positions : ]
  1354. events += ['0']*(self.num_events - len(events))
  1355. for i in range(self.num_positions):
  1356. position = positions[i]
  1357. if position == '*':
  1358. position = self.last_positions[i]
  1359. elif position[0] in '-+':
  1360. position = self.last_positions[i] + int(position)
  1361. elif position.startswith('0x'):
  1362. position = int(position, 16)
  1363. else:
  1364. position = int(position)
  1365. self.last_positions[i] = position
  1366. events = [float(event) for event in events]
  1367. if calls is None:
  1368. function[SAMPLES] += events[0]
  1369. self.profile[SAMPLES] += events[0]
  1370. else:
  1371. callee = self.get_callee()
  1372. callee.called += calls
  1373. try:
  1374. call = function.calls[callee.id]
  1375. except KeyError:
  1376. call = Call(callee.id)
  1377. call[CALLS] = calls
  1378. call[SAMPLES2] = events[0]
  1379. function.add_call(call)
  1380. else:
  1381. call[CALLS] += calls
  1382. call[SAMPLES2] += events[0]
  1383. self.consume()
  1384. return True
  1385. def parse_association_spec(self):
  1386. line = self.lookahead()
  1387. if not line.startswith('calls='):
  1388. return False
  1389. _, values = line.split('=', 1)
  1390. values = values.strip().split()
  1391. calls = int(values[0])
  1392. call_position = values[1:]
  1393. self.consume()
  1394. self.parse_cost_line(calls)
  1395. return True
  1396. _position_re = re.compile(r'^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?')
  1397. _position_table_map = {
  1398. 'ob': 'ob',
  1399. 'fl': 'fl',
  1400. 'fi': 'fl',
  1401. 'fe': 'fl',
  1402. 'fn': 'fn',
  1403. 'cob': 'ob',
  1404. 'cfl': 'fl',
  1405. 'cfi': 'fl',
  1406. 'cfe': 'fl',
  1407. 'cfn': 'fn',
  1408. 'jfi': 'fl',
  1409. }
  1410. _position_map = {
  1411. 'ob': 'ob',
  1412. 'fl': 'fl',
  1413. 'fi': 'fl',
  1414. 'fe': 'fl',
  1415. 'fn': 'fn',
  1416. 'cob': 'cob',
  1417. 'cfl': 'cfl',
  1418. 'cfi': 'cfl',
  1419. 'cfe': 'cfl',
  1420. 'cfn': 'cfn',
  1421. 'jfi': 'jfi',
  1422. }
  1423. def parse_position_spec(self):
  1424. line = self.lookahead()
  1425. if line.startswith('jump=') or line.startswith('jcnd='):
  1426. self.consume()
  1427. return True
  1428. mo = self._position_re.match(line)
  1429. if not mo:
  1430. return False
  1431. position, id, name = mo.groups()
  1432. if id:
  1433. table = self._position_table_map[position]
  1434. if name:
  1435. self.position_ids[(table, id)] = name
  1436. else:
  1437. name = self.position_ids.get((table, id), '')
  1438. self.positions[self._position_map[position]] = name
  1439. self.consume()
  1440. return True
  1441. def parse_empty(self):
  1442. if self.eof():
  1443. return False
  1444. line = self.lookahead()
  1445. if line.strip():
  1446. return False
  1447. self.consume()
  1448. return True
  1449. def parse_comment(self):
  1450. line = self.lookahead()
  1451. if not line.startswith('#'):
  1452. return False
  1453. self.consume()
  1454. return True
  1455. _key_re = re.compile(r'^(\w+):')
  1456. def parse_key(self, key):
  1457. pair = self.parse_keys((key,))
  1458. if not pair:
  1459. return None
  1460. key, value = pair
  1461. return value
  1462. def parse_keys(self, keys):
  1463. line = self.lookahead()
  1464. mo = self._key_re.match(line)
  1465. if not mo:
  1466. return None
  1467. key, value = line.split(':', 1)
  1468. if key not in keys:
  1469. return None
  1470. value = value.strip()
  1471. self.consume()
  1472. return key, value
  1473. def make_function(self, module, filename, name):
  1474. # FIXME: module and filename are not being tracked reliably
  1475. #id = '|'.join((module, filename, name))
  1476. id = name
  1477. try:
  1478. function = self.profile.functions[id]
  1479. except KeyError:
  1480. function = Function(id, name)
  1481. if module:
  1482. function.module = os.path.basename(module)
  1483. function[SAMPLES] = 0
  1484. function.called = 0
  1485. self.profile.add_function(function)
  1486. return function
  1487. def get_function(self):
  1488. module = self.positions.get('ob', '')
  1489. filename = self.positions.get('fl', '')
  1490. function = self.positions.get('fn', '')
  1491. return self.make_function(module, filename, function)
  1492. def get_callee(self):
  1493. module = self.positions.get('cob', '')
  1494. filename = self.positions.get('cfi', '')
  1495. function = self.positions.get('cfn', '')
  1496. return self.make_function(module, filename, function)
  1497. class PerfParser(LineParser):
  1498. """Parser for linux perf callgraph output.
  1499. It expects output generated with
  1500. perf record -g
  1501. perf script | gprof2dot.py --format=perf
  1502. """
  1503. def __init__(self, infile):
  1504. LineParser.__init__(self, infile)
  1505. self.profile = Profile()
  1506. def readline(self):
  1507. # Override LineParser.readline to ignore comment lines
  1508. while True:
  1509. LineParser.readline(self)
  1510. if self.eof() or not self.lookahead().startswith('#'):
  1511. break
  1512. def parse(self):
  1513. # read lookahead
  1514. self.readline()
  1515. profile = self.profile
  1516. profile[SAMPLES] = 0
  1517. while not self.eof():
  1518. self.parse_event()
  1519. # compute derived data
  1520. profile.validate()
  1521. profile.find_cycles()
  1522. profile.ratio(TIME_RATIO, SAMPLES)
  1523. profile.call_ratios(SAMPLES2)
  1524. if totalMethod == "callratios":
  1525. # Heuristic approach. TOTAL_SAMPLES is unused.
  1526. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1527. elif totalMethod == "callstacks":
  1528. # Use the actual call chains for functions.
  1529. profile[TOTAL_SAMPLES] = profile[SAMPLES]
  1530. profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES)
  1531. # Then propagate that total time to the calls.
  1532. for function in compat_itervalues(profile.functions):
  1533. for call in compat_itervalues(function.calls):
  1534. if call.ratio is not None:
  1535. callee = profile.functions[call.callee_id]
  1536. call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO]
  1537. else:
  1538. assert False
  1539. return profile
  1540. def parse_event(self):
  1541. if self.eof():
  1542. return
  1543. line = self.consume()
  1544. assert line
  1545. callchain = self.parse_callchain()
  1546. if not callchain:
  1547. return
  1548. callee = callchain[0]
  1549. callee[SAMPLES] += 1
  1550. self.profile[SAMPLES] += 1
  1551. for caller in callchain[1:]:
  1552. try:
  1553. call = caller.calls[callee.id]
  1554. except KeyError:
  1555. call = Call(callee.id)
  1556. call[SAMPLES2] = 1
  1557. caller.add_call(call)
  1558. else:
  1559. call[SAMPLES2] += 1
  1560. callee = caller
  1561. # Increment TOTAL_SAMPLES only once on each function.
  1562. stack = set(callchain)
  1563. for function in stack:
  1564. function[TOTAL_SAMPLES] += 1
  1565. def parse_callchain(self):
  1566. callchain = []
  1567. while self.lookahead():
  1568. function = self.parse_call()
  1569. if function is None:
  1570. break
  1571. callchain.append(function)
  1572. if self.lookahead() == '':
  1573. self.consume()
  1574. return callchain
  1575. call_re = re.compile(r'^\s+(?P<address>[0-9a-fA-F]+)\s+(?P<symbol>.*)\s+\((?P<module>[^)]*)\)$')
  1576. def parse_call(self):
  1577. line = self.consume()
  1578. mo = self.call_re.match(line)
  1579. assert mo
  1580. if not mo:
  1581. return None
  1582. function_name = mo.group('symbol')
  1583. if not function_name:
  1584. function_name = mo.group('address')
  1585. module = mo.group('module')
  1586. function_id = function_name + ':' + module
  1587. try:
  1588. function = self.profile.functions[function_id]
  1589. except KeyError:
  1590. function = Function(function_id, function_name)
  1591. function.module = os.path.basename(module)
  1592. function[SAMPLES] = 0
  1593. function[TOTAL_SAMPLES] = 0
  1594. self.profile.add_function(function)
  1595. return function
  1596. class OprofileParser(LineParser):
  1597. """Parser for oprofile callgraph output.
  1598. See also:
  1599. - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph
  1600. """
  1601. _fields_re = {
  1602. 'samples': r'(\d+)',
  1603. '%': r'(\S+)',
  1604. 'linenr info': r'(?P<source>\(no location information\)|\S+:\d+)',
  1605. 'image name': r'(?P<image>\S+(?:\s\(tgid:[^)]*\))?)',
  1606. 'app name': r'(?P<application>\S+)',
  1607. 'symbol name': r'(?P<symbol>\(no symbols\)|.+?)',
  1608. }
  1609. def __init__(self, infile):
  1610. LineParser.__init__(self, infile)
  1611. self.entries = {}
  1612. self.entry_re = None
  1613. def add_entry(self, callers, function, callees):
  1614. try:
  1615. entry = self.entries[function.id]
  1616. except KeyError:
  1617. self.entries[function.id] = (callers, function, callees)
  1618. else:
  1619. callers_total, function_total, callees_total = entry
  1620. self.update_subentries_dict(callers_total, callers)
  1621. function_total.samples += function.samples
  1622. self.update_subentries_dict(callees_total, callees)
  1623. def update_subentries_dict(self, totals, partials):
  1624. for partial in compat_itervalues(partials):
  1625. try:
  1626. total = totals[partial.id]
  1627. except KeyError:
  1628. totals[partial.id] = partial
  1629. else:
  1630. total.samples += partial.samples
  1631. def parse(self):
  1632. # read lookahead
  1633. self.readline()
  1634. self.parse_header()
  1635. while self.lookahead():
  1636. self.parse_entry()
  1637. profile = Profile()
  1638. reverse_call_samples = {}
  1639. # populate the profile
  1640. profile[SAMPLES] = 0
  1641. for _callers, _function, _callees in compat_itervalues(self.entries):
  1642. function = Function(_function.id, _function.name)
  1643. function[SAMPLES] = _function.samples
  1644. profile.add_function(function)
  1645. profile[SAMPLES] += _function.samples
  1646. if _function.application:
  1647. function.process = os.path.basename(_function.application)
  1648. if _function.image:
  1649. function.module = os.path.basename(_function.image)
  1650. total_callee_samples = 0
  1651. for _callee in compat_itervalues(_callees):
  1652. total_callee_samples += _callee.samples
  1653. for _callee in compat_itervalues(_callees):
  1654. if not _callee.self:
  1655. call = Call(_callee.id)
  1656. call[SAMPLES2] = _callee.samples
  1657. function.add_call(call)
  1658. # compute derived data
  1659. profile.validate()
  1660. profile.find_cycles()
  1661. profile.ratio(TIME_RATIO, SAMPLES)
  1662. profile.call_ratios(SAMPLES2)
  1663. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1664. return profile
  1665. def parse_header(self):
  1666. while not self.match_header():
  1667. self.consume()
  1668. line = self.lookahead()
  1669. fields = re.split(r'\s\s+', line)
  1670. entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P<self>\s+\[self\])?$'
  1671. self.entry_re = re.compile(entry_re)
  1672. self.skip_separator()
  1673. def parse_entry(self):
  1674. callers = self.parse_subentries()
  1675. if self.match_primary():
  1676. function = self.parse_subentry()
  1677. if function is not None:
  1678. callees = self.parse_subentries()
  1679. self.add_entry(callers, function, callees)
  1680. self.skip_separator()
  1681. def parse_subentries(self):
  1682. subentries = {}
  1683. while self.match_secondary():
  1684. subentry = self.parse_subentry()
  1685. subentries[subentry.id] = subentry
  1686. return subentries
  1687. def parse_subentry(self):
  1688. entry = Struct()
  1689. line = self.consume()
  1690. mo = self.entry_re.match(line)
  1691. if not mo:
  1692. raise ParseError('failed to parse', line)
  1693. fields = mo.groupdict()
  1694. entry.samples = int(mo.group(1))
  1695. if 'source' in fields and fields['source'] != '(no location information)':
  1696. source = fields['source']
  1697. filename, lineno = source.split(':')
  1698. entry.filename = filename
  1699. entry.lineno = int(lineno)
  1700. else:
  1701. source = ''
  1702. entry.filename = None
  1703. entry.lineno = None
  1704. entry.image = fields.get('image', '')
  1705. entry.application = fields.get('application', '')
  1706. if 'symbol' in fields and fields['symbol'] != '(no symbols)':
  1707. entry.symbol = fields['symbol']
  1708. else:
  1709. entry.symbol = ''
  1710. if entry.symbol.startswith('"') and entry.symbol.endswith('"'):
  1711. entry.symbol = entry.symbol[1:-1]
  1712. entry.id = ':'.join((entry.application, entry.image, source, entry.symbol))
  1713. entry.self = fields.get('self', None) != None
  1714. if entry.self:
  1715. entry.id += ':self'
  1716. if entry.symbol:
  1717. entry.name = entry.symbol
  1718. else:
  1719. entry.name = entry.image
  1720. return entry
  1721. def skip_separator(self):
  1722. while not self.match_separator():
  1723. self.consume()
  1724. self.consume()
  1725. def match_header(self):
  1726. line = self.lookahead()
  1727. return line.startswith('samples')
  1728. def match_separator(self):
  1729. line = self.lookahead()
  1730. return line == '-'*len(line)
  1731. def match_primary(self):
  1732. line = self.lookahead()
  1733. return not line[:1].isspace()
  1734. def match_secondary(self):
  1735. line = self.lookahead()
  1736. return line[:1].isspace()
  1737. class HProfParser(LineParser):
  1738. """Parser for java hprof output
  1739. See also:
  1740. - http://java.sun.com/developer/technicalArticles/Programming/HPROF.html
  1741. """
  1742. trace_re = re.compile(r'\t(.*)\((.*):(.*)\)')
  1743. trace_id_re = re.compile(r'^TRACE (\d+):$')
  1744. def __init__(self, infile):
  1745. LineParser.__init__(self, infile)
  1746. self.traces = {}
  1747. self.samples = {}
  1748. def parse(self):
  1749. # read lookahead
  1750. self.readline()
  1751. while not self.lookahead().startswith('------'): self.consume()
  1752. while not self.lookahead().startswith('TRACE '): self.consume()
  1753. self.parse_traces()
  1754. while not self.lookahead().startswith('CPU'):
  1755. self.consume()
  1756. self.parse_samples()
  1757. # populate the profile
  1758. profile = Profile()
  1759. profile[SAMPLES] = 0
  1760. functions = {}
  1761. # build up callgraph
  1762. for id, trace in compat_iteritems(self.traces):
  1763. if not id in self.samples: continue
  1764. mtime = self.samples[id][0]
  1765. last = None
  1766. for func, file, line in trace:
  1767. if not func in functions:
  1768. function = Function(func, func)
  1769. function[SAMPLES] = 0
  1770. profile.add_function(function)
  1771. functions[func] = function
  1772. function = functions[func]
  1773. # allocate time to the deepest method in the trace
  1774. if not last:
  1775. function[SAMPLES] += mtime
  1776. profile[SAMPLES] += mtime
  1777. else:
  1778. c = function.get_call(last)
  1779. c[SAMPLES2] += mtime
  1780. last = func
  1781. # compute derived data
  1782. profile.validate()
  1783. profile.find_cycles()
  1784. profile.ratio(TIME_RATIO, SAMPLES)
  1785. profile.call_ratios(SAMPLES2)
  1786. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1787. return profile
  1788. def parse_traces(self):
  1789. while self.lookahead().startswith('TRACE '):
  1790. self.parse_trace()
  1791. def parse_trace(self):
  1792. l = self.consume()
  1793. mo = self.trace_id_re.match(l)
  1794. tid = mo.group(1)
  1795. last = None
  1796. trace = []
  1797. while self.lookahead().startswith('\t'):
  1798. l = self.consume()
  1799. match = self.trace_re.search(l)
  1800. if not match:
  1801. #sys.stderr.write('Invalid line: %s\n' % l)
  1802. break
  1803. else:
  1804. function_name, file, line = match.groups()
  1805. trace += [(function_name, file, line)]
  1806. self.traces[int(tid)] = trace
  1807. def parse_samples(self):
  1808. self.consume()
  1809. self.consume()
  1810. while not self.lookahead().startswith('CPU'):
  1811. rank, percent_self, percent_accum, count, traceid, method = self.lookahead().split()
  1812. self.samples[int(traceid)] = (int(count), method)
  1813. self.consume()
  1814. class SysprofParser(XmlParser):
  1815. def __init__(self, stream):
  1816. XmlParser.__init__(self, stream)
  1817. def parse(self):
  1818. objects = {}
  1819. nodes = {}
  1820. self.element_start('profile')
  1821. while self.token.type == XML_ELEMENT_START:
  1822. if self.token.name_or_data == 'objects':
  1823. assert not objects
  1824. objects = self.parse_items('objects')
  1825. elif self.token.name_or_data == 'nodes':
  1826. assert not nodes
  1827. nodes = self.parse_items('nodes')
  1828. else:
  1829. self.parse_value(self.token.name_or_data)
  1830. self.element_end('profile')
  1831. return self.build_profile(objects, nodes)
  1832. def parse_items(self, name):
  1833. assert name[-1] == 's'
  1834. items = {}
  1835. self.element_start(name)
  1836. while self.token.type == XML_ELEMENT_START:
  1837. id, values = self.parse_item(name[:-1])
  1838. assert id not in items
  1839. items[id] = values
  1840. self.element_end(name)
  1841. return items
  1842. def parse_item(self, name):
  1843. attrs = self.element_start(name)
  1844. id = int(attrs['id'])
  1845. values = self.parse_values()
  1846. self.element_end(name)
  1847. return id, values
  1848. def parse_values(self):
  1849. values = {}
  1850. while self.token.type == XML_ELEMENT_START:
  1851. name = self.token.name_or_data
  1852. value = self.parse_value(name)
  1853. assert name not in values
  1854. values[name] = value
  1855. return values
  1856. def parse_value(self, tag):
  1857. self.element_start(tag)
  1858. value = self.character_data()
  1859. self.element_end(tag)
  1860. if value.isdigit():
  1861. return int(value)
  1862. if value.startswith('"') and value.endswith('"'):
  1863. return value[1:-1]
  1864. return value
  1865. def build_profile(self, objects, nodes):
  1866. profile = Profile()
  1867. profile[SAMPLES] = 0
  1868. for id, object in compat_iteritems(objects):
  1869. # Ignore fake objects (process names, modules, "Everything", "kernel", etc.)
  1870. if object['self'] == 0:
  1871. continue
  1872. function = Function(id, object['name'])
  1873. function[SAMPLES] = object['self']
  1874. profile.add_function(function)
  1875. profile[SAMPLES] += function[SAMPLES]
  1876. for id, node in compat_iteritems(nodes):
  1877. # Ignore fake calls
  1878. if node['self'] == 0:
  1879. continue
  1880. # Find a non-ignored parent
  1881. parent_id = node['parent']
  1882. while parent_id != 0:
  1883. parent = nodes[parent_id]
  1884. caller_id = parent['object']
  1885. if objects[caller_id]['self'] != 0:
  1886. break
  1887. parent_id = parent['parent']
  1888. if parent_id == 0:
  1889. continue
  1890. callee_id = node['object']
  1891. assert objects[caller_id]['self']
  1892. assert objects[callee_id]['self']
  1893. function = profile.functions[caller_id]
  1894. samples = node['self']
  1895. try:
  1896. call = function.calls[callee_id]
  1897. except KeyError:
  1898. call = Call(callee_id)
  1899. call[SAMPLES2] = samples
  1900. function.add_call(call)
  1901. else:
  1902. call[SAMPLES2] += samples
  1903. # Compute derived events
  1904. profile.validate()
  1905. profile.find_cycles()
  1906. profile.ratio(TIME_RATIO, SAMPLES)
  1907. profile.call_ratios(SAMPLES2)
  1908. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1909. return profile
  1910. class XPerfParser(Parser):
  1911. """Parser for CSVs generted by XPerf, from Microsoft Windows Performance Tools.
  1912. """
  1913. def __init__(self, stream):
  1914. Parser.__init__(self)
  1915. self.stream = stream
  1916. self.profile = Profile()
  1917. self.profile[SAMPLES] = 0
  1918. self.column = {}
  1919. def parse(self):
  1920. import csv
  1921. reader = csv.reader(
  1922. self.stream,
  1923. delimiter = ',',
  1924. quotechar = None,
  1925. escapechar = None,
  1926. doublequote = False,
  1927. skipinitialspace = True,
  1928. lineterminator = '\r\n',
  1929. quoting = csv.QUOTE_NONE)
  1930. header = True
  1931. for row in reader:
  1932. if header:
  1933. self.parse_header(row)
  1934. header = False
  1935. else:
  1936. self.parse_row(row)
  1937. # compute derived data
  1938. self.profile.validate()
  1939. self.profile.find_cycles()
  1940. self.profile.ratio(TIME_RATIO, SAMPLES)
  1941. self.profile.call_ratios(SAMPLES2)
  1942. self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1943. return self.profile
  1944. def parse_header(self, row):
  1945. for column in range(len(row)):
  1946. name = row[column]
  1947. assert name not in self.column
  1948. self.column[name] = column
  1949. def parse_row(self, row):
  1950. fields = {}
  1951. for name, column in compat_iteritems(self.column):
  1952. value = row[column]
  1953. for factory in int, float:
  1954. try:
  1955. value = factory(value)
  1956. except ValueError:
  1957. pass
  1958. else:
  1959. break
  1960. fields[name] = value
  1961. process = fields['Process Name']
  1962. symbol = fields['Module'] + '!' + fields['Function']
  1963. weight = fields['Weight']
  1964. count = fields['Count']
  1965. if process == 'Idle':
  1966. return
  1967. function = self.get_function(process, symbol)
  1968. function[SAMPLES] += weight * count
  1969. self.profile[SAMPLES] += weight * count
  1970. stack = fields['Stack']
  1971. if stack != '?':
  1972. stack = stack.split('/')
  1973. assert stack[0] == '[Root]'
  1974. if stack[-1] != symbol:
  1975. # XXX: some cases the sampled function does not appear in the stack
  1976. stack.append(symbol)
  1977. caller = None
  1978. for symbol in stack[1:]:
  1979. callee = self.get_function(process, symbol)
  1980. if caller is not None:
  1981. try:
  1982. call = caller.calls[callee.id]
  1983. except KeyError:
  1984. call = Call(callee.id)
  1985. call[SAMPLES2] = count
  1986. caller.add_call(call)
  1987. else:
  1988. call[SAMPLES2] += count
  1989. caller = callee
  1990. def get_function(self, process, symbol):
  1991. function_id = process + '!' + symbol
  1992. try:
  1993. function = self.profile.functions[function_id]
  1994. except KeyError:
  1995. module, name = symbol.split('!', 1)
  1996. function = Function(function_id, name)
  1997. function.process = process
  1998. function.module = module
  1999. function[SAMPLES] = 0
  2000. self.profile.add_function(function)
  2001. return function
  2002. class SleepyParser(Parser):
  2003. """Parser for GNU gprof output.
  2004. See also:
  2005. - http://www.codersnotes.com/sleepy/
  2006. - http://sleepygraph.sourceforge.net/
  2007. """
  2008. stdinInput = False
  2009. def __init__(self, filename):
  2010. Parser.__init__(self)
  2011. from zipfile import ZipFile
  2012. self.database = ZipFile(filename)
  2013. self.symbols = {}
  2014. self.calls = {}
  2015. self.profile = Profile()
  2016. _symbol_re = re.compile(
  2017. r'^(?P<id>\w+)' +
  2018. r'\s+"(?P<module>[^"]*)"' +
  2019. r'\s+"(?P<procname>[^"]*)"' +
  2020. r'\s+"(?P<sourcefile>[^"]*)"' +
  2021. r'\s+(?P<sourceline>\d+)$'
  2022. )
  2023. def openEntry(self, name):
  2024. # Some versions of verysleepy use lowercase filenames
  2025. for database_name in self.database.namelist():
  2026. if name.lower() == database_name.lower():
  2027. name = database_name
  2028. break
  2029. return self.database.open(name, 'rU')
  2030. def parse_symbols(self):
  2031. for line in self.openEntry('Symbols.txt'):
  2032. line = line.decode('UTF-8')
  2033. mo = self._symbol_re.match(line)
  2034. if mo:
  2035. symbol_id, module, procname, sourcefile, sourceline = mo.groups()
  2036. function_id = ':'.join([module, procname])
  2037. try:
  2038. function = self.profile.functions[function_id]
  2039. except KeyError:
  2040. function = Function(function_id, procname)
  2041. function.module = module
  2042. function[SAMPLES] = 0
  2043. self.profile.add_function(function)
  2044. self.symbols[symbol_id] = function
  2045. def parse_callstacks(self):
  2046. for line in self.openEntry('Callstacks.txt'):
  2047. line = line.decode('UTF-8')
  2048. fields = line.split()
  2049. samples = float(fields[0])
  2050. callstack = fields[1:]
  2051. callstack = [self.symbols[symbol_id] for symbol_id in callstack]
  2052. callee = callstack[0]
  2053. callee[SAMPLES] += samples
  2054. self.profile[SAMPLES] += samples
  2055. for caller in callstack[1:]:
  2056. try:
  2057. call = caller.calls[callee.id]
  2058. except KeyError:
  2059. call = Call(callee.id)
  2060. call[SAMPLES2] = samples
  2061. caller.add_call(call)
  2062. else:
  2063. call[SAMPLES2] += samples
  2064. callee = caller
  2065. def parse(self):
  2066. profile = self.profile
  2067. profile[SAMPLES] = 0
  2068. self.parse_symbols()
  2069. self.parse_callstacks()
  2070. # Compute derived events
  2071. profile.validate()
  2072. profile.find_cycles()
  2073. profile.ratio(TIME_RATIO, SAMPLES)
  2074. profile.call_ratios(SAMPLES2)
  2075. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  2076. return profile
  2077. class PstatsParser:
  2078. """Parser python profiling statistics saved with te pstats module."""
  2079. stdinInput = False
  2080. multipleInput = True
  2081. def __init__(self, *filename):
  2082. import pstats
  2083. try:
  2084. self.stats = pstats.Stats(*filename)
  2085. except ValueError:
  2086. if PYTHON_3:
  2087. sys.stderr.write('error: failed to load %s\n' % ', '.join(filename))
  2088. sys.exit(1)
  2089. import hotshot.stats
  2090. self.stats = hotshot.stats.load(filename[0])
  2091. self.profile = Profile()
  2092. self.function_ids = {}
  2093. def get_function_name(self, key):
  2094. filename, line, name = key
  2095. module = os.path.splitext(filename)[0]
  2096. module = os.path.basename(module)
  2097. return "%s:%d:%s" % (module, line, name)
  2098. def get_function(self, key):
  2099. try:
  2100. id = self.function_ids[key]
  2101. except KeyError:
  2102. id = len(self.function_ids)
  2103. name = self.get_function_name(key)
  2104. function = Function(id, name)
  2105. self.profile.functions[id] = function
  2106. self.function_ids[key] = id
  2107. else:
  2108. function = self.profile.functions[id]
  2109. return function
  2110. def parse(self):
  2111. self.profile[TIME] = 0.0
  2112. self.profile[TOTAL_TIME] = self.stats.total_tt
  2113. for fn, (cc, nc, tt, ct, callers) in compat_iteritems(self.stats.stats):
  2114. callee = self.get_function(fn)
  2115. callee.called = nc
  2116. callee[TOTAL_TIME] = ct
  2117. callee[TIME] = tt
  2118. self.profile[TIME] += tt
  2119. self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct)
  2120. for fn, value in compat_iteritems(callers):
  2121. caller = self.get_function(fn)
  2122. call = Call(callee.id)
  2123. if isinstance(value, tuple):
  2124. for i in xrange(0, len(value), 4):
  2125. nc, cc, tt, ct = value[i:i+4]
  2126. if CALLS in call:
  2127. call[CALLS] += cc
  2128. else:
  2129. call[CALLS] = cc
  2130. if TOTAL_TIME in call:
  2131. call[TOTAL_TIME] += ct
  2132. else:
  2133. call[TOTAL_TIME] = ct
  2134. else:
  2135. call[CALLS] = value
  2136. call[TOTAL_TIME] = ratio(value, nc)*ct
  2137. caller.add_call(call)
  2138. if False:
  2139. self.stats.print_stats()
  2140. self.stats.print_callees()
  2141. # Compute derived events
  2142. self.profile.validate()
  2143. self.profile.ratio(TIME_RATIO, TIME)
  2144. self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
  2145. return self.profile
  2146. formats = {
  2147. "axe": AXEParser,
  2148. "callgrind": CallgrindParser,
  2149. "hprof": HProfParser,
  2150. "json": JsonParser,
  2151. "oprofile": OprofileParser,
  2152. "perf": PerfParser,
  2153. "prof": GprofParser,
  2154. "pstats": PstatsParser,
  2155. "sleepy": SleepyParser,
  2156. "sysprof": SysprofParser,
  2157. "xperf": XPerfParser,
  2158. }
  2159. ########################################################################
  2160. # Output
  2161. class Theme:
  2162. def __init__(self,
  2163. bgcolor = (0.0, 0.0, 1.0),
  2164. mincolor = (0.0, 0.0, 0.0),
  2165. maxcolor = (0.0, 0.0, 1.0),
  2166. fontname = "Arial",
  2167. fontcolor = "white",
  2168. nodestyle = "filled",
  2169. minfontsize = 10.0,
  2170. maxfontsize = 10.0,
  2171. minpenwidth = 0.5,
  2172. maxpenwidth = 4.0,
  2173. gamma = 2.2,
  2174. skew = 1.0):
  2175. self.bgcolor = bgcolor
  2176. self.mincolor = mincolor
  2177. self.maxcolor = maxcolor
  2178. self.fontname = fontname
  2179. self.fontcolor = fontcolor
  2180. self.nodestyle = nodestyle
  2181. self.minfontsize = minfontsize
  2182. self.maxfontsize = maxfontsize
  2183. self.minpenwidth = minpenwidth
  2184. self.maxpenwidth = maxpenwidth
  2185. self.gamma = gamma
  2186. self.skew = skew
  2187. def graph_bgcolor(self):
  2188. return self.hsl_to_rgb(*self.bgcolor)
  2189. def graph_fontname(self):
  2190. return self.fontname
  2191. def graph_fontcolor(self):
  2192. return self.fontcolor
  2193. def graph_fontsize(self):
  2194. return self.minfontsize
  2195. def node_bgcolor(self, weight):
  2196. return self.color(weight)
  2197. def node_fgcolor(self, weight):
  2198. if self.nodestyle == "filled":
  2199. return self.graph_bgcolor()
  2200. else:
  2201. return self.color(weight)
  2202. def node_fontsize(self, weight):
  2203. return self.fontsize(weight)
  2204. def node_style(self):
  2205. return self.nodestyle
  2206. def edge_color(self, weight):
  2207. return self.color(weight)
  2208. def edge_fontsize(self, weight):
  2209. return self.fontsize(weight)
  2210. def edge_penwidth(self, weight):
  2211. return max(weight*self.maxpenwidth, self.minpenwidth)
  2212. def edge_arrowsize(self, weight):
  2213. return 0.5 * math.sqrt(self.edge_penwidth(weight))
  2214. def fontsize(self, weight):
  2215. return max(weight**2 * self.maxfontsize, self.minfontsize)
  2216. def color(self, weight):
  2217. weight = min(max(weight, 0.0), 1.0)
  2218. hmin, smin, lmin = self.mincolor
  2219. hmax, smax, lmax = self.maxcolor
  2220. if self.skew < 0:
  2221. raise ValueError("Skew must be greater than 0")
  2222. elif self.skew == 1.0:
  2223. h = hmin + weight*(hmax - hmin)
  2224. s = smin + weight*(smax - smin)
  2225. l = lmin + weight*(lmax - lmin)
  2226. else:
  2227. base = self.skew
  2228. h = hmin + ((hmax-hmin)*(-1.0 + (base ** weight)) / (base - 1.0))
  2229. s = smin + ((smax-smin)*(-1.0 + (base ** weight)) / (base - 1.0))
  2230. l = lmin + ((lmax-lmin)*(-1.0 + (base ** weight)) / (base - 1.0))
  2231. return self.hsl_to_rgb(h, s, l)
  2232. def hsl_to_rgb(self, h, s, l):
  2233. """Convert a color from HSL color-model to RGB.
  2234. See also:
  2235. - http://www.w3.org/TR/css3-color/#hsl-color
  2236. """
  2237. h = h % 1.0
  2238. s = min(max(s, 0.0), 1.0)
  2239. l = min(max(l, 0.0), 1.0)
  2240. if l <= 0.5:
  2241. m2 = l*(s + 1.0)
  2242. else:
  2243. m2 = l + s - l*s
  2244. m1 = l*2.0 - m2
  2245. r = self._hue_to_rgb(m1, m2, h + 1.0/3.0)
  2246. g = self._hue_to_rgb(m1, m2, h)
  2247. b = self._hue_to_rgb(m1, m2, h - 1.0/3.0)
  2248. # Apply gamma correction
  2249. r **= self.gamma
  2250. g **= self.gamma
  2251. b **= self.gamma
  2252. return (r, g, b)
  2253. def _hue_to_rgb(self, m1, m2, h):
  2254. if h < 0.0:
  2255. h += 1.0
  2256. elif h > 1.0:
  2257. h -= 1.0
  2258. if h*6 < 1.0:
  2259. return m1 + (m2 - m1)*h*6.0
  2260. elif h*2 < 1.0:
  2261. return m2
  2262. elif h*3 < 2.0:
  2263. return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0
  2264. else:
  2265. return m1
  2266. TEMPERATURE_COLORMAP = Theme(
  2267. mincolor = (2.0/3.0, 0.80, 0.25), # dark blue
  2268. maxcolor = (0.0, 1.0, 0.5), # satured red
  2269. gamma = 1.0
  2270. )
  2271. PINK_COLORMAP = Theme(
  2272. mincolor = (0.0, 1.0, 0.90), # pink
  2273. maxcolor = (0.0, 1.0, 0.5), # satured red
  2274. )
  2275. GRAY_COLORMAP = Theme(
  2276. mincolor = (0.0, 0.0, 0.85), # light gray
  2277. maxcolor = (0.0, 0.0, 0.0), # black
  2278. )
  2279. BW_COLORMAP = Theme(
  2280. minfontsize = 8.0,
  2281. maxfontsize = 24.0,
  2282. mincolor = (0.0, 0.0, 0.0), # black
  2283. maxcolor = (0.0, 0.0, 0.0), # black
  2284. minpenwidth = 0.1,
  2285. maxpenwidth = 8.0,
  2286. )
  2287. PRINT_COLORMAP = Theme(
  2288. minfontsize = 18.0,
  2289. maxfontsize = 30.0,
  2290. fontcolor = "black",
  2291. nodestyle = "solid",
  2292. mincolor = (0.0, 0.0, 0.0), # black
  2293. maxcolor = (0.0, 0.0, 0.0), # black
  2294. minpenwidth = 0.1,
  2295. maxpenwidth = 8.0,
  2296. )
  2297. themes = {
  2298. "color": TEMPERATURE_COLORMAP,
  2299. "pink": PINK_COLORMAP,
  2300. "gray": GRAY_COLORMAP,
  2301. "bw": BW_COLORMAP,
  2302. "print": PRINT_COLORMAP,
  2303. }
  2304. def sorted_iteritems(d):
  2305. # Used mostly for result reproducibility (while testing.)
  2306. keys = compat_keys(d)
  2307. keys.sort()
  2308. for key in keys:
  2309. value = d[key]
  2310. yield key, value
  2311. class DotWriter:
  2312. """Writer for the DOT language.
  2313. See also:
  2314. - "The DOT Language" specification
  2315. http://www.graphviz.org/doc/info/lang.html
  2316. """
  2317. strip = False
  2318. wrap = False
  2319. def __init__(self, fp):
  2320. self.fp = fp
  2321. def wrap_function_name(self, name):
  2322. """Split the function name on multiple lines."""
  2323. if len(name) > 32:
  2324. ratio = 2.0/3.0
  2325. height = max(int(len(name)/(1.0 - ratio) + 0.5), 1)
  2326. width = max(len(name)/height, 32)
  2327. # TODO: break lines in symbols
  2328. name = textwrap.fill(name, width, break_long_words=False)
  2329. # Take away spaces
  2330. name = name.replace(", ", ",")
  2331. name = name.replace("> >", ">>")
  2332. name = name.replace("> >", ">>") # catch consecutive
  2333. return name
  2334. show_function_events = [TOTAL_TIME_RATIO, TIME_RATIO]
  2335. show_edge_events = [TOTAL_TIME_RATIO, CALLS]
  2336. def graph(self, profile, theme):
  2337. self.begin_graph()
  2338. fontname = theme.graph_fontname()
  2339. fontcolor = theme.graph_fontcolor()
  2340. nodestyle = theme.node_style()
  2341. self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
  2342. self.attr('node', fontname=fontname, shape="box", style=nodestyle, fontcolor=fontcolor, width=0, height=0)
  2343. self.attr('edge', fontname=fontname)
  2344. for _, function in sorted_iteritems(profile.functions):
  2345. labels = []
  2346. if function.process is not None:
  2347. labels.append(function.process)
  2348. if function.module is not None:
  2349. labels.append(function.module)
  2350. if self.strip:
  2351. function_name = function.stripped_name()
  2352. else:
  2353. function_name = function.name
  2354. if self.wrap:
  2355. function_name = self.wrap_function_name(function_name)
  2356. labels.append(function_name)
  2357. for event in self.show_function_events:
  2358. if event in function.events:
  2359. label = event.format(function[event])
  2360. labels.append(label)
  2361. if function.called is not None:
  2362. labels.append("%u%s" % (function.called, MULTIPLICATION_SIGN))
  2363. if function.weight is not None:
  2364. weight = function.weight
  2365. else:
  2366. weight = 0.0
  2367. label = '\n'.join(labels)
  2368. self.node(function.id,
  2369. label = label,
  2370. color = self.color(theme.node_bgcolor(weight)),
  2371. fontcolor = self.color(theme.node_fgcolor(weight)),
  2372. fontsize = "%.2f" % theme.node_fontsize(weight),
  2373. )
  2374. for _, call in sorted_iteritems(function.calls):
  2375. callee = profile.functions[call.callee_id]
  2376. labels = []
  2377. for event in self.show_edge_events:
  2378. if event in call.events:
  2379. label = event.format(call[event])
  2380. labels.append(label)
  2381. if call.weight is not None:
  2382. weight = call.weight
  2383. elif callee.weight is not None:
  2384. weight = callee.weight
  2385. else:
  2386. weight = 0.0
  2387. label = '\n'.join(labels)
  2388. self.edge(function.id, call.callee_id,
  2389. label = label,
  2390. color = self.color(theme.edge_color(weight)),
  2391. fontcolor = self.color(theme.edge_color(weight)),
  2392. fontsize = "%.2f" % theme.edge_fontsize(weight),
  2393. penwidth = "%.2f" % theme.edge_penwidth(weight),
  2394. labeldistance = "%.2f" % theme.edge_penwidth(weight),
  2395. arrowsize = "%.2f" % theme.edge_arrowsize(weight),
  2396. )
  2397. self.end_graph()
  2398. def begin_graph(self):
  2399. self.write('digraph {\n')
  2400. def end_graph(self):
  2401. self.write('}\n')
  2402. def attr(self, what, **attrs):
  2403. self.write("\t")
  2404. self.write(what)
  2405. self.attr_list(attrs)
  2406. self.write(";\n")
  2407. def node(self, node, **attrs):
  2408. self.write("\t")
  2409. self.id(node)
  2410. self.attr_list(attrs)
  2411. self.write(";\n")
  2412. def edge(self, src, dst, **attrs):
  2413. self.write("\t")
  2414. self.id(src)
  2415. self.write(" -> ")
  2416. self.id(dst)
  2417. self.attr_list(attrs)
  2418. self.write(";\n")
  2419. def attr_list(self, attrs):
  2420. if not attrs:
  2421. return
  2422. self.write(' [')
  2423. first = True
  2424. for name, value in sorted_iteritems(attrs):
  2425. if first:
  2426. first = False
  2427. else:
  2428. self.write(", ")
  2429. self.id(name)
  2430. self.write('=')
  2431. self.id(value)
  2432. self.write(']')
  2433. def id(self, id):
  2434. if isinstance(id, (int, float)):
  2435. s = str(id)
  2436. elif isinstance(id, basestring):
  2437. if id.isalnum() and not id.startswith('0x'):
  2438. s = id
  2439. else:
  2440. s = self.escape(id)
  2441. else:
  2442. raise TypeError
  2443. self.write(s)
  2444. def color(self, rgb):
  2445. r, g, b = rgb
  2446. def float2int(f):
  2447. if f <= 0.0:
  2448. return 0
  2449. if f >= 1.0:
  2450. return 255
  2451. return int(255.0*f + 0.5)
  2452. return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)])
  2453. def escape(self, s):
  2454. if not PYTHON_3:
  2455. s = s.encode('utf-8')
  2456. s = s.replace('\\', r'\\')
  2457. s = s.replace('\n', r'\n')
  2458. s = s.replace('\t', r'\t')
  2459. s = s.replace('"', r'\"')
  2460. return '"' + s + '"'
  2461. def write(self, s):
  2462. self.fp.write(s)
  2463. ########################################################################
  2464. # Main program
  2465. def naturalJoin(values):
  2466. if len(values) >= 2:
  2467. return ', '.join(values[:-1]) + ' or ' + values[-1]
  2468. else:
  2469. return ''.join(values)
  2470. def main():
  2471. """Main program."""
  2472. global totalMethod
  2473. formatNames = list(formats.keys())
  2474. formatNames.sort()
  2475. optparser = optparse.OptionParser(
  2476. usage="\n\t%prog [options] [file] ...")
  2477. optparser.add_option(
  2478. '-o', '--output', metavar='FILE',
  2479. type="string", dest="output",
  2480. help="output filename [stdout]")
  2481. optparser.add_option(
  2482. '-n', '--node-thres', metavar='PERCENTAGE',
  2483. type="float", dest="node_thres", default=0.5,
  2484. help="eliminate nodes below this threshold [default: %default]")
  2485. optparser.add_option(
  2486. '-e', '--edge-thres', metavar='PERCENTAGE',
  2487. type="float", dest="edge_thres", default=0.1,
  2488. help="eliminate edges below this threshold [default: %default]")
  2489. optparser.add_option(
  2490. '-f', '--format',
  2491. type="choice", choices=formatNames,
  2492. dest="format", default="prof",
  2493. help="profile format: %s [default: %%default]" % naturalJoin(formatNames))
  2494. optparser.add_option(
  2495. '--total',
  2496. type="choice", choices=('callratios', 'callstacks'),
  2497. dest="totalMethod", default=totalMethod,
  2498. help="preferred method of calculating total time: callratios or callstacks (currently affects only perf format) [default: %default]")
  2499. optparser.add_option(
  2500. '-c', '--colormap',
  2501. type="choice", choices=('color', 'pink', 'gray', 'bw', 'print'),
  2502. dest="theme", default="color",
  2503. help="color map: color, pink, gray, bw, or print [default: %default]")
  2504. optparser.add_option(
  2505. '-s', '--strip',
  2506. action="store_true",
  2507. dest="strip", default=False,
  2508. help="strip function parameters, template parameters, and const modifiers from demangled C++ function names")
  2509. optparser.add_option(
  2510. '-w', '--wrap',
  2511. action="store_true",
  2512. dest="wrap", default=False,
  2513. help="wrap function names")
  2514. optparser.add_option(
  2515. '--show-samples',
  2516. action="store_true",
  2517. dest="show_samples", default=False,
  2518. help="show function samples")
  2519. # add option to create subtree or show paths
  2520. optparser.add_option(
  2521. '-z', '--root',
  2522. type="string",
  2523. dest="root", default="",
  2524. help="prune call graph to show only descendants of specified root function")
  2525. optparser.add_option(
  2526. '-l', '--leaf',
  2527. type="string",
  2528. dest="leaf", default="",
  2529. help="prune call graph to show only ancestors of specified leaf function")
  2530. # add a new option to control skew of the colorization curve
  2531. optparser.add_option(
  2532. '--skew',
  2533. type="float", dest="theme_skew", default=1.0,
  2534. help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Values > 1.0 give less variety to lower percentages")
  2535. (options, args) = optparser.parse_args(sys.argv[1:])
  2536. if len(args) > 1 and options.format != 'pstats':
  2537. optparser.error('incorrect number of arguments')
  2538. try:
  2539. theme = themes[options.theme]
  2540. except KeyError:
  2541. optparser.error('invalid colormap \'%s\'' % options.theme)
  2542. # set skew on the theme now that it has been picked.
  2543. if options.theme_skew:
  2544. theme.skew = options.theme_skew
  2545. totalMethod = options.totalMethod
  2546. try:
  2547. Format = formats[options.format]
  2548. except KeyError:
  2549. optparser.error('invalid format \'%s\'' % options.format)
  2550. if Format.stdinInput:
  2551. if not args:
  2552. fp = sys.stdin
  2553. else:
  2554. fp = open(args[0], 'rt')
  2555. parser = Format(fp)
  2556. elif Format.multipleInput:
  2557. if not args:
  2558. optparser.error('at least a file must be specified for %s input' % options.format)
  2559. parser = Format(*args)
  2560. else:
  2561. if len(args) != 1:
  2562. optparser.error('exactly one file must be specified for %s input' % options.format)
  2563. parser = Format(args[0])
  2564. profile = parser.parse()
  2565. if options.output is None:
  2566. output = sys.stdout
  2567. else:
  2568. if PYTHON_3:
  2569. output = open(options.output, 'wt', encoding='UTF-8')
  2570. else:
  2571. output = open(options.output, 'wt')
  2572. dot = DotWriter(output)
  2573. dot.strip = options.strip
  2574. dot.wrap = options.wrap
  2575. if options.show_samples:
  2576. dot.show_function_events.append(SAMPLES)
  2577. profile = profile
  2578. profile.prune(options.node_thres/100.0, options.edge_thres/100.0)
  2579. if options.root:
  2580. rootId = profile.getFunctionId(options.root)
  2581. if not rootId:
  2582. sys.stderr.write('root node ' + options.root + ' not found (might already be pruned : try -e0 -n0 flags)\n')
  2583. sys.exit(1)
  2584. profile.prune_root(rootId)
  2585. if options.leaf:
  2586. leafId = profile.getFunctionId(options.leaf)
  2587. if not leafId:
  2588. sys.stderr.write('leaf node ' + options.leaf + ' not found (maybe already pruned : try -e0 -n0 flags)\n')
  2589. sys.exit(1)
  2590. profile.prune_leaf(leafId)
  2591. dot.graph(profile, theme)
  2592. if __name__ == '__main__':
  2593. main()