PageRenderTime 289ms CodeModel.GetById 20ms RepoModel.GetById 1ms 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

Large files files are truncated, but you can click here to view the full 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.writ

Large files files are truncated, but you can click here to view the full file