PageRenderTime 39ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/library/gprof2dot.py

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