gprof2dot.py 104 KB

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