gprof2dot.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227
  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"
  20. __version__ = "1.0"
  21. import sys
  22. import math
  23. import os.path
  24. import re
  25. import textwrap
  26. import optparse
  27. import xml.parsers.expat
  28. try:
  29. # Debugging helper module
  30. import debug
  31. except ImportError:
  32. pass
  33. def percentage(p):
  34. return "%.02f%%" % (p*100.0,)
  35. def add(a, b):
  36. return a + b
  37. def equal(a, b):
  38. if a == b:
  39. return a
  40. else:
  41. return None
  42. def fail(a, b):
  43. assert False
  44. tol = 2 ** -23
  45. def ratio(numerator, denominator):
  46. try:
  47. ratio = float(numerator)/float(denominator)
  48. except ZeroDivisionError:
  49. # 0/0 is undefined, but 1.0 yields more useful results
  50. return 1.0
  51. if ratio < 0.0:
  52. if ratio < -tol:
  53. sys.stderr.write('warning: negative ratio (%s/%s)\n' % (numerator, denominator))
  54. return 0.0
  55. if ratio > 1.0:
  56. if ratio > 1.0 + tol:
  57. sys.stderr.write('warning: ratio greater than one (%s/%s)\n' % (numerator, denominator))
  58. return 1.0
  59. return ratio
  60. class UndefinedEvent(Exception):
  61. """Raised when attempting to get an event which is undefined."""
  62. def __init__(self, event):
  63. Exception.__init__(self)
  64. self.event = event
  65. def __str__(self):
  66. return 'unspecified event %s' % self.event.name
  67. class Event(object):
  68. """Describe a kind of event, and its basic operations."""
  69. def __init__(self, name, null, aggregator, formatter = str):
  70. self.name = name
  71. self._null = null
  72. self._aggregator = aggregator
  73. self._formatter = formatter
  74. def __eq__(self, other):
  75. return self is other
  76. def __hash__(self):
  77. return id(self)
  78. def null(self):
  79. return self._null
  80. def aggregate(self, val1, val2):
  81. """Aggregate two event values."""
  82. assert val1 is not None
  83. assert val2 is not None
  84. return self._aggregator(val1, val2)
  85. def format(self, val):
  86. """Format an event value."""
  87. assert val is not None
  88. return self._formatter(val)
  89. MODULE = Event("Module", None, equal)
  90. PROCESS = Event("Process", None, equal)
  91. CALLS = Event("Calls", 0, add)
  92. SAMPLES = Event("Samples", 0, add)
  93. SAMPLES2 = Event("Samples", 0, add)
  94. TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')')
  95. TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')')
  96. TOTAL_TIME = Event("Total time", 0.0, fail)
  97. TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage)
  98. CALL_RATIO = Event("Call ratio", 0.0, add, percentage)
  99. PRUNE_RATIO = Event("Prune ratio", 0.0, add, percentage)
  100. class Object(object):
  101. """Base class for all objects in profile which can store events."""
  102. def __init__(self, events=None):
  103. if events is None:
  104. self.events = {}
  105. else:
  106. self.events = events
  107. def __hash__(self):
  108. return id(self)
  109. def __eq__(self, other):
  110. return self is other
  111. def __contains__(self, event):
  112. return event in self.events
  113. def __getitem__(self, event):
  114. try:
  115. return self.events[event]
  116. except KeyError:
  117. raise UndefinedEvent(event)
  118. def __setitem__(self, event, value):
  119. if value is None:
  120. if event in self.events:
  121. del self.events[event]
  122. else:
  123. self.events[event] = value
  124. class Call(Object):
  125. """A call between functions.
  126. There should be at most one call object for every pair of functions.
  127. """
  128. def __init__(self, callee_id):
  129. Object.__init__(self)
  130. self.callee_id = callee_id
  131. class Function(Object):
  132. """A function."""
  133. def __init__(self, id, name):
  134. Object.__init__(self)
  135. self.id = id
  136. self.name = name
  137. self.calls = {}
  138. self.cycle = None
  139. def add_call(self, call):
  140. if call.callee_id in self.calls:
  141. sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id)))
  142. self.calls[call.callee_id] = call
  143. # TODO: write utility functions
  144. def __repr__(self):
  145. return self.name
  146. class Cycle(Object):
  147. """A cycle made from recursive function calls."""
  148. def __init__(self):
  149. Object.__init__(self)
  150. # XXX: Do cycles need an id?
  151. self.functions = set()
  152. def add_function(self, function):
  153. assert function not in self.functions
  154. self.functions.add(function)
  155. # XXX: Aggregate events?
  156. if function.cycle is not None:
  157. for other in function.cycle.functions:
  158. if function not in self.functions:
  159. self.add_function(other)
  160. function.cycle = self
  161. class Profile(Object):
  162. """The whole profile."""
  163. def __init__(self):
  164. Object.__init__(self)
  165. self.functions = {}
  166. self.cycles = []
  167. def add_function(self, function):
  168. if function.id in self.functions:
  169. sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id)))
  170. self.functions[function.id] = function
  171. def add_cycle(self, cycle):
  172. self.cycles.append(cycle)
  173. def validate(self):
  174. """Validate the edges."""
  175. for function in self.functions.itervalues():
  176. for callee_id in function.calls.keys():
  177. assert function.calls[callee_id].callee_id == callee_id
  178. if callee_id not in self.functions:
  179. sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name))
  180. del function.calls[callee_id]
  181. def find_cycles(self):
  182. """Find cycles using Tarjan's strongly connected components algorithm."""
  183. # Apply the Tarjan's algorithm successively until all functions are visited
  184. visited = set()
  185. for function in self.functions.itervalues():
  186. if function not in visited:
  187. self._tarjan(function, 0, [], {}, {}, visited)
  188. cycles = []
  189. for function in self.functions.itervalues():
  190. if function.cycle is not None and function.cycle not in cycles:
  191. cycles.append(function.cycle)
  192. self.cycles = cycles
  193. if 0:
  194. for cycle in cycles:
  195. sys.stderr.write("Cycle:\n")
  196. for member in cycle.functions:
  197. sys.stderr.write("\tFunction %s\n" % member.name)
  198. def _tarjan(self, function, order, stack, orders, lowlinks, visited):
  199. """Tarjan's strongly connected components algorithm.
  200. See also:
  201. - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
  202. """
  203. visited.add(function)
  204. orders[function] = order
  205. lowlinks[function] = order
  206. order += 1
  207. pos = len(stack)
  208. stack.append(function)
  209. for call in function.calls.itervalues():
  210. callee = self.functions[call.callee_id]
  211. # TODO: use a set to optimize lookup
  212. if callee not in orders:
  213. order = self._tarjan(callee, order, stack, orders, lowlinks, visited)
  214. lowlinks[function] = min(lowlinks[function], lowlinks[callee])
  215. elif callee in stack:
  216. lowlinks[function] = min(lowlinks[function], orders[callee])
  217. if lowlinks[function] == orders[function]:
  218. # Strongly connected component found
  219. members = stack[pos:]
  220. del stack[pos:]
  221. if len(members) > 1:
  222. cycle = Cycle()
  223. for member in members:
  224. cycle.add_function(member)
  225. return order
  226. def call_ratios(self, event):
  227. # Aggregate for incoming calls
  228. cycle_totals = {}
  229. for cycle in self.cycles:
  230. cycle_totals[cycle] = 0.0
  231. function_totals = {}
  232. for function in self.functions.itervalues():
  233. function_totals[function] = 0.0
  234. for function in self.functions.itervalues():
  235. for call in function.calls.itervalues():
  236. if call.callee_id != function.id:
  237. callee = self.functions[call.callee_id]
  238. function_totals[callee] += call[event]
  239. if callee.cycle is not None and callee.cycle is not function.cycle:
  240. cycle_totals[callee.cycle] += call[event]
  241. # Compute the ratios
  242. for function in self.functions.itervalues():
  243. for call in function.calls.itervalues():
  244. assert CALL_RATIO not in call
  245. if call.callee_id != function.id:
  246. callee = self.functions[call.callee_id]
  247. if callee.cycle is not None and callee.cycle is not function.cycle:
  248. total = cycle_totals[callee.cycle]
  249. else:
  250. total = function_totals[callee]
  251. call[CALL_RATIO] = ratio(call[event], total)
  252. def integrate(self, outevent, inevent):
  253. """Propagate function time ratio allong the function calls.
  254. Must be called after finding the cycles.
  255. See also:
  256. - http://citeseer.ist.psu.edu/graham82gprof.html
  257. """
  258. # Sanity checking
  259. assert outevent not in self
  260. for function in self.functions.itervalues():
  261. assert outevent not in function
  262. assert inevent in function
  263. for call in function.calls.itervalues():
  264. assert outevent not in call
  265. if call.callee_id != function.id:
  266. assert CALL_RATIO in call
  267. # Aggregate the input for each cycle
  268. for cycle in self.cycles:
  269. total = inevent.null()
  270. for function in self.functions.itervalues():
  271. total = inevent.aggregate(total, function[inevent])
  272. self[inevent] = total
  273. # Integrate along the edges
  274. total = inevent.null()
  275. for function in self.functions.itervalues():
  276. total = inevent.aggregate(total, function[inevent])
  277. self._integrate_function(function, outevent, inevent)
  278. self[outevent] = total
  279. def _integrate_function(self, function, outevent, inevent):
  280. if function.cycle is not None:
  281. return self._integrate_cycle(function.cycle, outevent, inevent)
  282. else:
  283. if outevent not in function:
  284. total = function[inevent]
  285. for call in function.calls.itervalues():
  286. if call.callee_id != function.id:
  287. total += self._integrate_call(call, outevent, inevent)
  288. function[outevent] = total
  289. return function[outevent]
  290. def _integrate_call(self, call, outevent, inevent):
  291. assert outevent not in call
  292. assert CALL_RATIO in call
  293. callee = self.functions[call.callee_id]
  294. subtotal = call[CALL_RATIO]*self._integrate_function(callee, outevent, inevent)
  295. call[outevent] = subtotal
  296. return subtotal
  297. def _integrate_cycle(self, cycle, outevent, inevent):
  298. if outevent not in cycle:
  299. # Compute the outevent for the whole cycle
  300. total = inevent.null()
  301. for member in cycle.functions:
  302. subtotal = member[inevent]
  303. for call in member.calls.itervalues():
  304. callee = self.functions[call.callee_id]
  305. if callee.cycle is not cycle:
  306. subtotal += self._integrate_call(call, outevent, inevent)
  307. total += subtotal
  308. cycle[outevent] = total
  309. # Compute the time propagated to callers of this cycle
  310. callees = {}
  311. for function in self.functions.itervalues():
  312. if function.cycle is not cycle:
  313. for call in function.calls.itervalues():
  314. callee = self.functions[call.callee_id]
  315. if callee.cycle is cycle:
  316. try:
  317. callees[callee] += call[CALL_RATIO]
  318. except KeyError:
  319. callees[callee] = call[CALL_RATIO]
  320. for member in cycle.functions:
  321. member[outevent] = outevent.null()
  322. for callee, call_ratio in callees.iteritems():
  323. ranks = {}
  324. call_ratios = {}
  325. partials = {}
  326. self._rank_cycle_function(cycle, callee, 0, ranks)
  327. self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set())
  328. partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent)
  329. assert partial == max(partials.values())
  330. assert not total or abs(1.0 - partial/(call_ratio*total)) <= 0.001
  331. return cycle[outevent]
  332. def _rank_cycle_function(self, cycle, function, rank, ranks):
  333. if function not in ranks or ranks[function] > rank:
  334. ranks[function] = rank
  335. for call in function.calls.itervalues():
  336. if call.callee_id != function.id:
  337. callee = self.functions[call.callee_id]
  338. if callee.cycle is cycle:
  339. self._rank_cycle_function(cycle, callee, rank + 1, ranks)
  340. def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited):
  341. if function not in visited:
  342. visited.add(function)
  343. for call in function.calls.itervalues():
  344. if call.callee_id != function.id:
  345. callee = self.functions[call.callee_id]
  346. if callee.cycle is cycle:
  347. if ranks[callee] > ranks[function]:
  348. call_ratios[callee] = call_ratios.get(callee, 0.0) + call[CALL_RATIO]
  349. self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited)
  350. def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent):
  351. if function not in partials:
  352. partial = partial_ratio*function[inevent]
  353. for call in function.calls.itervalues():
  354. if call.callee_id != function.id:
  355. callee = self.functions[call.callee_id]
  356. if callee.cycle is not cycle:
  357. assert outevent in call
  358. partial += partial_ratio*call[outevent]
  359. else:
  360. if ranks[callee] > ranks[function]:
  361. callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent)
  362. call_ratio = ratio(call[CALL_RATIO], call_ratios[callee])
  363. call_partial = call_ratio*callee_partial
  364. try:
  365. call[outevent] += call_partial
  366. except UndefinedEvent:
  367. call[outevent] = call_partial
  368. partial += call_partial
  369. partials[function] = partial
  370. try:
  371. function[outevent] += partial
  372. except UndefinedEvent:
  373. function[outevent] = partial
  374. return partials[function]
  375. def aggregate(self, event):
  376. """Aggregate an event for the whole profile."""
  377. total = event.null()
  378. for function in self.functions.itervalues():
  379. try:
  380. total = event.aggregate(total, function[event])
  381. except UndefinedEvent:
  382. return
  383. self[event] = total
  384. def ratio(self, outevent, inevent):
  385. assert outevent not in self
  386. assert inevent in self
  387. for function in self.functions.itervalues():
  388. assert outevent not in function
  389. assert inevent in function
  390. function[outevent] = ratio(function[inevent], self[inevent])
  391. for call in function.calls.itervalues():
  392. assert outevent not in call
  393. if inevent in call:
  394. call[outevent] = ratio(call[inevent], self[inevent])
  395. self[outevent] = 1.0
  396. def prune(self, node_thres, edge_thres):
  397. """Prune the profile"""
  398. # compute the prune ratios
  399. for function in self.functions.itervalues():
  400. try:
  401. function[PRUNE_RATIO] = function[TOTAL_TIME_RATIO]
  402. except UndefinedEvent:
  403. pass
  404. for call in function.calls.itervalues():
  405. callee = self.functions[call.callee_id]
  406. if TOTAL_TIME_RATIO in call:
  407. # handle exact cases first
  408. call[PRUNE_RATIO] = call[TOTAL_TIME_RATIO]
  409. else:
  410. try:
  411. # make a safe estimate
  412. call[PRUNE_RATIO] = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO])
  413. except UndefinedEvent:
  414. pass
  415. # prune the nodes
  416. for function_id in self.functions.keys():
  417. function = self.functions[function_id]
  418. try:
  419. if function[PRUNE_RATIO] < node_thres:
  420. del self.functions[function_id]
  421. except UndefinedEvent:
  422. pass
  423. # prune the egdes
  424. for function in self.functions.itervalues():
  425. for callee_id in function.calls.keys():
  426. call = function.calls[callee_id]
  427. try:
  428. if callee_id not in self.functions or call[PRUNE_RATIO] < edge_thres:
  429. del function.calls[callee_id]
  430. except UndefinedEvent:
  431. pass
  432. def dump(self):
  433. for function in self.functions.itervalues():
  434. sys.stderr.write('Function %s:\n' % (function.name,))
  435. self._dump_events(function.events)
  436. for call in function.calls.itervalues():
  437. callee = self.functions[call.callee_id]
  438. sys.stderr.write(' Call %s:\n' % (callee.name,))
  439. self._dump_events(call.events)
  440. for cycle in self.cycles:
  441. sys.stderr.write('Cycle:\n')
  442. self._dump_events(cycle.events)
  443. for function in cycle.functions:
  444. sys.stderr.write(' Function %s\n' % (function.name,))
  445. def _dump_events(self, events):
  446. for event, value in events.iteritems():
  447. sys.stderr.write(' %s: %s\n' % (event.name, event.format(value)))
  448. class Struct:
  449. """Masquerade a dictionary with a structure-like behavior."""
  450. def __init__(self, attrs = None):
  451. if attrs is None:
  452. attrs = {}
  453. self.__dict__['_attrs'] = attrs
  454. def __getattr__(self, name):
  455. try:
  456. return self._attrs[name]
  457. except KeyError:
  458. raise AttributeError(name)
  459. def __setattr__(self, name, value):
  460. self._attrs[name] = value
  461. def __str__(self):
  462. return str(self._attrs)
  463. def __repr__(self):
  464. return repr(self._attrs)
  465. class ParseError(Exception):
  466. """Raised when parsing to signal mismatches."""
  467. def __init__(self, msg, line):
  468. self.msg = msg
  469. # TODO: store more source line information
  470. self.line = line
  471. def __str__(self):
  472. return '%s: %r' % (self.msg, self.line)
  473. class Parser:
  474. """Parser interface."""
  475. def __init__(self):
  476. pass
  477. def parse(self):
  478. raise NotImplementedError
  479. class LineParser(Parser):
  480. """Base class for parsers that read line-based formats."""
  481. def __init__(self, file):
  482. Parser.__init__(self)
  483. self._file = file
  484. self.__line = None
  485. self.__eof = False
  486. def readline(self):
  487. line = self._file.readline()
  488. if not line:
  489. self.__line = ''
  490. self.__eof = True
  491. self.__line = line.rstrip('\r\n')
  492. def lookahead(self):
  493. assert self.__line is not None
  494. return self.__line
  495. def consume(self):
  496. assert self.__line is not None
  497. line = self.__line
  498. self.readline()
  499. return line
  500. def eof(self):
  501. assert self.__line is not None
  502. return self.__eof
  503. XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF = range(4)
  504. class XmlToken:
  505. def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
  506. assert type in (XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF)
  507. self.type = type
  508. self.name_or_data = name_or_data
  509. self.attrs = attrs
  510. self.line = line
  511. self.column = column
  512. def __str__(self):
  513. if self.type == XML_ELEMENT_START:
  514. return '<' + self.name_or_data + ' ...>'
  515. if self.type == XML_ELEMENT_END:
  516. return '</' + self.name_or_data + '>'
  517. if self.type == XML_CHARACTER_DATA:
  518. return self.name_or_data
  519. if self.type == XML_EOF:
  520. return 'end of file'
  521. assert 0
  522. class XmlTokenizer:
  523. """Expat based XML tokenizer."""
  524. def __init__(self, fp, skip_ws = True):
  525. self.fp = fp
  526. self.tokens = []
  527. self.index = 0
  528. self.final = False
  529. self.skip_ws = skip_ws
  530. self.character_pos = 0, 0
  531. self.character_data = ''
  532. self.parser = xml.parsers.expat.ParserCreate()
  533. self.parser.StartElementHandler = self.handle_element_start
  534. self.parser.EndElementHandler = self.handle_element_end
  535. self.parser.CharacterDataHandler = self.handle_character_data
  536. def handle_element_start(self, name, attributes):
  537. self.finish_character_data()
  538. line, column = self.pos()
  539. token = XmlToken(XML_ELEMENT_START, name, attributes, line, column)
  540. self.tokens.append(token)
  541. def handle_element_end(self, name):
  542. self.finish_character_data()
  543. line, column = self.pos()
  544. token = XmlToken(XML_ELEMENT_END, name, None, line, column)
  545. self.tokens.append(token)
  546. def handle_character_data(self, data):
  547. if not self.character_data:
  548. self.character_pos = self.pos()
  549. self.character_data += data
  550. def finish_character_data(self):
  551. if self.character_data:
  552. if not self.skip_ws or not self.character_data.isspace():
  553. line, column = self.character_pos
  554. token = XmlToken(XML_CHARACTER_DATA, self.character_data, None, line, column)
  555. self.tokens.append(token)
  556. self.character_data = ''
  557. def next(self):
  558. size = 16*1024
  559. while self.index >= len(self.tokens) and not self.final:
  560. self.tokens = []
  561. self.index = 0
  562. data = self.fp.read(size)
  563. self.final = len(data) < size
  564. try:
  565. self.parser.Parse(data, self.final)
  566. except xml.parsers.expat.ExpatError, e:
  567. #if e.code == xml.parsers.expat.errors.XML_ERROR_NO_ELEMENTS:
  568. if e.code == 3:
  569. pass
  570. else:
  571. raise e
  572. if self.index >= len(self.tokens):
  573. line, column = self.pos()
  574. token = XmlToken(XML_EOF, None, None, line, column)
  575. else:
  576. token = self.tokens[self.index]
  577. self.index += 1
  578. return token
  579. def pos(self):
  580. return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
  581. class XmlTokenMismatch(Exception):
  582. def __init__(self, expected, found):
  583. self.expected = expected
  584. self.found = found
  585. def __str__(self):
  586. return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
  587. class XmlParser(Parser):
  588. """Base XML document parser."""
  589. def __init__(self, fp):
  590. Parser.__init__(self)
  591. self.tokenizer = XmlTokenizer(fp)
  592. self.consume()
  593. def consume(self):
  594. self.token = self.tokenizer.next()
  595. def match_element_start(self, name):
  596. return self.token.type == XML_ELEMENT_START and self.token.name_or_data == name
  597. def match_element_end(self, name):
  598. return self.token.type == XML_ELEMENT_END and self.token.name_or_data == name
  599. def element_start(self, name):
  600. while self.token.type == XML_CHARACTER_DATA:
  601. self.consume()
  602. if self.token.type != XML_ELEMENT_START:
  603. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
  604. if self.token.name_or_data != name:
  605. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
  606. attrs = self.token.attrs
  607. self.consume()
  608. return attrs
  609. def element_end(self, name):
  610. while self.token.type == XML_CHARACTER_DATA:
  611. self.consume()
  612. if self.token.type != XML_ELEMENT_END:
  613. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
  614. if self.token.name_or_data != name:
  615. raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
  616. self.consume()
  617. def character_data(self, strip = True):
  618. data = ''
  619. while self.token.type == XML_CHARACTER_DATA:
  620. data += self.token.name_or_data
  621. self.consume()
  622. if strip:
  623. data = data.strip()
  624. return data
  625. class GprofParser(Parser):
  626. """Parser for GNU gprof output.
  627. See also:
  628. - Chapter "Interpreting gprof's Output" from the GNU gprof manual
  629. http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph
  630. - File "cg_print.c" from the GNU gprof source code
  631. http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src
  632. """
  633. def __init__(self, fp):
  634. Parser.__init__(self)
  635. self.fp = fp
  636. self.functions = {}
  637. self.cycles = {}
  638. def readline(self):
  639. line = self.fp.readline()
  640. if not line:
  641. sys.stderr.write('error: unexpected end of file\n')
  642. sys.exit(1)
  643. line = line.rstrip('\r\n')
  644. return line
  645. _int_re = re.compile(r'^\d+$')
  646. _float_re = re.compile(r'^\d+\.\d+$')
  647. def translate(self, mo):
  648. """Extract a structure from a match object, while translating the types in the process."""
  649. attrs = {}
  650. groupdict = mo.groupdict()
  651. for name, value in groupdict.iteritems():
  652. if value is None:
  653. value = None
  654. elif self._int_re.match(value):
  655. value = int(value)
  656. elif self._float_re.match(value):
  657. value = float(value)
  658. attrs[name] = (value)
  659. return Struct(attrs)
  660. _cg_header_re = re.compile(
  661. # original gprof header
  662. r'^\s+called/total\s+parents\s*$|' +
  663. r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' +
  664. r'^\s+called/total\s+children\s*$|' +
  665. # GNU gprof header
  666. r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$'
  667. )
  668. _cg_ignore_re = re.compile(
  669. # spontaneous
  670. r'^\s+<spontaneous>\s*$|'
  671. # internal calls (such as "mcount")
  672. r'^.*\((\d+)\)$'
  673. )
  674. _cg_primary_re = re.compile(
  675. r'^\[(?P<index>\d+)\]?' +
  676. r'\s+(?P<percentage_time>\d+\.\d+)' +
  677. r'\s+(?P<self>\d+\.\d+)' +
  678. r'\s+(?P<descendants>\d+\.\d+)' +
  679. r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
  680. r'\s+(?P<name>\S.*?)' +
  681. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  682. r'\s\[(\d+)\]$'
  683. )
  684. _cg_parent_re = re.compile(
  685. r'^\s+(?P<self>\d+\.\d+)?' +
  686. r'\s+(?P<descendants>\d+\.\d+)?' +
  687. r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' +
  688. r'\s+(?P<name>\S.*?)' +
  689. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  690. r'\s\[(?P<index>\d+)\]$'
  691. )
  692. _cg_child_re = _cg_parent_re
  693. _cg_cycle_header_re = re.compile(
  694. r'^\[(?P<index>\d+)\]?' +
  695. r'\s+(?P<percentage_time>\d+\.\d+)' +
  696. r'\s+(?P<self>\d+\.\d+)' +
  697. r'\s+(?P<descendants>\d+\.\d+)' +
  698. r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' +
  699. r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' +
  700. r'\s\[(\d+)\]$'
  701. )
  702. _cg_cycle_member_re = re.compile(
  703. r'^\s+(?P<self>\d+\.\d+)?' +
  704. r'\s+(?P<descendants>\d+\.\d+)?' +
  705. r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' +
  706. r'\s+(?P<name>\S.*?)' +
  707. r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
  708. r'\s\[(?P<index>\d+)\]$'
  709. )
  710. _cg_sep_re = re.compile(r'^--+$')
  711. def parse_function_entry(self, lines):
  712. parents = []
  713. children = []
  714. while True:
  715. if not lines:
  716. sys.stderr.write('warning: unexpected end of entry\n')
  717. line = lines.pop(0)
  718. if line.startswith('['):
  719. break
  720. # read function parent line
  721. mo = self._cg_parent_re.match(line)
  722. if not mo:
  723. if self._cg_ignore_re.match(line):
  724. continue
  725. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  726. else:
  727. parent = self.translate(mo)
  728. parents.append(parent)
  729. # read primary line
  730. mo = self._cg_primary_re.match(line)
  731. if not mo:
  732. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  733. return
  734. else:
  735. function = self.translate(mo)
  736. while lines:
  737. line = lines.pop(0)
  738. # read function subroutine line
  739. mo = self._cg_child_re.match(line)
  740. if not mo:
  741. if self._cg_ignore_re.match(line):
  742. continue
  743. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  744. else:
  745. child = self.translate(mo)
  746. children.append(child)
  747. function.parents = parents
  748. function.children = children
  749. self.functions[function.index] = function
  750. def parse_cycle_entry(self, lines):
  751. # read cycle header line
  752. line = lines[0]
  753. mo = self._cg_cycle_header_re.match(line)
  754. if not mo:
  755. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  756. return
  757. cycle = self.translate(mo)
  758. # read cycle member lines
  759. cycle.functions = []
  760. for line in lines[1:]:
  761. mo = self._cg_cycle_member_re.match(line)
  762. if not mo:
  763. sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
  764. continue
  765. call = self.translate(mo)
  766. cycle.functions.append(call)
  767. self.cycles[cycle.cycle] = cycle
  768. def parse_cg_entry(self, lines):
  769. if lines[0].startswith("["):
  770. self.parse_cycle_entry(lines)
  771. else:
  772. self.parse_function_entry(lines)
  773. def parse_cg(self):
  774. """Parse the call graph."""
  775. # skip call graph header
  776. while not self._cg_header_re.match(self.readline()):
  777. pass
  778. line = self.readline()
  779. while self._cg_header_re.match(line):
  780. line = self.readline()
  781. # process call graph entries
  782. entry_lines = []
  783. while line != '\014': # form feed
  784. if line and not line.isspace():
  785. if self._cg_sep_re.match(line):
  786. self.parse_cg_entry(entry_lines)
  787. entry_lines = []
  788. else:
  789. entry_lines.append(line)
  790. line = self.readline()
  791. def parse(self):
  792. self.parse_cg()
  793. self.fp.close()
  794. profile = Profile()
  795. profile[TIME] = 0.0
  796. cycles = {}
  797. for index in self.cycles.iterkeys():
  798. cycles[index] = Cycle()
  799. for entry in self.functions.itervalues():
  800. # populate the function
  801. function = Function(entry.index, entry.name)
  802. function[TIME] = entry.self
  803. if entry.called is not None:
  804. function[CALLS] = entry.called
  805. if entry.called_self is not None:
  806. call = Call(entry.index)
  807. call[CALLS] = entry.called_self
  808. function[CALLS] += entry.called_self
  809. # populate the function calls
  810. for child in entry.children:
  811. call = Call(child.index)
  812. assert child.called is not None
  813. call[CALLS] = child.called
  814. if child.index not in self.functions:
  815. # NOTE: functions that were never called but were discovered by gprof's
  816. # static call graph analysis dont have a call graph entry so we need
  817. # to add them here
  818. missing = Function(child.index, child.name)
  819. function[TIME] = 0.0
  820. function[CALLS] = 0
  821. profile.add_function(missing)
  822. function.add_call(call)
  823. profile.add_function(function)
  824. if entry.cycle is not None:
  825. try:
  826. cycle = cycles[entry.cycle]
  827. except KeyError:
  828. sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle)
  829. cycle = Cycle()
  830. cycles[entry.cycle] = cycle
  831. cycle.add_function(function)
  832. profile[TIME] = profile[TIME] + function[TIME]
  833. for cycle in cycles.itervalues():
  834. profile.add_cycle(cycle)
  835. # Compute derived events
  836. profile.validate()
  837. profile.ratio(TIME_RATIO, TIME)
  838. profile.call_ratios(CALLS)
  839. profile.integrate(TOTAL_TIME, TIME)
  840. profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
  841. return profile
  842. class OprofileParser(LineParser):
  843. """Parser for oprofile callgraph output.
  844. See also:
  845. - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph
  846. """
  847. _fields_re = {
  848. 'samples': r'(?P<samples>\d+)',
  849. '%': r'(?P<percentage>\S+)',
  850. 'linenr info': r'(?P<source>\(no location information\)|\S+:\d+)',
  851. 'image name': r'(?P<image>\S+(?:\s\(tgid:[^)]*\))?)',
  852. 'app name': r'(?P<application>\S+)',
  853. 'symbol name': r'(?P<symbol>\(no symbols\)|.+?)',
  854. }
  855. def __init__(self, infile):
  856. LineParser.__init__(self, infile)
  857. self.entries = {}
  858. self.entry_re = None
  859. def add_entry(self, callers, function, callees):
  860. try:
  861. entry = self.entries[function.id]
  862. except KeyError:
  863. self.entries[function.id] = (callers, function, callees)
  864. else:
  865. callers_total, function_total, callees_total = entry
  866. self.update_subentries_dict(callers_total, callers)
  867. function_total.samples += function.samples
  868. self.update_subentries_dict(callees_total, callees)
  869. def update_subentries_dict(self, totals, partials):
  870. for partial in partials.itervalues():
  871. try:
  872. total = totals[partial.id]
  873. except KeyError:
  874. totals[partial.id] = partial
  875. else:
  876. total.samples += partial.samples
  877. def parse(self):
  878. # read lookahead
  879. self.readline()
  880. self.parse_header()
  881. while self.lookahead():
  882. self.parse_entry()
  883. profile = Profile()
  884. reverse_call_samples = {}
  885. # populate the profile
  886. profile[SAMPLES] = 0
  887. for _callers, _function, _callees in self.entries.itervalues():
  888. function = Function(_function.id, _function.name)
  889. function[SAMPLES] = _function.samples
  890. profile.add_function(function)
  891. profile[SAMPLES] += _function.samples
  892. if _function.application:
  893. function[PROCESS] = os.path.basename(_function.application)
  894. if _function.image:
  895. function[MODULE] = os.path.basename(_function.image)
  896. total_callee_samples = 0
  897. for _callee in _callees.itervalues():
  898. total_callee_samples += _callee.samples
  899. for _callee in _callees.itervalues():
  900. if not _callee.self:
  901. call = Call(_callee.id)
  902. call[SAMPLES2] = _callee.samples
  903. function.add_call(call)
  904. # compute derived data
  905. profile.validate()
  906. profile.find_cycles()
  907. profile.ratio(TIME_RATIO, SAMPLES)
  908. profile.call_ratios(SAMPLES2)
  909. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  910. return profile
  911. def parse_header(self):
  912. while not self.match_header():
  913. self.consume()
  914. line = self.lookahead()
  915. fields = re.split(r'\s\s+', line)
  916. entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P<self>\s+\[self\])?$'
  917. self.entry_re = re.compile(entry_re)
  918. self.skip_separator()
  919. def parse_entry(self):
  920. callers = self.parse_subentries()
  921. if self.match_primary():
  922. function = self.parse_subentry()
  923. if function is not None:
  924. callees = self.parse_subentries()
  925. self.add_entry(callers, function, callees)
  926. self.skip_separator()
  927. def parse_subentries(self):
  928. subentries = {}
  929. while self.match_secondary():
  930. subentry = self.parse_subentry()
  931. subentries[subentry.id] = subentry
  932. return subentries
  933. def parse_subentry(self):
  934. entry = Struct()
  935. line = self.consume()
  936. mo = self.entry_re.match(line)
  937. if not mo:
  938. raise ParseError('failed to parse', line)
  939. fields = mo.groupdict()
  940. entry.samples = int(fields.get('samples', 0))
  941. entry.percentage = float(fields.get('percentage', 0.0))
  942. if 'source' in fields and fields['source'] != '(no location information)':
  943. source = fields['source']
  944. filename, lineno = source.split(':')
  945. entry.filename = filename
  946. entry.lineno = int(lineno)
  947. else:
  948. source = ''
  949. entry.filename = None
  950. entry.lineno = None
  951. entry.image = fields.get('image', '')
  952. entry.application = fields.get('application', '')
  953. if 'symbol' in fields and fields['symbol'] != '(no symbols)':
  954. entry.symbol = fields['symbol']
  955. else:
  956. entry.symbol = ''
  957. if entry.symbol.startswith('"') and entry.symbol.endswith('"'):
  958. entry.symbol = entry.symbol[1:-1]
  959. entry.id = ':'.join((entry.application, entry.image, source, entry.symbol))
  960. entry.self = fields.get('self', None) != None
  961. if entry.self:
  962. entry.id += ':self'
  963. if entry.symbol:
  964. entry.name = entry.symbol
  965. else:
  966. entry.name = entry.image
  967. return entry
  968. def skip_separator(self):
  969. while not self.match_separator():
  970. self.consume()
  971. self.consume()
  972. def match_header(self):
  973. line = self.lookahead()
  974. return line.startswith('samples')
  975. def match_separator(self):
  976. line = self.lookahead()
  977. return line == '-'*len(line)
  978. def match_primary(self):
  979. line = self.lookahead()
  980. return not line[:1].isspace()
  981. def match_secondary(self):
  982. line = self.lookahead()
  983. return line[:1].isspace()
  984. class SysprofParser(XmlParser):
  985. def __init__(self, stream):
  986. XmlParser.__init__(self, stream)
  987. def parse(self):
  988. objects = {}
  989. nodes = {}
  990. self.element_start('profile')
  991. while self.token.type == XML_ELEMENT_START:
  992. if self.token.name_or_data == 'objects':
  993. assert not objects
  994. objects = self.parse_items('objects')
  995. elif self.token.name_or_data == 'nodes':
  996. assert not nodes
  997. nodes = self.parse_items('nodes')
  998. else:
  999. self.parse_value(self.token.name_or_data)
  1000. self.element_end('profile')
  1001. return self.build_profile(objects, nodes)
  1002. def parse_items(self, name):
  1003. assert name[-1] == 's'
  1004. items = {}
  1005. self.element_start(name)
  1006. while self.token.type == XML_ELEMENT_START:
  1007. id, values = self.parse_item(name[:-1])
  1008. assert id not in items
  1009. items[id] = values
  1010. self.element_end(name)
  1011. return items
  1012. def parse_item(self, name):
  1013. attrs = self.element_start(name)
  1014. id = int(attrs['id'])
  1015. values = self.parse_values()
  1016. self.element_end(name)
  1017. return id, values
  1018. def parse_values(self):
  1019. values = {}
  1020. while self.token.type == XML_ELEMENT_START:
  1021. name = self.token.name_or_data
  1022. value = self.parse_value(name)
  1023. assert name not in values
  1024. values[name] = value
  1025. return values
  1026. def parse_value(self, tag):
  1027. self.element_start(tag)
  1028. value = self.character_data()
  1029. self.element_end(tag)
  1030. if value.isdigit():
  1031. return int(value)
  1032. if value.startswith('"') and value.endswith('"'):
  1033. return value[1:-1]
  1034. return value
  1035. def build_profile(self, objects, nodes):
  1036. profile = Profile()
  1037. profile[SAMPLES] = 0
  1038. for id, object in objects.iteritems():
  1039. # Ignore fake objects (process names, modules, "Everything", "kernel", etc.)
  1040. if object['self'] == 0:
  1041. continue
  1042. function = Function(id, object['name'])
  1043. function[SAMPLES] = object['self']
  1044. profile.add_function(function)
  1045. profile[SAMPLES] += function[SAMPLES]
  1046. for id, node in nodes.iteritems():
  1047. # Ignore fake calls
  1048. if node['self'] == 0:
  1049. continue
  1050. # Find a non-ignored parent
  1051. parent_id = node['parent']
  1052. while parent_id != 0:
  1053. parent = nodes[parent_id]
  1054. caller_id = parent['object']
  1055. if objects[caller_id]['self'] != 0:
  1056. break
  1057. parent_id = parent['parent']
  1058. if parent_id == 0:
  1059. continue
  1060. callee_id = node['object']
  1061. assert objects[caller_id]['self']
  1062. assert objects[callee_id]['self']
  1063. function = profile.functions[caller_id]
  1064. samples = node['self']
  1065. try:
  1066. call = function.calls[callee_id]
  1067. except KeyError:
  1068. call = Call(callee_id)
  1069. call[SAMPLES2] = samples
  1070. function.add_call(call)
  1071. else:
  1072. call[SAMPLES2] += samples
  1073. # Compute derived events
  1074. profile.validate()
  1075. profile.find_cycles()
  1076. profile.ratio(TIME_RATIO, SAMPLES)
  1077. profile.call_ratios(SAMPLES2)
  1078. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1079. return profile
  1080. class SharkParser(LineParser):
  1081. """Parser for MacOSX Shark output.
  1082. Author: tom@dbservice.com
  1083. """
  1084. def __init__(self, infile):
  1085. LineParser.__init__(self, infile)
  1086. self.stack = []
  1087. self.entries = {}
  1088. def add_entry(self, function):
  1089. try:
  1090. entry = self.entries[function.id]
  1091. except KeyError:
  1092. self.entries[function.id] = (function, { })
  1093. else:
  1094. function_total, callees_total = entry
  1095. function_total.samples += function.samples
  1096. def add_callee(self, function, callee):
  1097. func, callees = self.entries[function.id]
  1098. try:
  1099. entry = callees[callee.id]
  1100. except KeyError:
  1101. callees[callee.id] = callee
  1102. else:
  1103. entry.samples += callee.samples
  1104. def parse(self):
  1105. self.readline()
  1106. self.readline()
  1107. self.readline()
  1108. self.readline()
  1109. match = re.compile(r'(?P<prefix>[|+ ]*)(?P<samples>\d+), (?P<symbol>[^,]+), (?P<image>.*)')
  1110. while self.lookahead():
  1111. line = self.consume()
  1112. mo = match.match(line)
  1113. if not mo:
  1114. raise ParseError('failed to parse', line)
  1115. fields = mo.groupdict()
  1116. prefix = len(fields.get('prefix', 0)) / 2 - 1
  1117. symbol = str(fields.get('symbol', 0))
  1118. image = str(fields.get('image', 0))
  1119. entry = Struct()
  1120. entry.id = ':'.join([symbol, image])
  1121. entry.samples = int(fields.get('samples', 0))
  1122. entry.name = symbol
  1123. entry.image = image
  1124. # adjust the callstack
  1125. if prefix < len(self.stack):
  1126. del self.stack[prefix:]
  1127. if prefix == len(self.stack):
  1128. self.stack.append(entry)
  1129. # if the callstack has had an entry, it's this functions caller
  1130. if prefix > 0:
  1131. self.add_callee(self.stack[prefix - 1], entry)
  1132. self.add_entry(entry)
  1133. profile = Profile()
  1134. profile[SAMPLES] = 0
  1135. for _function, _callees in self.entries.itervalues():
  1136. function = Function(_function.id, _function.name)
  1137. function[SAMPLES] = _function.samples
  1138. profile.add_function(function)
  1139. profile[SAMPLES] += _function.samples
  1140. if _function.image:
  1141. function[MODULE] = os.path.basename(_function.image)
  1142. for _callee in _callees.itervalues():
  1143. call = Call(_callee.id)
  1144. call[SAMPLES] = _callee.samples
  1145. function.add_call(call)
  1146. # compute derived data
  1147. profile.validate()
  1148. profile.find_cycles()
  1149. profile.ratio(TIME_RATIO, SAMPLES)
  1150. profile.call_ratios(SAMPLES)
  1151. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1152. return profile
  1153. class SleepyParser(Parser):
  1154. """Parser for GNU gprof output.
  1155. See also:
  1156. - http://www.codersnotes.com/sleepy/
  1157. - http://sleepygraph.sourceforge.net/
  1158. """
  1159. def __init__(self, filename):
  1160. Parser.__init__(self)
  1161. from zipfile import ZipFile
  1162. self.database = ZipFile(filename)
  1163. self.symbols = {}
  1164. self.calls = {}
  1165. self.profile = Profile()
  1166. _symbol_re = re.compile(
  1167. r'^(?P<id>\w+)' +
  1168. r'\s+"(?P<module>[^"]*)"' +
  1169. r'\s+"(?P<procname>[^"]*)"' +
  1170. r'\s+"(?P<sourcefile>[^"]*)"' +
  1171. r'\s+(?P<sourceline>\d+)$'
  1172. )
  1173. def parse_symbols(self):
  1174. lines = self.database.read('symbols.txt').splitlines()
  1175. for line in lines:
  1176. mo = self._symbol_re.match(line)
  1177. if mo:
  1178. symbol_id, module, procname, sourcefile, sourceline = mo.groups()
  1179. function_id = ':'.join([module, procname])
  1180. try:
  1181. function = self.profile.functions[function_id]
  1182. except KeyError:
  1183. function = Function(function_id, procname)
  1184. function[SAMPLES] = 0
  1185. self.profile.add_function(function)
  1186. self.symbols[symbol_id] = function
  1187. def parse_callstacks(self):
  1188. lines = self.database.read("callstacks.txt").splitlines()
  1189. for line in lines:
  1190. fields = line.split()
  1191. samples = int(fields[0])
  1192. callstack = fields[1:]
  1193. callstack = [self.symbols[symbol_id] for symbol_id in callstack]
  1194. callee = callstack[0]
  1195. callee[SAMPLES] += samples
  1196. self.profile[SAMPLES] += samples
  1197. for caller in callstack[1:]:
  1198. try:
  1199. call = caller.calls[callee.id]
  1200. except KeyError:
  1201. call = Call(callee.id)
  1202. call[SAMPLES2] = samples
  1203. caller.add_call(call)
  1204. else:
  1205. call[SAMPLES2] += samples
  1206. callee = caller
  1207. def parse(self):
  1208. profile = self.profile
  1209. profile[SAMPLES] = 0
  1210. self.parse_symbols()
  1211. self.parse_callstacks()
  1212. # Compute derived events
  1213. profile.validate()
  1214. profile.find_cycles()
  1215. profile.ratio(TIME_RATIO, SAMPLES)
  1216. profile.call_ratios(SAMPLES2)
  1217. profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
  1218. return profile
  1219. class AQtimeTable:
  1220. def __init__(self, name, fields):
  1221. self.name = name
  1222. self.fields = fields
  1223. self.field_column = {}
  1224. for column in range(len(fields)):
  1225. self.field_column[fields[column]] = column
  1226. self.rows = []
  1227. def __len__(self):
  1228. return len(self.rows)
  1229. def __iter__(self):
  1230. for values, children in self.rows:
  1231. fields = {}
  1232. for name, value in zip(self.fields, values):
  1233. fields[name] = value
  1234. children = dict([(child.name, child) for child in children])
  1235. yield fields, children
  1236. raise StopIteration
  1237. def add_row(self, values, children=()):
  1238. self.rows.append((values, children))
  1239. class AQtimeParser(XmlParser):
  1240. def __init__(self, stream):
  1241. XmlParser.__init__(self, stream)
  1242. self.tables = {}
  1243. def parse(self):
  1244. self.element_start('AQtime_Results')
  1245. self.parse_headers()
  1246. results = self.parse_results()
  1247. self.element_end('AQtime_Results')
  1248. return self.build_profile(results)
  1249. def parse_headers(self):
  1250. self.element_start('HEADERS')
  1251. while self.token.type == XML_ELEMENT_START:
  1252. self.parse_table_header()
  1253. self.element_end('HEADERS')
  1254. def parse_table_header(self):
  1255. attrs = self.element_start('TABLE_HEADER')
  1256. name = attrs['NAME']
  1257. id = int(attrs['ID'])
  1258. field_types = []
  1259. field_names = []
  1260. while self.token.type == XML_ELEMENT_START:
  1261. field_type, field_name = self.parse_table_field()
  1262. field_types.append(field_type)
  1263. field_names.append(field_name)
  1264. self.element_end('TABLE_HEADER')
  1265. self.tables[id] = name, field_types, field_names
  1266. def parse_table_field(self):
  1267. attrs = self.element_start('TABLE_FIELD')
  1268. type = attrs['TYPE']
  1269. name = self.character_data()
  1270. self.element_end('TABLE_FIELD')
  1271. return type, name
  1272. def parse_results(self):
  1273. self.element_start('RESULTS')
  1274. table = self.parse_data()
  1275. self.element_end('RESULTS')
  1276. return table
  1277. def parse_data(self):
  1278. rows = []
  1279. attrs = self.element_start('DATA')
  1280. table_id = int(attrs['TABLE_ID'])
  1281. table_name, field_types, field_names = self.tables[table_id]
  1282. table = AQtimeTable(table_name, field_names)
  1283. while self.token.type == XML_ELEMENT_START:
  1284. row, children = self.parse_row(field_types)
  1285. table.add_row(row, children)
  1286. self.element_end('DATA')
  1287. return table
  1288. def parse_row(self, field_types):
  1289. row = [None]*len(field_types)
  1290. children = []
  1291. self.element_start('ROW')
  1292. while self.token.type == XML_ELEMENT_START:
  1293. if self.token.name_or_data == 'FIELD':
  1294. field_id, field_value = self.parse_field(field_types)
  1295. row[field_id] = field_value
  1296. elif self.token.name_or_data == 'CHILDREN':
  1297. children = self.parse_children()
  1298. else:
  1299. raise XmlTokenMismatch("<FIELD ...> or <CHILDREN ...>", self.token)
  1300. self.element_end('ROW')
  1301. return row, children
  1302. def parse_field(self, field_types):
  1303. attrs = self.element_start('FIELD')
  1304. id = int(attrs['ID'])
  1305. type = field_types[id]
  1306. value = self.character_data()
  1307. if type == 'Integer':
  1308. value = int(value)
  1309. elif type == 'Float':
  1310. value = float(value)
  1311. elif type == 'Address':
  1312. value = int(value)
  1313. elif type == 'String':
  1314. pass
  1315. else:
  1316. assert False
  1317. self.element_end('FIELD')
  1318. return id, value
  1319. def parse_children(self):
  1320. children = []
  1321. self.element_start('CHILDREN')
  1322. while self.token.type == XML_ELEMENT_START:
  1323. table = self.parse_data()
  1324. assert table.name not in children
  1325. children.append(table)
  1326. self.element_end('CHILDREN')
  1327. return children
  1328. def build_profile(self, results):
  1329. assert results.name == 'Routines'
  1330. profile = Profile()
  1331. profile[TIME] = 0.0
  1332. for fields, tables in results:
  1333. function = self.build_function(fields)
  1334. children = tables['Children']
  1335. for fields, _ in children:
  1336. call = self.build_call(fields)
  1337. function.add_call(call)
  1338. profile.add_function(function)
  1339. profile[TIME] = profile[TIME] + function[TIME]
  1340. profile[TOTAL_TIME] = profile[TIME]
  1341. profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
  1342. return profile
  1343. def build_function(self, fields):
  1344. function = Function(self.build_id(fields), self.build_name(fields))
  1345. function[TIME] = fields['Time']
  1346. function[TOTAL_TIME] = fields['Time with Children']
  1347. #function[TIME_RATIO] = fields['% Time']/100.0
  1348. #function[TOTAL_TIME_RATIO] = fields['% with Children']/100.0
  1349. return function
  1350. def build_call(self, fields):
  1351. call = Call(self.build_id(fields))
  1352. call[TIME] = fields['Time']
  1353. call[TOTAL_TIME] = fields['Time with Children']
  1354. #call[TIME_RATIO] = fields['% Time']/100.0
  1355. #call[TOTAL_TIME_RATIO] = fields['% with Children']/100.0
  1356. return call
  1357. def build_id(self, fields):
  1358. return ':'.join([fields['Module Name'], fields['Unit Name'], fields['Routine Name']])
  1359. def build_name(self, fields):
  1360. # TODO: use more fields
  1361. return fields['Routine Name']
  1362. class PstatsParser:
  1363. """Parser python profiling statistics saved with te pstats module."""
  1364. def __init__(self, *filename):
  1365. import pstats
  1366. try:
  1367. self.stats = pstats.Stats(*filename)
  1368. except ValueError:
  1369. import hotshot.stats
  1370. self.stats = hotshot.stats.load(filename[0])
  1371. self.profile = Profile()
  1372. self.function_ids = {}
  1373. def get_function_name(self, (filename, line, name)):
  1374. module = os.path.splitext(filename)[0]
  1375. module = os.path.basename(module)
  1376. return "%s:%d:%s" % (module, line, name)
  1377. def get_function(self, key):
  1378. try:
  1379. id = self.function_ids[key]
  1380. except KeyError:
  1381. id = len(self.function_ids)
  1382. name = self.get_function_name(key)
  1383. function = Function(id, name)
  1384. self.profile.functions[id] = function
  1385. self.function_ids[key] = id
  1386. else:
  1387. function = self.profile.functions[id]
  1388. return function
  1389. def parse(self):
  1390. self.profile[TIME] = 0.0
  1391. self.profile[TOTAL_TIME] = self.stats.total_tt
  1392. for fn, (cc, nc, tt, ct, callers) in self.stats.stats.iteritems():
  1393. callee = self.get_function(fn)
  1394. callee[CALLS] = nc
  1395. callee[TOTAL_TIME] = ct
  1396. callee[TIME] = tt
  1397. self.profile[TIME] += tt
  1398. self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct)
  1399. for fn, value in callers.iteritems():
  1400. caller = self.get_function(fn)
  1401. call = Call(callee.id)
  1402. if isinstance(value, tuple):
  1403. for i in xrange(0, len(value), 4):
  1404. nc, cc, tt, ct = value[i:i+4]
  1405. if CALLS in call:
  1406. call[CALLS] += cc
  1407. else:
  1408. call[CALLS] = cc
  1409. if TOTAL_TIME in call:
  1410. call[TOTAL_TIME] += ct
  1411. else:
  1412. call[TOTAL_TIME] = ct
  1413. else:
  1414. call[CALLS] = value
  1415. call[TOTAL_TIME] = ratio(value, nc)*ct
  1416. caller.add_call(call)
  1417. #self.stats.print_stats()
  1418. #self.stats.print_callees()
  1419. # Compute derived events
  1420. self.profile.validate()
  1421. self.profile.ratio(TIME_RATIO, TIME)
  1422. self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
  1423. return self.profile
  1424. class Theme:
  1425. def __init__(self,
  1426. bgcolor = (0.0, 0.0, 1.0),
  1427. mincolor = (0.0, 0.0, 0.0),
  1428. maxcolor = (0.0, 0.0, 1.0),
  1429. fontname = "Arial",
  1430. minfontsize = 10.0,
  1431. maxfontsize = 10.0,
  1432. minpenwidth = 0.5,
  1433. maxpenwidth = 4.0,
  1434. gamma = 2.2,
  1435. skew = 1.0):
  1436. self.bgcolor = bgcolor
  1437. self.mincolor = mincolor
  1438. self.maxcolor = maxcolor
  1439. self.fontname = fontname
  1440. self.minfontsize = minfontsize
  1441. self.maxfontsize = maxfontsize
  1442. self.minpenwidth = minpenwidth
  1443. self.maxpenwidth = maxpenwidth
  1444. self.gamma = gamma
  1445. self.skew = skew
  1446. def graph_bgcolor(self):
  1447. return self.hsl_to_rgb(*self.bgcolor)
  1448. def graph_fontname(self):
  1449. return self.fontname
  1450. def graph_fontsize(self):
  1451. return self.minfontsize
  1452. def node_bgcolor(self, weight):
  1453. return self.color(weight)
  1454. def node_fgcolor(self, weight):
  1455. return self.graph_bgcolor()
  1456. def node_fontsize(self, weight):
  1457. return self.fontsize(weight)
  1458. def edge_color(self, weight):
  1459. return self.color(weight)
  1460. def edge_fontsize(self, weight):
  1461. return self.fontsize(weight)
  1462. def edge_penwidth(self, weight):
  1463. return max(weight*self.maxpenwidth, self.minpenwidth)
  1464. def edge_arrowsize(self, weight):
  1465. return 0.5 * math.sqrt(self.edge_penwidth(weight))
  1466. def fontsize(self, weight):
  1467. return max(weight**2 * self.maxfontsize, self.minfontsize)
  1468. def color(self, weight):
  1469. weight = min(max(weight, 0.0), 1.0)
  1470. hmin, smin, lmin = self.mincolor
  1471. hmax, smax, lmax = self.maxcolor
  1472. if self.skew < 0:
  1473. raise ValueError("Skew must be greater than 0")
  1474. elif self.skew == 1.0:
  1475. h = hmin + weight*(hmax - hmin)
  1476. s = smin + weight*(smax - smin)
  1477. l = lmin + weight*(lmax - lmin)
  1478. else:
  1479. base = self.skew
  1480. h = hmin + ((hmax-hmin)*(-1.0 + (base ** weight)) / (base - 1.0))
  1481. s = smin + ((smax-smin)*(-1.0 + (base ** weight)) / (base - 1.0))
  1482. l = lmin + ((lmax-lmin)*(-1.0 + (base ** weight)) / (base - 1.0))
  1483. return self.hsl_to_rgb(h, s, l)
  1484. def hsl_to_rgb(self, h, s, l):
  1485. """Convert a color from HSL color-model to RGB.
  1486. See also:
  1487. - http://www.w3.org/TR/css3-color/#hsl-color
  1488. """
  1489. h = h % 1.0
  1490. s = min(max(s, 0.0), 1.0)
  1491. l = min(max(l, 0.0), 1.0)
  1492. if l <= 0.5:
  1493. m2 = l*(s + 1.0)
  1494. else:
  1495. m2 = l + s - l*s
  1496. m1 = l*2.0 - m2
  1497. r = self._hue_to_rgb(m1, m2, h + 1.0/3.0)
  1498. g = self._hue_to_rgb(m1, m2, h)
  1499. b = self._hue_to_rgb(m1, m2, h - 1.0/3.0)
  1500. # Apply gamma correction
  1501. r **= self.gamma
  1502. g **= self.gamma
  1503. b **= self.gamma
  1504. return (r, g, b)
  1505. def _hue_to_rgb(self, m1, m2, h):
  1506. if h < 0.0:
  1507. h += 1.0
  1508. elif h > 1.0:
  1509. h -= 1.0
  1510. if h*6 < 1.0:
  1511. return m1 + (m2 - m1)*h*6.0
  1512. elif h*2 < 1.0:
  1513. return m2
  1514. elif h*3 < 2.0:
  1515. return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0
  1516. else:
  1517. return m1
  1518. TEMPERATURE_COLORMAP = Theme(
  1519. mincolor = (2.0/3.0, 0.80, 0.25), # dark blue
  1520. maxcolor = (0.0, 1.0, 0.5), # satured red
  1521. gamma = 1.0
  1522. )
  1523. PINK_COLORMAP = Theme(
  1524. mincolor = (0.0, 1.0, 0.90), # pink
  1525. maxcolor = (0.0, 1.0, 0.5), # satured red
  1526. )
  1527. GRAY_COLORMAP = Theme(
  1528. mincolor = (0.0, 0.0, 0.85), # light gray
  1529. maxcolor = (0.0, 0.0, 0.0), # black
  1530. )
  1531. BW_COLORMAP = Theme(
  1532. minfontsize = 8.0,
  1533. maxfontsize = 24.0,
  1534. mincolor = (0.0, 0.0, 0.0), # black
  1535. maxcolor = (0.0, 0.0, 0.0), # black
  1536. minpenwidth = 0.1,
  1537. maxpenwidth = 8.0,
  1538. )
  1539. class DotWriter:
  1540. """Writer for the DOT language.
  1541. See also:
  1542. - "The DOT Language" specification
  1543. http://www.graphviz.org/doc/info/lang.html
  1544. """
  1545. def __init__(self, fp):
  1546. self.fp = fp
  1547. def graph(self, profile, theme):
  1548. self.begin_graph()
  1549. fontname = theme.graph_fontname()
  1550. self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
  1551. self.attr('node', fontname=fontname, shape="box", style="filled", fontcolor="white", width=0, height=0)
  1552. self.attr('edge', fontname=fontname)
  1553. for function in profile.functions.itervalues():
  1554. labels = []
  1555. for event in PROCESS, MODULE:
  1556. if event in function.events:
  1557. label = event.format(function[event])
  1558. labels.append(label)
  1559. labels.append(function.name)
  1560. for event in TOTAL_TIME_RATIO, TIME_RATIO, CALLS:
  1561. if event in function.events:
  1562. label = event.format(function[event])
  1563. labels.append(label)
  1564. try:
  1565. weight = function[PRUNE_RATIO]
  1566. except UndefinedEvent:
  1567. weight = 0.0
  1568. label = '\n'.join(labels)
  1569. self.node(function.id,
  1570. label = label,
  1571. color = self.color(theme.node_bgcolor(weight)),
  1572. fontcolor = self.color(theme.node_fgcolor(weight)),
  1573. fontsize = "%.2f" % theme.node_fontsize(weight),
  1574. )
  1575. for call in function.calls.itervalues():
  1576. callee = profile.functions[call.callee_id]
  1577. labels = []
  1578. for event in TOTAL_TIME_RATIO, CALLS:
  1579. if event in call.events:
  1580. label = event.format(call[event])
  1581. labels.append(label)
  1582. try:
  1583. weight = call[PRUNE_RATIO]
  1584. except UndefinedEvent:
  1585. try:
  1586. weight = callee[PRUNE_RATIO]
  1587. except UndefinedEvent:
  1588. weight = 0.0
  1589. label = '\n'.join(labels)
  1590. self.edge(function.id, call.callee_id,
  1591. label = label,
  1592. color = self.color(theme.edge_color(weight)),
  1593. fontcolor = self.color(theme.edge_color(weight)),
  1594. fontsize = "%.2f" % theme.edge_fontsize(weight),
  1595. penwidth = "%.2f" % theme.edge_penwidth(weight),
  1596. labeldistance = "%.2f" % theme.edge_penwidth(weight),
  1597. arrowsize = "%.2f" % theme.edge_arrowsize(weight),
  1598. )
  1599. self.end_graph()
  1600. def begin_graph(self):
  1601. self.write('digraph {\n')
  1602. def end_graph(self):
  1603. self.write('}\n')
  1604. def attr(self, what, **attrs):
  1605. self.write("\t")
  1606. self.write(what)
  1607. self.attr_list(attrs)
  1608. self.write(";\n")
  1609. def node(self, node, **attrs):
  1610. self.write("\t")
  1611. self.id(node)
  1612. self.attr_list(attrs)
  1613. self.write(";\n")
  1614. def edge(self, src, dst, **attrs):
  1615. self.write("\t")
  1616. self.id(src)
  1617. self.write(" -> ")
  1618. self.id(dst)
  1619. self.attr_list(attrs)
  1620. self.write(";\n")
  1621. def attr_list(self, attrs):
  1622. if not attrs:
  1623. return
  1624. self.write(' [')
  1625. first = True
  1626. for name, value in attrs.iteritems():
  1627. if first:
  1628. first = False
  1629. else:
  1630. self.write(", ")
  1631. self.id(name)
  1632. self.write('=')
  1633. self.id(value)
  1634. self.write(']')
  1635. def id(self, id):
  1636. if isinstance(id, (int, float)):
  1637. s = str(id)
  1638. elif isinstance(id, basestring):
  1639. if id.isalnum():
  1640. s = id
  1641. else:
  1642. s = self.escape(id)
  1643. else:
  1644. raise TypeError
  1645. self.write(s)
  1646. def color(self, (r, g, b)):
  1647. def float2int(f):
  1648. if f <= 0.0:
  1649. return 0
  1650. if f >= 1.0:
  1651. return 255
  1652. return int(255.0*f + 0.5)
  1653. return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)])
  1654. def escape(self, s):
  1655. s = s.encode('utf-8')
  1656. s = s.replace('\\', r'\\')
  1657. s = s.replace('\n', r'\n')
  1658. s = s.replace('\t', r'\t')
  1659. s = s.replace('"', r'\"')
  1660. return '"' + s + '"'
  1661. def write(self, s):
  1662. self.fp.write(s)
  1663. class Main:
  1664. """Main program."""
  1665. themes = {
  1666. "color": TEMPERATURE_COLORMAP,
  1667. "pink": PINK_COLORMAP,
  1668. "gray": GRAY_COLORMAP,
  1669. "bw": BW_COLORMAP,
  1670. }
  1671. def main(self):
  1672. """Main program."""
  1673. parser = optparse.OptionParser(
  1674. usage="\n\t%prog [options] [file] ...",
  1675. version="%%prog %s" % __version__)
  1676. parser.add_option(
  1677. '-o', '--output', metavar='FILE',
  1678. type="string", dest="output",
  1679. help="output filename [stdout]")
  1680. parser.add_option(
  1681. '-n', '--node-thres', metavar='PERCENTAGE',
  1682. type="float", dest="node_thres", default=0.5,
  1683. help="eliminate nodes below this threshold [default: %default]")
  1684. parser.add_option(
  1685. '-e', '--edge-thres', metavar='PERCENTAGE',
  1686. type="float", dest="edge_thres", default=0.1,
  1687. help="eliminate edges below this threshold [default: %default]")
  1688. parser.add_option(
  1689. '-f', '--format',
  1690. type="choice", choices=('prof', 'oprofile', 'sysprof', 'pstats', 'shark', 'sleepy', 'aqtime'),
  1691. dest="format", default="prof",
  1692. help="profile format: prof, oprofile, sysprof, shark, sleepy, aqtime, or pstats [default: %default]")
  1693. parser.add_option(
  1694. '-c', '--colormap',
  1695. type="choice", choices=('color', 'pink', 'gray', 'bw'),
  1696. dest="theme", default="color",
  1697. help="color map: color, pink, gray, or bw [default: %default]")
  1698. parser.add_option(
  1699. '-s', '--strip',
  1700. action="store_true",
  1701. dest="strip", default=False,
  1702. help="strip function parameters, template parameters, and const modifiers from demangled C++ function names")
  1703. parser.add_option(
  1704. '-w', '--wrap',
  1705. action="store_true",
  1706. dest="wrap", default=False,
  1707. help="wrap function names")
  1708. # add a new option to control skew of the colorization curve
  1709. parser.add_option(
  1710. '--skew',
  1711. type="float", dest="theme_skew", default=1.0,
  1712. help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Value > 1.0 give less variety to lower percentages")
  1713. (self.options, self.args) = parser.parse_args(sys.argv[1:])
  1714. if len(self.args) > 1 and self.options.format != 'pstats':
  1715. parser.error('incorrect number of arguments')
  1716. try:
  1717. self.theme = self.themes[self.options.theme]
  1718. except KeyError:
  1719. parser.error('invalid colormap \'%s\'' % self.options.theme)
  1720. # set skew on the theme now that it has been picked.
  1721. if self.options.theme_skew:
  1722. self.theme.skew = self.options.theme_skew
  1723. if self.options.format == 'prof':
  1724. if not self.args:
  1725. fp = sys.stdin
  1726. else:
  1727. fp = open(self.args[0], 'rt')
  1728. parser = GprofParser(fp)
  1729. elif self.options.format == 'oprofile':
  1730. if not self.args:
  1731. fp = sys.stdin
  1732. else:
  1733. fp = open(self.args[0], 'rt')
  1734. parser = OprofileParser(fp)
  1735. elif self.options.format == 'sysprof':
  1736. if not self.args:
  1737. fp = sys.stdin
  1738. else:
  1739. fp = open(self.args[0], 'rt')
  1740. parser = SysprofParser(fp)
  1741. elif self.options.format == 'pstats':
  1742. if not self.args:
  1743. parser.error('at least a file must be specified for pstats input')
  1744. parser = PstatsParser(*self.args)
  1745. elif self.options.format == 'shark':
  1746. if not self.args:
  1747. fp = sys.stdin
  1748. else:
  1749. fp = open(self.args[0], 'rt')
  1750. parser = SharkParser(fp)
  1751. elif self.options.format == 'sleepy':
  1752. if len(self.args) != 1:
  1753. parser.error('exactly one file must be specified for sleepy input')
  1754. parser = SleepyParser(self.args[0])
  1755. elif self.options.format == 'aqtime':
  1756. if not self.args:
  1757. fp = sys.stdin
  1758. else:
  1759. fp = open(self.args[0], 'rt')
  1760. parser = AQtimeParser(fp)
  1761. else:
  1762. parser.error('invalid format \'%s\'' % self.options.format)
  1763. self.profile = parser.parse()
  1764. if self.options.output is None:
  1765. self.output = sys.stdout
  1766. else:
  1767. self.output = open(self.options.output, 'wt')
  1768. self.write_graph()
  1769. _parenthesis_re = re.compile(r'\([^()]*\)')
  1770. _angles_re = re.compile(r'<[^<>]*>')
  1771. _const_re = re.compile(r'\s+const$')
  1772. def strip_function_name(self, name):
  1773. """Remove extraneous information from C++ demangled function names."""
  1774. # Strip function parameters from name by recursively removing paired parenthesis
  1775. while True:
  1776. name, n = self._parenthesis_re.subn('', name)
  1777. if not n:
  1778. break
  1779. # Strip const qualifier
  1780. name = self._const_re.sub('', name)
  1781. # Strip template parameters from name by recursively removing paired angles
  1782. while True:
  1783. name, n = self._angles_re.subn('', name)
  1784. if not n:
  1785. break
  1786. return name
  1787. def wrap_function_name(self, name):
  1788. """Split the function name on multiple lines."""
  1789. if len(name) > 32:
  1790. ratio = 2.0/3.0
  1791. height = max(int(len(name)/(1.0 - ratio) + 0.5), 1)
  1792. width = max(len(name)/height, 32)
  1793. # TODO: break lines in symbols
  1794. name = textwrap.fill(name, width, break_long_words=False)
  1795. # Take away spaces
  1796. name = name.replace(", ", ",")
  1797. name = name.replace("> >", ">>")
  1798. name = name.replace("> >", ">>") # catch consecutive
  1799. return name
  1800. def compress_function_name(self, name):
  1801. """Compress function name according to the user preferences."""
  1802. if self.options.strip:
  1803. name = self.strip_function_name(name)
  1804. if self.options.wrap:
  1805. name = self.wrap_function_name(name)
  1806. # TODO: merge functions with same resulting name
  1807. return name
  1808. def write_graph(self):
  1809. dot = DotWriter(self.output)
  1810. profile = self.profile
  1811. profile.prune(self.options.node_thres/100.0, self.options.edge_thres/100.0)
  1812. for function in profile.functions.itervalues():
  1813. function.name = self.compress_function_name(function.name)
  1814. dot.graph(profile, self.theme)
  1815. if __name__ == '__main__':
  1816. Main().main()