Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/gprof2dot.py: 15%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

2330 statements  

1#!/usr/bin/env python3 

2# 

3# Copyright 2008-2023 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 

19"""Generate a dot graph from the output of several profilers.""" 

20 

21__author__ = "Jose Fonseca et al" 

22 

23 

24import sys 

25import math 

26import os.path 

27import re 

28import textwrap 

29import argparse 

30import xml.parsers.expat 

31import collections 

32import locale 

33import json 

34import fnmatch 

35import codecs 

36import io 

37import hashlib 

38 

39assert sys.version_info[0] >= 3 

40 

41 

42######################################################################## 

43# Model 

44 

45 

46MULTIPLICATION_SIGN = chr(0xd7) 

47timeFormat = "%.7g" 

48 

49 

50def times(x): 

51 return "%u%s" % (x, MULTIPLICATION_SIGN) 

52 

53def percentage(p): 

54 return "%.02f%%" % (p*100.0,) 

55 

56def fmttime(t): 

57 return timeFormat % t 

58 

59def add(a, b): 

60 return a + b 

61 

62def fail(a, b): 

63 assert False 

64 

65# To enhance readability, labels are rounded to the number of decimal 

66# places corresponding to the tolerance value. 

67def round_difference(difference, tolerance): 

68 n = -math.floor(math.log10(tolerance)) 

69 return round(difference, n) 

70 

71 

72def rescale_difference(x, min_val, max_val): 

73 return (x - min_val) / (max_val - min_val) 

74 

75 

76def min_max_difference(profile1, profile2): 

77 f1_events = [f1[TOTAL_TIME_RATIO] for _, f1 in sorted_iteritems(profile1.functions)] 

78 f2_events = [f2[TOTAL_TIME_RATIO] for _, f2 in sorted_iteritems(profile2.functions)] 

79 differences = [] 

80 for i in range(len(f1_events)): 

81 try: 

82 differences.append(abs(f1_events[i] - f2_events[i]) * 100) 

83 except IndexError: 

84 differences.append(0) 

85 

86 return min(differences), max(differences) 

87 

88 

89tol = 2 ** -23 

90 

91def ratio(numerator, denominator): 

92 try: 

93 ratio = float(numerator)/float(denominator) 

94 except ZeroDivisionError: 

95 # 0/0 is undefined, but 1.0 yields more useful results 

96 return 1.0 

97 if ratio < 0.0: 

98 if ratio < -tol: 

99 sys.stderr.write('warning: negative ratio (%s/%s)\n' % (numerator, denominator)) 

100 return 0.0 

101 if ratio > 1.0: 

102 if ratio > 1.0 + tol: 

103 sys.stderr.write('warning: ratio greater than one (%s/%s)\n' % (numerator, denominator)) 

104 return 1.0 

105 return ratio 

106 

107 

108class UndefinedEvent(Exception): 

109 """Raised when attempting to get an event which is undefined.""" 

110 

111 def __init__(self, event): 

112 Exception.__init__(self) 

113 self.event = event 

114 

115 def __str__(self): 

116 return 'unspecified event %s' % self.event.name 

117 

118 

119class Event: 

120 """Describe a kind of event, and its basic operations.""" 

121 

122 def __init__(self, name, null, aggregator, formatter = str): 

123 self.name = name 

124 self._null = null 

125 self._aggregator = aggregator 

126 self._formatter = formatter 

127 

128 def __repr__(self): 

129 return self.name 

130 

131 def null(self): 

132 return self._null 

133 

134 def aggregate(self, val1, val2): 

135 """Aggregate two event values.""" 

136 assert val1 is not None 

137 assert val2 is not None 

138 return self._aggregator(val1, val2) 

139 

140 def format(self, val): 

141 """Format an event value.""" 

142 assert val is not None 

143 return self._formatter(val) 

144 

145 

146CALLS = Event("Calls", 0, add, times) 

147SAMPLES = Event("Samples", 0, add, times) 

148SAMPLES2 = Event("Samples", 0, add, times) 

149 

150# Count of samples where a given function was either executing or on the stack. 

151# This is used to calculate the total time ratio according to the 

152# straightforward method described in Mike Dunlavey's answer to 

153# stackoverflow.com/questions/1777556/alternatives-to-gprof, item 4 (the myth 

154# "that recursion is a tricky confusing issue"), last edited 2012-08-30: it's 

155# just the ratio of TOTAL_SAMPLES over the number of samples in the profile. 

156# 

157# Used only when totalMethod == callstacks 

158TOTAL_SAMPLES = Event("Samples", 0, add, times) 

159 

160TIME = Event("Time", 0.0, add, lambda x: '(' + fmttime(x) + ')') 

161TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')') 

162TOTAL_TIME = Event("Total time", 0.0, fail, fmttime) 

163TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage) 

164 

165labels = { 

166 'self-time': TIME, 

167 'self-time-percentage': TIME_RATIO, 

168 'total-time': TOTAL_TIME, 

169 'total-time-percentage': TOTAL_TIME_RATIO, 

170} 

171defaultLabelNames = ['total-time-percentage', 'self-time-percentage'] 

172 

173totalMethod = 'callratios' 

174 

175 

176class Object: 

177 """Base class for all objects in profile which can store events.""" 

178 

179 def __init__(self, events=None): 

180 if events is None: 

181 self.events = {} 

182 else: 

183 self.events = events 

184 

185 def __lt__(self, other): 

186 return id(self) < id(other) 

187 

188 def __contains__(self, event): 

189 return event in self.events 

190 

191 def __getitem__(self, event): 

192 try: 

193 return self.events[event] 

194 except KeyError: 

195 raise UndefinedEvent(event) 

196 

197 def __setitem__(self, event, value): 

198 if value is None: 

199 if event in self.events: 

200 del self.events[event] 

201 else: 

202 self.events[event] = value 

203 

204 

205class Call(Object): 

206 """A call between functions. 

207 

208 There should be at most one call object for every pair of functions. 

209 """ 

210 

211 def __init__(self, callee_id): 

212 Object.__init__(self) 

213 self.callee_id = callee_id 

214 self.ratio = None 

215 self.weight = None 

216 

217 

218class Function(Object): 

219 """A function.""" 

220 

221 def __init__(self, id, name): 

222 Object.__init__(self) 

223 self.id = id 

224 self.name = name 

225 self.module = None 

226 self.process = None 

227 self.calls = {} 

228 self.called = None 

229 self.weight = None 

230 self.cycle = None 

231 self.filename = None 

232 

233 def add_call(self, call): 

234 if call.callee_id in self.calls: 

235 sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id))) 

236 self.calls[call.callee_id] = call 

237 

238 def get_call(self, callee_id): 

239 if not callee_id in self.calls: 

240 call = Call(callee_id) 

241 call[SAMPLES] = 0 

242 call[SAMPLES2] = 0 

243 call[CALLS] = 0 

244 self.calls[callee_id] = call 

245 return self.calls[callee_id] 

246 

247 _parenthesis_re = re.compile(r'\([^()]*\)') 

248 _angles_re = re.compile(r'<[^<>]*>') 

249 _const_re = re.compile(r'\s+const$') 

250 

251 def stripped_name(self): 

252 """Remove extraneous information from C++ demangled function names.""" 

253 

254 name = self.name 

255 

256 # Strip function parameters from name by recursively removing paired parenthesis 

257 while True: 

258 name, n = self._parenthesis_re.subn('', name) 

259 if not n: 

260 break 

261 

262 # Strip const qualifier 

263 name = self._const_re.sub('', name) 

264 

265 # Strip template parameters from name by recursively removing paired angles 

266 while True: 

267 name, n = self._angles_re.subn('', name) 

268 if not n: 

269 break 

270 

271 return name 

272 

273 # TODO: write utility functions 

274 

275 def __repr__(self): 

276 return self.name 

277 

278 def dump(self, sep1=",\n\t", sep2=":=", sep3="\n"): 

279 """ Returns as a string all information available in this Function object 

280 separators sep1:between entries 

281 sep2:between attribute name and value, 

282 sep3: inserted at end 

283 """ 

284 return sep1.join(sep2.join([k,str(v)]) for (k,v) in sorted(self.__dict__.items())) + sep3 

285 

286 

287class Cycle(Object): 

288 """A cycle made from recursive function calls.""" 

289 

290 def __init__(self): 

291 Object.__init__(self) 

292 self.functions = set() 

293 

294 def add_function(self, function): 

295 assert function not in self.functions 

296 self.functions.add(function) 

297 if function.cycle is not None: 

298 for other in function.cycle.functions: 

299 if function not in self.functions: 

300 self.add_function(other) 

301 function.cycle = self 

302 

303 

304class Profile(Object): 

305 """The whole profile.""" 

306 

307 def __init__(self): 

308 Object.__init__(self) 

309 self.functions = {} 

310 self.cycles = [] 

311 

312 def add_function(self, function): 

313 if function.id in self.functions: 

314 sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id))) 

315 self.functions[function.id] = function 

316 

317 def add_cycle(self, cycle): 

318 self.cycles.append(cycle) 

319 

320 def validate(self): 

321 """Validate the edges.""" 

322 

323 for function in self.functions.values(): 

324 for callee_id in list(function.calls.keys()): 

325 assert function.calls[callee_id].callee_id == callee_id 

326 if callee_id not in self.functions: 

327 sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name)) 

328 del function.calls[callee_id] 

329 

330 def find_cycles(self): 

331 """Find cycles using Tarjan's strongly connected components algorithm.""" 

332 

333 # Apply the Tarjan's algorithm successively until all functions are visited 

334 stack = [] 

335 data = {} 

336 order = 0 

337 for function in self.functions.values(): 

338 order = self._tarjan(function, order, stack, data) 

339 cycles = [] 

340 for function in self.functions.values(): 

341 if function.cycle is not None and function.cycle not in cycles: 

342 cycles.append(function.cycle) 

343 self.cycles = cycles 

344 if 0: 

345 for cycle in cycles: 

346 sys.stderr.write("Cycle:\n") 

347 for member in cycle.functions: 

348 sys.stderr.write("\tFunction %s\n" % member.name) 

349 

350 def prune_root(self, roots, depth=-1): 

351 visited = set() 

352 frontier = set([(root_node, depth) for root_node in roots]) 

353 while len(frontier) > 0: 

354 node, node_depth = frontier.pop() 

355 visited.add(node) 

356 if node_depth == 0: 

357 continue 

358 f = self.functions[node] 

359 newNodes = set(f.calls.keys()) - visited 

360 frontier = frontier.union({(new_node, node_depth - 1) for new_node in newNodes}) 

361 subtreeFunctions = {} 

362 for n in visited: 

363 f = self.functions[n] 

364 newCalls = {} 

365 for c in f.calls.keys(): 

366 if c in visited: 

367 newCalls[c] = f.calls[c] 

368 f.calls = newCalls 

369 subtreeFunctions[n] = f 

370 self.functions = subtreeFunctions 

371 

372 def prune_leaf(self, leafs, depth=-1): 

373 edgesUp = collections.defaultdict(set) 

374 for f in self.functions.keys(): 

375 for n in self.functions[f].calls.keys(): 

376 edgesUp[n].add(f) 

377 # build the tree up 

378 visited = set() 

379 frontier = set([(leaf_node, depth) for leaf_node in leafs]) 

380 while len(frontier) > 0: 

381 node, node_depth = frontier.pop() 

382 visited.add(node) 

383 if node_depth == 0: 

384 continue 

385 newNodes = edgesUp[node] - visited 

386 frontier = frontier.union({(new_node, node_depth - 1) for new_node in newNodes}) 

387 downTree = set(self.functions.keys()) 

388 upTree = visited 

389 path = downTree.intersection(upTree) 

390 pathFunctions = {} 

391 for n in path: 

392 f = self.functions[n] 

393 newCalls = {} 

394 for c in f.calls.keys(): 

395 if c in path: 

396 newCalls[c] = f.calls[c] 

397 f.calls = newCalls 

398 pathFunctions[n] = f 

399 self.functions = pathFunctions 

400 

401 def getFunctionIds(self, funcName): 

402 function_names = {v.name: k for (k, v) in self.functions.items()} 

403 return [function_names[name] for name in fnmatch.filter(function_names.keys(), funcName)] 

404 

405 def getFunctionId(self, funcName): 

406 for f in self.functions: 

407 if self.functions[f].name == funcName: 

408 return f 

409 return False 

410 

411 def printFunctionIds(self, selector=None, file=sys.stderr): 

412 """ Print to file function entries selected by fnmatch.fnmatch like in 

413 method getFunctionIds, with following extensions: 

414 - selector starts with "%": dump all information available 

415 - selector is '+' or '-': select all function entries 

416 """ 

417 if selector is None or selector in ("+", "*"): 

418 v = ",\n".join(("%s:\t%s" % (kf,self.functions[kf].name) 

419 for kf in self.functions.keys())) 

420 else: 

421 if selector[0]=="%": 

422 selector=selector[1:] 

423 function_info={k:v for (k,v) 

424 in self.functions.items() 

425 if fnmatch.fnmatch(v.name,selector)} 

426 v = ",\n".join( ("%s\t({k})\t(%s)::\n\t%s" % (v.name,type(v),v.dump()) 

427 for (k,v) in function_info.items() 

428 )) 

429 

430 else: 

431 function_names = (v.name for v in self.functions.values()) 

432 v = ",\n".join( ( nm for nm in fnmatch.filter(function_names,selector ))) 

433 

434 file.write(v+"\n") 

435 file.flush() 

436 

437 class _TarjanData: 

438 def __init__(self, order): 

439 self.order = order 

440 self.lowlink = order 

441 self.onstack = False 

442 

443 def _tarjan(self, function, order, stack, data): 

444 """Tarjan's strongly connected components algorithm. 

445 

446 See also: 

447 - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm 

448 """ 

449 

450 try: 

451 func_data = data[function.id] 

452 return order 

453 except KeyError: 

454 func_data = self._TarjanData(order) 

455 data[function.id] = func_data 

456 order += 1 

457 pos = len(stack) 

458 stack.append(function) 

459 func_data.onstack = True 

460 for call in function.calls.values(): 

461 try: 

462 callee_data = data[call.callee_id] 

463 if callee_data.onstack: 

464 func_data.lowlink = min(func_data.lowlink, callee_data.order) 

465 except KeyError: 

466 callee = self.functions[call.callee_id] 

467 order = self._tarjan(callee, order, stack, data) 

468 callee_data = data[call.callee_id] 

469 func_data.lowlink = min(func_data.lowlink, callee_data.lowlink) 

470 if func_data.lowlink == func_data.order: 

471 # Strongly connected component found 

472 members = stack[pos:] 

473 del stack[pos:] 

474 if len(members) > 1: 

475 cycle = Cycle() 

476 for member in members: 

477 cycle.add_function(member) 

478 data[member.id].onstack = False 

479 else: 

480 for member in members: 

481 data[member.id].onstack = False 

482 return order 

483 

484 def call_ratios(self, event): 

485 # Aggregate for incoming calls 

486 cycle_totals = {} 

487 for cycle in self.cycles: 

488 cycle_totals[cycle] = 0.0 

489 function_totals = {} 

490 for function in self.functions.values(): 

491 function_totals[function] = 0.0 

492 

493 # Pass 1: function_total gets the sum of call[event] for all 

494 # incoming arrows. Same for cycle_total for all arrows 

495 # that are coming into the *cycle* but are not part of it. 

496 for function in self.functions.values(): 

497 for call in function.calls.values(): 

498 if call.callee_id != function.id: 

499 callee = self.functions[call.callee_id] 

500 if event in call.events: 

501 function_totals[callee] += call[event] 

502 if callee.cycle is not None and callee.cycle is not function.cycle: 

503 cycle_totals[callee.cycle] += call[event] 

504 else: 

505 sys.stderr.write("call_ratios: No data for " + function.name + " call to " + callee.name + "\n") 

506 

507 # Pass 2: Compute the ratios. Each call[event] is scaled by the 

508 # function_total of the callee. Calls into cycles use the 

509 # cycle_total, but not calls within cycles. 

510 for function in self.functions.values(): 

511 for call in function.calls.values(): 

512 assert call.ratio is None 

513 if call.callee_id != function.id: 

514 callee = self.functions[call.callee_id] 

515 if event in call.events: 

516 if callee.cycle is not None and callee.cycle is not function.cycle: 

517 total = cycle_totals[callee.cycle] 

518 else: 

519 total = function_totals[callee] 

520 call.ratio = ratio(call[event], total) 

521 else: 

522 # Warnings here would only repeat those issued above. 

523 call.ratio = 0.0 

524 

525 def integrate(self, outevent, inevent): 

526 """Propagate function time ratio along the function calls. 

527 

528 Must be called after finding the cycles. 

529 

530 See also: 

531 - http://citeseer.ist.psu.edu/graham82gprof.html 

532 """ 

533 

534 # Sanity checking 

535 assert outevent not in self 

536 for function in self.functions.values(): 

537 assert outevent not in function 

538 assert inevent in function 

539 for call in function.calls.values(): 

540 assert outevent not in call 

541 if call.callee_id != function.id: 

542 assert call.ratio is not None 

543 

544 # Aggregate the input for each cycle 

545 for cycle in self.cycles: 

546 total = inevent.null() 

547 for function in self.functions.values(): 

548 total = inevent.aggregate(total, function[inevent]) 

549 self[inevent] = total 

550 

551 # Integrate along the edges 

552 total = inevent.null() 

553 for function in self.functions.values(): 

554 total = inevent.aggregate(total, function[inevent]) 

555 self._integrate_function(function, outevent, inevent) 

556 self[outevent] = total 

557 

558 def _integrate_function(self, function, outevent, inevent): 

559 if function.cycle is not None: 

560 return self._integrate_cycle(function.cycle, outevent, inevent) 

561 else: 

562 if outevent not in function: 

563 total = function[inevent] 

564 for call in function.calls.values(): 

565 if call.callee_id != function.id: 

566 total += self._integrate_call(call, outevent, inevent) 

567 function[outevent] = total 

568 return function[outevent] 

569 

570 def _integrate_call(self, call, outevent, inevent): 

571 assert outevent not in call 

572 assert call.ratio is not None 

573 callee = self.functions[call.callee_id] 

574 subtotal = call.ratio *self._integrate_function(callee, outevent, inevent) 

575 call[outevent] = subtotal 

576 return subtotal 

577 

578 def _integrate_cycle(self, cycle, outevent, inevent): 

579 if outevent not in cycle: 

580 

581 # Compute the outevent for the whole cycle 

582 total = inevent.null() 

583 for member in cycle.functions: 

584 subtotal = member[inevent] 

585 for call in member.calls.values(): 

586 callee = self.functions[call.callee_id] 

587 if callee.cycle is not cycle: 

588 subtotal += self._integrate_call(call, outevent, inevent) 

589 total += subtotal 

590 cycle[outevent] = total 

591 

592 # Compute the time propagated to callers of this cycle 

593 callees = {} 

594 for function in self.functions.values(): 

595 if function.cycle is not cycle: 

596 for call in function.calls.values(): 

597 callee = self.functions[call.callee_id] 

598 if callee.cycle is cycle: 

599 try: 

600 callees[callee] += call.ratio 

601 except KeyError: 

602 callees[callee] = call.ratio 

603 

604 for member in cycle.functions: 

605 member[outevent] = outevent.null() 

606 

607 for callee, call_ratio in callees.items(): 

608 ranks = {} 

609 call_ratios = {} 

610 partials = {} 

611 self._rank_cycle_function(cycle, callee, ranks) 

612 self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set()) 

613 partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent) 

614 

615 # Ensure `partial == max(partials.values())`, but with round-off tolerance 

616 max_partial = max(partials.values()) 

617 assert abs(partial - max_partial) <= 1e-7*max_partial 

618 

619 assert abs(call_ratio*total - partial) <= 0.001*call_ratio*total 

620 

621 return cycle[outevent] 

622 

623 def _rank_cycle_function(self, cycle, function, ranks): 

624 """Dijkstra's shortest paths algorithm. 

625 

626 See also: 

627 - http://en.wikipedia.org/wiki/Dijkstra's_algorithm 

628 """ 

629 

630 import heapq 

631 Q = [] 

632 Qd = {} 

633 p = {} 

634 visited = set([function]) 

635 

636 ranks[function] = 0 

637 for call in function.calls.values(): 

638 if call.callee_id != function.id: 

639 callee = self.functions[call.callee_id] 

640 if callee.cycle is cycle: 

641 ranks[callee] = 1 

642 item = [ranks[callee], function, callee] 

643 heapq.heappush(Q, item) 

644 Qd[callee] = item 

645 

646 while Q: 

647 cost, parent, member = heapq.heappop(Q) 

648 if member not in visited: 

649 p[member]= parent 

650 visited.add(member) 

651 for call in member.calls.values(): 

652 if call.callee_id != member.id: 

653 callee = self.functions[call.callee_id] 

654 if callee.cycle is cycle: 

655 member_rank = ranks[member] 

656 rank = ranks.get(callee) 

657 if rank is not None: 

658 if rank > 1 + member_rank: 

659 rank = 1 + member_rank 

660 ranks[callee] = rank 

661 Qd_callee = Qd[callee] 

662 Qd_callee[0] = rank 

663 Qd_callee[1] = member 

664 heapq._siftdown(Q, 0, Q.index(Qd_callee)) 

665 else: 

666 rank = 1 + member_rank 

667 ranks[callee] = rank 

668 item = [rank, member, callee] 

669 heapq.heappush(Q, item) 

670 Qd[callee] = item 

671 

672 def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited): 

673 if function not in visited: 

674 visited.add(function) 

675 for call in function.calls.values(): 

676 if call.callee_id != function.id: 

677 callee = self.functions[call.callee_id] 

678 if callee.cycle is cycle: 

679 if ranks[callee] > ranks[function]: 

680 call_ratios[callee] = call_ratios.get(callee, 0.0) + call.ratio 

681 self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited) 

682 

683 def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent): 

684 if function not in partials: 

685 partial = partial_ratio*function[inevent] 

686 for call in function.calls.values(): 

687 if call.callee_id != function.id: 

688 callee = self.functions[call.callee_id] 

689 if callee.cycle is not cycle: 

690 assert outevent in call 

691 partial += partial_ratio*call[outevent] 

692 else: 

693 if ranks[callee] > ranks[function]: 

694 callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent) 

695 call_ratio = ratio(call.ratio, call_ratios[callee]) 

696 call_partial = call_ratio*callee_partial 

697 try: 

698 call[outevent] += call_partial 

699 except UndefinedEvent: 

700 call[outevent] = call_partial 

701 partial += call_partial 

702 partials[function] = partial 

703 try: 

704 function[outevent] += partial 

705 except UndefinedEvent: 

706 function[outevent] = partial 

707 return partials[function] 

708 

709 def aggregate(self, event): 

710 """Aggregate an event for the whole profile.""" 

711 

712 total = event.null() 

713 for function in self.functions.values(): 

714 try: 

715 total = event.aggregate(total, function[event]) 

716 except UndefinedEvent: 

717 return 

718 self[event] = total 

719 

720 def ratio(self, outevent, inevent): 

721 assert outevent not in self 

722 assert inevent in self 

723 for function in self.functions.values(): 

724 assert outevent not in function 

725 assert inevent in function 

726 function[outevent] = ratio(function[inevent], self[inevent]) 

727 for call in function.calls.values(): 

728 assert outevent not in call 

729 if inevent in call: 

730 call[outevent] = ratio(call[inevent], self[inevent]) 

731 self[outevent] = 1.0 

732 

733 def prune(self, node_thres, edge_thres, paths, color_nodes_by_selftime): 

734 """Prune the profile""" 

735 

736 # compute the prune ratios 

737 for function in self.functions.values(): 

738 try: 

739 function.weight = function[TOTAL_TIME_RATIO] 

740 except UndefinedEvent: 

741 pass 

742 

743 for call in function.calls.values(): 

744 callee = self.functions[call.callee_id] 

745 

746 if TOTAL_TIME_RATIO in call: 

747 # handle exact cases first 

748 call.weight = call[TOTAL_TIME_RATIO] 

749 else: 

750 try: 

751 # make a safe estimate 

752 call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO]) 

753 except UndefinedEvent: 

754 pass 

755 

756 # prune the nodes 

757 for function_id in list(self.functions.keys()): 

758 function = self.functions[function_id] 

759 if function.weight is not None: 

760 if function.weight < node_thres: 

761 del self.functions[function_id] 

762 

763 # prune file paths 

764 for function_id in list(self.functions.keys()): 

765 function = self.functions[function_id] 

766 if paths and function.filename and not any(function.filename.startswith(path) for path in paths): 

767 del self.functions[function_id] 

768 elif paths and function.module and not any((function.module.find(path)>-1) for path in paths): 

769 del self.functions[function_id] 

770 

771 # prune the edges 

772 for function in self.functions.values(): 

773 for callee_id in list(function.calls.keys()): 

774 call = function.calls[callee_id] 

775 if callee_id not in self.functions or call.weight is not None and call.weight < edge_thres: 

776 del function.calls[callee_id] 

777 

778 if color_nodes_by_selftime: 

779 weights = [] 

780 for function in self.functions.values(): 

781 try: 

782 weights.append(function[TIME_RATIO]) 

783 except UndefinedEvent: 

784 pass 

785 max_ratio = max(weights or [1]) 

786 

787 # apply rescaled weights for coloriung 

788 for function in self.functions.values(): 

789 try: 

790 function.weight = function[TIME_RATIO] / max_ratio 

791 except (ZeroDivisionError, UndefinedEvent): 

792 pass 

793 

794 def dump(self): 

795 for function in self.functions.values(): 

796 sys.stderr.write('Function %s:\n' % (function.name,)) 

797 self._dump_events(function.events) 

798 for call in function.calls.values(): 

799 callee = self.functions[call.callee_id] 

800 sys.stderr.write(' Call %s:\n' % (callee.name,)) 

801 self._dump_events(call.events) 

802 for cycle in self.cycles: 

803 sys.stderr.write('Cycle:\n') 

804 self._dump_events(cycle.events) 

805 for function in cycle.functions: 

806 sys.stderr.write(' Function %s\n' % (function.name,)) 

807 

808 def _dump_events(self, events): 

809 for event, value in events.items(): 

810 sys.stderr.write(' %s: %s\n' % (event.name, event.format(value))) 

811 

812 

813 

814######################################################################## 

815# Parsers 

816 

817 

818class Struct: 

819 """Masquerade a dictionary with a structure-like behavior.""" 

820 

821 def __init__(self, attrs = None): 

822 if attrs is None: 

823 attrs = {} 

824 self.__dict__['_attrs'] = attrs 

825 

826 def __getattr__(self, name): 

827 try: 

828 return self._attrs[name] 

829 except KeyError: 

830 raise AttributeError(name) 

831 

832 def __setattr__(self, name, value): 

833 self._attrs[name] = value 

834 

835 def __str__(self): 

836 return str(self._attrs) 

837 

838 def __repr__(self): 

839 return repr(self._attrs) 

840 

841 

842class ParseError(Exception): 

843 """Raised when parsing to signal mismatches.""" 

844 

845 def __init__(self, msg, line): 

846 Exception.__init__(self) 

847 self.msg = msg 

848 # TODO: store more source line information 

849 self.line = line 

850 

851 def __str__(self): 

852 return '%s: %r' % (self.msg, self.line) 

853 

854 

855class Parser: 

856 """Parser interface.""" 

857 

858 stdinInput = True 

859 multipleInput = False 

860 

861 def __init__(self): 

862 pass 

863 

864 def parse(self): 

865 raise NotImplementedError 

866 

867 

868class JsonParser(Parser): 

869 """Parser for a custom JSON representation of profile data. 

870 

871 See schema.json for details. 

872 """ 

873 

874 

875 def __init__(self, stream): 

876 Parser.__init__(self) 

877 self.stream = stream 

878 

879 def parse(self): 

880 

881 obj = json.load(self.stream) 

882 

883 assert obj['version'] == 0 

884 

885 profile = Profile() 

886 profile[SAMPLES] = 0 

887 

888 fns = obj['functions'] 

889 

890 for functionIndex in range(len(fns)): 

891 fn = fns[functionIndex] 

892 function = Function(functionIndex, fn['name']) 

893 try: 

894 function.module = fn['module'] 

895 except KeyError: 

896 pass 

897 try: 

898 function.process = fn['process'] 

899 except KeyError: 

900 pass 

901 function[SAMPLES] = 0 

902 function.called = 0 

903 profile.add_function(function) 

904 

905 for event in obj['events']: 

906 callchain = [] 

907 

908 for functionIndex in event['callchain']: 

909 function = profile.functions[functionIndex] 

910 callchain.append(function) 

911 

912 # increment the call count of the first in the callchain 

913 function = profile.functions[event['callchain'][0]] 

914 function.called = function.called + 1 

915 

916 cost = event['cost'][0] 

917 

918 callee = callchain[0] 

919 callee[SAMPLES] += cost 

920 profile[SAMPLES] += cost 

921 

922 for caller in callchain[1:]: 

923 try: 

924 call = caller.calls[callee.id] 

925 except KeyError: 

926 call = Call(callee.id) 

927 call[SAMPLES2] = cost 

928 caller.add_call(call) 

929 else: 

930 call[SAMPLES2] += cost 

931 

932 callee = caller 

933 

934 if False: 

935 profile.dump() 

936 

937 # compute derived data 

938 profile.validate() 

939 profile.find_cycles() 

940 profile.ratio(TIME_RATIO, SAMPLES) 

941 profile.call_ratios(SAMPLES2) 

942 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

943 

944 return profile 

945 

946 

947class LineParser(Parser): 

948 """Base class for parsers that read line-based formats.""" 

949 

950 def __init__(self, stream): 

951 Parser.__init__(self) 

952 self._stream = stream 

953 self.__line = None 

954 self.__eof = False 

955 self.line_no = 0 

956 

957 def readline(self): 

958 line = self._stream.readline() 

959 if not line: 

960 self.__line = '' 

961 self.__eof = True 

962 else: 

963 self.line_no += 1 

964 line = line.rstrip('\r\n') 

965 self.__line = line 

966 

967 def lookahead(self): 

968 assert self.__line is not None 

969 return self.__line 

970 

971 def consume(self): 

972 assert self.__line is not None 

973 line = self.__line 

974 self.readline() 

975 return line 

976 

977 def eof(self): 

978 assert self.__line is not None 

979 return self.__eof 

980 

981 

982XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF = range(4) 

983 

984 

985class XmlToken: 

986 

987 def __init__(self, type, name_or_data, attrs = None, line = None, column = None): 

988 assert type in (XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF) 

989 self.type = type 

990 self.name_or_data = name_or_data 

991 self.attrs = attrs 

992 self.line = line 

993 self.column = column 

994 

995 def __str__(self): 

996 if self.type == XML_ELEMENT_START: 

997 return '<' + self.name_or_data + ' ...>' 

998 if self.type == XML_ELEMENT_END: 

999 return '</' + self.name_or_data + '>' 

1000 if self.type == XML_CHARACTER_DATA: 

1001 return self.name_or_data 

1002 if self.type == XML_EOF: 

1003 return 'end of file' 

1004 assert 0 

1005 

1006 

1007class XmlTokenizer: 

1008 """Expat based XML tokenizer.""" 

1009 

1010 def __init__(self, fp, skip_ws = True): 

1011 self.fp = fp 

1012 self.tokens = [] 

1013 self.index = 0 

1014 self.final = False 

1015 self.skip_ws = skip_ws 

1016 

1017 self.character_pos = 0, 0 

1018 self.character_data = '' 

1019 

1020 self.parser = xml.parsers.expat.ParserCreate() 

1021 self.parser.StartElementHandler = self.handle_element_start 

1022 self.parser.EndElementHandler = self.handle_element_end 

1023 self.parser.CharacterDataHandler = self.handle_character_data 

1024 

1025 def handle_element_start(self, name, attributes): 

1026 self.finish_character_data() 

1027 line, column = self.pos() 

1028 token = XmlToken(XML_ELEMENT_START, name, attributes, line, column) 

1029 self.tokens.append(token) 

1030 

1031 def handle_element_end(self, name): 

1032 self.finish_character_data() 

1033 line, column = self.pos() 

1034 token = XmlToken(XML_ELEMENT_END, name, None, line, column) 

1035 self.tokens.append(token) 

1036 

1037 def handle_character_data(self, data): 

1038 if not self.character_data: 

1039 self.character_pos = self.pos() 

1040 self.character_data += data 

1041 

1042 def finish_character_data(self): 

1043 if self.character_data: 

1044 if not self.skip_ws or not self.character_data.isspace(): 

1045 line, column = self.character_pos 

1046 token = XmlToken(XML_CHARACTER_DATA, self.character_data, None, line, column) 

1047 self.tokens.append(token) 

1048 self.character_data = '' 

1049 

1050 def next(self): 

1051 size = 16*1024 

1052 while self.index >= len(self.tokens) and not self.final: 

1053 self.tokens = [] 

1054 self.index = 0 

1055 data = self.fp.read(size) 

1056 self.final = len(data) < size 

1057 self.parser.Parse(data, self.final) 

1058 if self.index >= len(self.tokens): 

1059 line, column = self.pos() 

1060 token = XmlToken(XML_EOF, None, None, line, column) 

1061 else: 

1062 token = self.tokens[self.index] 

1063 self.index += 1 

1064 return token 

1065 

1066 def pos(self): 

1067 return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber 

1068 

1069 

1070class XmlTokenMismatch(Exception): 

1071 

1072 def __init__(self, expected, found): 

1073 Exception.__init__(self) 

1074 self.expected = expected 

1075 self.found = found 

1076 

1077 def __str__(self): 

1078 return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found)) 

1079 

1080 

1081class XmlParser(Parser): 

1082 """Base XML document parser.""" 

1083 

1084 def __init__(self, fp): 

1085 Parser.__init__(self) 

1086 self.tokenizer = XmlTokenizer(fp) 

1087 self.consume() 

1088 

1089 def consume(self): 

1090 self.token = self.tokenizer.next() 

1091 

1092 def match_element_start(self, name): 

1093 return self.token.type == XML_ELEMENT_START and self.token.name_or_data == name 

1094 

1095 def match_element_end(self, name): 

1096 return self.token.type == XML_ELEMENT_END and self.token.name_or_data == name 

1097 

1098 def element_start(self, name): 

1099 while self.token.type == XML_CHARACTER_DATA: 

1100 self.consume() 

1101 if self.token.type != XML_ELEMENT_START: 

1102 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token) 

1103 if self.token.name_or_data != name: 

1104 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token) 

1105 attrs = self.token.attrs 

1106 self.consume() 

1107 return attrs 

1108 

1109 def element_end(self, name): 

1110 while self.token.type == XML_CHARACTER_DATA: 

1111 self.consume() 

1112 if self.token.type != XML_ELEMENT_END: 

1113 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token) 

1114 if self.token.name_or_data != name: 

1115 raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token) 

1116 self.consume() 

1117 

1118 def character_data(self, strip = True): 

1119 data = '' 

1120 while self.token.type == XML_CHARACTER_DATA: 

1121 data += self.token.name_or_data 

1122 self.consume() 

1123 if strip: 

1124 data = data.strip() 

1125 return data 

1126 

1127 

1128class GprofParser(Parser): 

1129 """Parser for GNU gprof output. 

1130 

1131 See also: 

1132 - Chapter "Interpreting gprof's Output" from the GNU gprof manual 

1133 http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph 

1134 - File "cg_print.c" from the GNU gprof source code 

1135 http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src 

1136 """ 

1137 

1138 def __init__(self, fp): 

1139 Parser.__init__(self) 

1140 self.fp = fp 

1141 self.functions = {} 

1142 self.cycles = {} 

1143 

1144 def readline(self): 

1145 line = self.fp.readline() 

1146 if not line: 

1147 sys.stderr.write('error: unexpected end of file\n') 

1148 sys.exit(1) 

1149 line = line.rstrip('\r\n') 

1150 return line 

1151 

1152 _int_re = re.compile(r'^\d+$') 

1153 _float_re = re.compile(r'^\d+\.\d+$') 

1154 

1155 def translate(self, mo): 

1156 """Extract a structure from a match object, while translating the types in the process.""" 

1157 attrs = {} 

1158 groupdict = mo.groupdict() 

1159 for name, value in groupdict.items(): 

1160 if value is None: 

1161 value = None 

1162 elif self._int_re.match(value): 

1163 value = int(value) 

1164 elif self._float_re.match(value): 

1165 value = float(value) 

1166 attrs[name] = (value) 

1167 return Struct(attrs) 

1168 

1169 _cg_header_re = re.compile( 

1170 # original gprof header 

1171 r'^\s+called/total\s+parents\s*$|' + 

1172 r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' + 

1173 r'^\s+called/total\s+children\s*$|' + 

1174 # GNU gprof header 

1175 r'^index\s+%\s+(time\s+)?self\s+children\s+called\s+name\s*$' 

1176 ) 

1177 

1178 _cg_ignore_re = re.compile( 

1179 # spontaneous 

1180 r'^\s+<spontaneous>\s*$|' 

1181 # internal calls (such as "mcount") 

1182 r'^.*\((\d+)\)$' 

1183 ) 

1184 

1185 _cg_primary_re = re.compile( 

1186 r'^\[(?P<index>\d+)\]?' + 

1187 r'\s+(?P<percentage_time>\d+\.\d+)' + 

1188 r'\s+(?P<self>\d+\.\d+)' + 

1189 r'\s+(?P<descendants>\d+\.\d+)' + 

1190 r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + 

1191 r'\s+(?P<name>\S.*?)' + 

1192 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1193 r'\s\[(\d+)\]$' 

1194 ) 

1195 

1196 _cg_parent_re = re.compile( 

1197 r'^\s+(?P<self>\d+\.\d+)?' + 

1198 r'\s+(?P<descendants>\d+\.\d+)?' + 

1199 r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' + 

1200 r'\s+(?P<name>\S.*?)' + 

1201 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1202 r'\s\[(?P<index>\d+)\]$' 

1203 ) 

1204 

1205 _cg_child_re = _cg_parent_re 

1206 

1207 _cg_cycle_header_re = re.compile( 

1208 r'^\[(?P<index>\d+)\]?' + 

1209 r'\s+(?P<percentage_time>\d+\.\d+)' + 

1210 r'\s+(?P<self>\d+\.\d+)' + 

1211 r'\s+(?P<descendants>\d+\.\d+)' + 

1212 r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + 

1213 r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' + 

1214 r'\s\[(\d+)\]$' 

1215 ) 

1216 

1217 _cg_cycle_member_re = re.compile( 

1218 r'^\s+(?P<self>\d+\.\d+)?' + 

1219 r'\s+(?P<descendants>\d+\.\d+)?' + 

1220 r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' + 

1221 r'\s+(?P<name>\S.*?)' + 

1222 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1223 r'\s\[(?P<index>\d+)\]$' 

1224 ) 

1225 

1226 _cg_sep_re = re.compile(r'^--+$') 

1227 

1228 def parse_function_entry(self, lines): 

1229 parents = [] 

1230 children = [] 

1231 

1232 while True: 

1233 if not lines: 

1234 sys.stderr.write('warning: unexpected end of entry\n') 

1235 line = lines.pop(0) 

1236 if line.startswith('['): 

1237 break 

1238 

1239 # read function parent line 

1240 mo = self._cg_parent_re.match(line) 

1241 if not mo: 

1242 if self._cg_ignore_re.match(line): 

1243 continue 

1244 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) 

1245 else: 

1246 parent = self.translate(mo) 

1247 parents.append(parent) 

1248 

1249 # read primary line 

1250 mo = self._cg_primary_re.match(line) 

1251 if not mo: 

1252 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) 

1253 return 

1254 else: 

1255 function = self.translate(mo) 

1256 

1257 while lines: 

1258 line = lines.pop(0) 

1259 

1260 # read function subroutine line 

1261 mo = self._cg_child_re.match(line) 

1262 if not mo: 

1263 if self._cg_ignore_re.match(line): 

1264 continue 

1265 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) 

1266 else: 

1267 child = self.translate(mo) 

1268 children.append(child) 

1269 

1270 function.parents = parents 

1271 function.children = children 

1272 

1273 self.functions[function.index] = function 

1274 

1275 def parse_cycle_entry(self, lines): 

1276 

1277 # read cycle header line 

1278 line = lines[0] 

1279 mo = self._cg_cycle_header_re.match(line) 

1280 if not mo: 

1281 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) 

1282 return 

1283 cycle = self.translate(mo) 

1284 

1285 # read cycle member lines 

1286 cycle.functions = [] 

1287 for line in lines[1:]: 

1288 mo = self._cg_cycle_member_re.match(line) 

1289 if not mo: 

1290 sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) 

1291 continue 

1292 call = self.translate(mo) 

1293 cycle.functions.append(call) 

1294 

1295 self.cycles[cycle.cycle] = cycle 

1296 

1297 def parse_cg_entry(self, lines): 

1298 if lines[0].startswith("["): 

1299 self.parse_cycle_entry(lines) 

1300 else: 

1301 self.parse_function_entry(lines) 

1302 

1303 def parse_cg(self): 

1304 """Parse the call graph.""" 

1305 

1306 # skip call graph header 

1307 while not self._cg_header_re.match(self.readline()): 

1308 pass 

1309 line = self.readline() 

1310 while self._cg_header_re.match(line): 

1311 line = self.readline() 

1312 

1313 # process call graph entries 

1314 entry_lines = [] 

1315 while line != '\014': # form feed 

1316 if line and not line.isspace(): 

1317 if self._cg_sep_re.match(line): 

1318 self.parse_cg_entry(entry_lines) 

1319 entry_lines = [] 

1320 else: 

1321 entry_lines.append(line) 

1322 line = self.readline() 

1323 

1324 def parse(self): 

1325 self.parse_cg() 

1326 self.fp.close() 

1327 

1328 profile = Profile() 

1329 profile[TIME] = 0.0 

1330 

1331 cycles = {} 

1332 for index in self.cycles: 

1333 cycles[index] = Cycle() 

1334 

1335 for entry in self.functions.values(): 

1336 # populate the function 

1337 function = Function(entry.index, entry.name) 

1338 function[TIME] = entry.self 

1339 if entry.called is not None: 

1340 function.called = entry.called 

1341 if entry.called_self is not None: 

1342 call = Call(entry.index) 

1343 call[CALLS] = entry.called_self 

1344 function.called += entry.called_self 

1345 

1346 # populate the function calls 

1347 for child in entry.children: 

1348 call = Call(child.index) 

1349 

1350 assert child.called is not None 

1351 call[CALLS] = child.called 

1352 

1353 if child.index not in self.functions: 

1354 # NOTE: functions that were never called but were discovered by gprof's 

1355 # static call graph analysis dont have a call graph entry so we need 

1356 # to add them here 

1357 missing = Function(child.index, child.name) 

1358 function[TIME] = 0.0 

1359 function.called = 0 

1360 profile.add_function(missing) 

1361 

1362 function.add_call(call) 

1363 

1364 profile.add_function(function) 

1365 

1366 if entry.cycle is not None: 

1367 try: 

1368 cycle = cycles[entry.cycle] 

1369 except KeyError: 

1370 sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle) 

1371 cycle = Cycle() 

1372 cycles[entry.cycle] = cycle 

1373 cycle.add_function(function) 

1374 

1375 profile[TIME] = profile[TIME] + function[TIME] 

1376 

1377 for cycle in cycles.values(): 

1378 profile.add_cycle(cycle) 

1379 

1380 # Compute derived events 

1381 profile.validate() 

1382 profile.ratio(TIME_RATIO, TIME) 

1383 profile.call_ratios(CALLS) 

1384 profile.integrate(TOTAL_TIME, TIME) 

1385 profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) 

1386 

1387 return profile 

1388 

1389 

1390# Clone&hack of GprofParser for VTune Amplifier XE 2013 gprof-cc output. 

1391# Tested only with AXE 2013 for Windows. 

1392# - Use total times as reported by AXE. 

1393# - In the absence of call counts, call ratios are faked from the relative 

1394# proportions of total time. This affects only the weighting of the calls. 

1395# - Different header, separator, and end marker. 

1396# - Extra whitespace after function names. 

1397# - You get a full entry for <spontaneous>, which does not have parents. 

1398# - Cycles do have parents. These are saved but unused (as they are 

1399# for functions). 

1400# - Disambiguated "unrecognized call graph entry" error messages. 

1401# Notes: 

1402# - Total time of functions as reported by AXE passes the val3 test. 

1403# - CPU Time:Children in the input is sometimes a negative number. This 

1404# value goes to the variable descendants, which is unused. 

1405# - The format of gprof-cc reports is unaffected by the use of 

1406# -knob enable-call-counts=true (no call counts, ever), or 

1407# -show-as=samples (results are quoted in seconds regardless). 

1408class AXEParser(Parser): 

1409 "Parser for VTune Amplifier XE 2013 gprof-cc report output." 

1410 

1411 def __init__(self, fp): 

1412 Parser.__init__(self) 

1413 self.fp = fp 

1414 self.functions = {} 

1415 self.cycles = {} 

1416 

1417 def readline(self): 

1418 line = self.fp.readline() 

1419 if not line: 

1420 sys.stderr.write('error: unexpected end of file\n') 

1421 sys.exit(1) 

1422 line = line.rstrip('\r\n') 

1423 return line 

1424 

1425 _int_re = re.compile(r'^\d+$') 

1426 _float_re = re.compile(r'^\d+\.\d+$') 

1427 

1428 def translate(self, mo): 

1429 """Extract a structure from a match object, while translating the types in the process.""" 

1430 attrs = {} 

1431 groupdict = mo.groupdict() 

1432 for name, value in groupdict.items(): 

1433 if value is None: 

1434 value = None 

1435 elif self._int_re.match(value): 

1436 value = int(value) 

1437 elif self._float_re.match(value): 

1438 value = float(value) 

1439 attrs[name] = (value) 

1440 return Struct(attrs) 

1441 

1442 _cg_header_re = re.compile( 

1443 '^Index |' 

1444 '^-----+ ' 

1445 ) 

1446 

1447 _cg_footer_re = re.compile(r'^Index\s+Function\s*$') 

1448 

1449 _cg_primary_re = re.compile( 

1450 r'^\[(?P<index>\d+)\]?' + 

1451 r'\s+(?P<percentage_time>\d+\.\d+)' + 

1452 r'\s+(?P<self>\d+\.\d+)' + 

1453 r'\s+(?P<descendants>\d+\.\d+)' + 

1454 r'\s+(?P<name>\S.*?)' + 

1455 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1456 r'\s+\[(\d+)\]' + 

1457 r'\s*$' 

1458 ) 

1459 

1460 _cg_parent_re = re.compile( 

1461 r'^\s+(?P<self>\d+\.\d+)?' + 

1462 r'\s+(?P<descendants>\d+\.\d+)?' + 

1463 r'\s+(?P<name>\S.*?)' + 

1464 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1465 r'(?:\s+\[(?P<index>\d+)\]\s*)?' + 

1466 r'\s*$' 

1467 ) 

1468 

1469 _cg_child_re = _cg_parent_re 

1470 

1471 _cg_cycle_header_re = re.compile( 

1472 r'^\[(?P<index>\d+)\]?' + 

1473 r'\s+(?P<percentage_time>\d+\.\d+)' + 

1474 r'\s+(?P<self>\d+\.\d+)' + 

1475 r'\s+(?P<descendants>\d+\.\d+)' + 

1476 r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' + 

1477 r'\s+\[(\d+)\]' + 

1478 r'\s*$' 

1479 ) 

1480 

1481 _cg_cycle_member_re = re.compile( 

1482 r'^\s+(?P<self>\d+\.\d+)?' + 

1483 r'\s+(?P<descendants>\d+\.\d+)?' + 

1484 r'\s+(?P<name>\S.*?)' + 

1485 r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + 

1486 r'\s+\[(?P<index>\d+)\]' + 

1487 r'\s*$' 

1488 ) 

1489 

1490 def parse_function_entry(self, lines): 

1491 parents = [] 

1492 children = [] 

1493 

1494 while True: 

1495 if not lines: 

1496 sys.stderr.write('warning: unexpected end of entry\n') 

1497 return 

1498 line = lines.pop(0) 

1499 if line.startswith('['): 

1500 break 

1501 

1502 # read function parent line 

1503 mo = self._cg_parent_re.match(line) 

1504 if not mo: 

1505 sys.stderr.write('warning: unrecognized call graph entry (1): %r\n' % line) 

1506 else: 

1507 parent = self.translate(mo) 

1508 if parent.name != '<spontaneous>': 

1509 parents.append(parent) 

1510 

1511 # read primary line 

1512 mo = self._cg_primary_re.match(line) 

1513 if not mo: 

1514 sys.stderr.write('warning: unrecognized call graph entry (2): %r\n' % line) 

1515 return 

1516 else: 

1517 function = self.translate(mo) 

1518 

1519 while lines: 

1520 line = lines.pop(0) 

1521 

1522 # read function subroutine line 

1523 mo = self._cg_child_re.match(line) 

1524 if not mo: 

1525 sys.stderr.write('warning: unrecognized call graph entry (3): %r\n' % line) 

1526 else: 

1527 child = self.translate(mo) 

1528 if child.name != '<spontaneous>': 

1529 children.append(child) 

1530 

1531 if function.name != '<spontaneous>': 

1532 function.parents = parents 

1533 function.children = children 

1534 

1535 self.functions[function.index] = function 

1536 

1537 def parse_cycle_entry(self, lines): 

1538 

1539 # Process the parents that were not there in gprof format. 

1540 parents = [] 

1541 while True: 

1542 if not lines: 

1543 sys.stderr.write('warning: unexpected end of cycle entry\n') 

1544 return 

1545 line = lines.pop(0) 

1546 if line.startswith('['): 

1547 break 

1548 mo = self._cg_parent_re.match(line) 

1549 if not mo: 

1550 sys.stderr.write('warning: unrecognized call graph entry (6): %r\n' % line) 

1551 else: 

1552 parent = self.translate(mo) 

1553 if parent.name != '<spontaneous>': 

1554 parents.append(parent) 

1555 

1556 # read cycle header line 

1557 mo = self._cg_cycle_header_re.match(line) 

1558 if not mo: 

1559 sys.stderr.write('warning: unrecognized call graph entry (4): %r\n' % line) 

1560 return 

1561 cycle = self.translate(mo) 

1562 

1563 # read cycle member lines 

1564 cycle.functions = [] 

1565 for line in lines[1:]: 

1566 mo = self._cg_cycle_member_re.match(line) 

1567 if not mo: 

1568 sys.stderr.write('warning: unrecognized call graph entry (5): %r\n' % line) 

1569 continue 

1570 call = self.translate(mo) 

1571 cycle.functions.append(call) 

1572 

1573 cycle.parents = parents 

1574 self.cycles[cycle.cycle] = cycle 

1575 

1576 def parse_cg_entry(self, lines): 

1577 if any("as a whole" in linelooper for linelooper in lines): 

1578 self.parse_cycle_entry(lines) 

1579 else: 

1580 self.parse_function_entry(lines) 

1581 

1582 def parse_cg(self): 

1583 """Parse the call graph.""" 

1584 

1585 # skip call graph header 

1586 line = self.readline() 

1587 while self._cg_header_re.match(line): 

1588 line = self.readline() 

1589 

1590 # process call graph entries 

1591 entry_lines = [] 

1592 # An EOF in readline terminates the program without returning. 

1593 while not self._cg_footer_re.match(line): 

1594 if line.isspace(): 

1595 self.parse_cg_entry(entry_lines) 

1596 entry_lines = [] 

1597 else: 

1598 entry_lines.append(line) 

1599 line = self.readline() 

1600 

1601 def parse(self): 

1602 sys.stderr.write('warning: for axe format, edge weights are unreliable estimates derived from function total times.\n') 

1603 self.parse_cg() 

1604 self.fp.close() 

1605 

1606 profile = Profile() 

1607 profile[TIME] = 0.0 

1608 

1609 cycles = {} 

1610 for index in self.cycles: 

1611 cycles[index] = Cycle() 

1612 

1613 for entry in self.functions.values(): 

1614 # populate the function 

1615 function = Function(entry.index, entry.name) 

1616 function[TIME] = entry.self 

1617 function[TOTAL_TIME_RATIO] = entry.percentage_time / 100.0 

1618 

1619 # populate the function calls 

1620 for child in entry.children: 

1621 call = Call(child.index) 

1622 # The following bogus value affects only the weighting of 

1623 # the calls. 

1624 call[TOTAL_TIME_RATIO] = function[TOTAL_TIME_RATIO] 

1625 

1626 if child.index not in self.functions: 

1627 # NOTE: functions that were never called but were discovered by gprof's 

1628 # static call graph analysis dont have a call graph entry so we need 

1629 # to add them here 

1630 # FIXME: Is this applicable? 

1631 missing = Function(child.index, child.name) 

1632 function[TIME] = 0.0 

1633 profile.add_function(missing) 

1634 

1635 function.add_call(call) 

1636 

1637 profile.add_function(function) 

1638 

1639 if entry.cycle is not None: 

1640 try: 

1641 cycle = cycles[entry.cycle] 

1642 except KeyError: 

1643 sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle) 

1644 cycle = Cycle() 

1645 cycles[entry.cycle] = cycle 

1646 cycle.add_function(function) 

1647 

1648 profile[TIME] = profile[TIME] + function[TIME] 

1649 

1650 for cycle in cycles.values(): 

1651 profile.add_cycle(cycle) 

1652 

1653 # Compute derived events. 

1654 profile.validate() 

1655 profile.ratio(TIME_RATIO, TIME) 

1656 # Lacking call counts, fake call ratios based on total times. 

1657 profile.call_ratios(TOTAL_TIME_RATIO) 

1658 # The TOTAL_TIME_RATIO of functions is already set. Propagate that 

1659 # total time to the calls. (TOTAL_TIME is neither set nor used.) 

1660 for function in profile.functions.values(): 

1661 for call in function.calls.values(): 

1662 if call.ratio is not None: 

1663 callee = profile.functions[call.callee_id] 

1664 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] 

1665 

1666 return profile 

1667 

1668 

1669class CallgrindParser(LineParser): 

1670 """Parser for valgrind's callgrind tool. 

1671 

1672 See also: 

1673 - https://valgrind.org/docs/manual/cl-format.html 

1674 """ 

1675 

1676 _call_re = re.compile(r'^calls=\s*(\d+)\s+((\d+|\+\d+|-\d+|\*)\s+)+$') 

1677 

1678 def __init__(self, infile): 

1679 LineParser.__init__(self, infile) 

1680 

1681 # Textual positions 

1682 self.position_ids = {} 

1683 self.positions = {} 

1684 

1685 # Numeric positions 

1686 self.num_positions = 1 

1687 self.cost_positions = ['line'] 

1688 self.last_positions = [0] 

1689 

1690 # Events 

1691 self.num_events = 0 

1692 self.cost_events = [] 

1693 

1694 self.profile = Profile() 

1695 self.profile[SAMPLES] = 0 

1696 

1697 def parse(self): 

1698 # read lookahead 

1699 self.readline() 

1700 

1701 self.parse_key('version') 

1702 self.parse_key('creator') 

1703 while self.parse_part(): 

1704 pass 

1705 if not self.eof(): 

1706 sys.stderr.write('warning: line %u: unexpected line\n' % self.line_no) 

1707 sys.stderr.write('%s\n' % self.lookahead()) 

1708 

1709 # compute derived data 

1710 self.profile.validate() 

1711 self.profile.find_cycles() 

1712 self.profile.ratio(TIME_RATIO, SAMPLES) 

1713 self.profile.call_ratios(SAMPLES2) 

1714 self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

1715 

1716 return self.profile 

1717 

1718 def parse_part(self): 

1719 if not self.parse_header_line(): 

1720 return False 

1721 while self.parse_header_line(): 

1722 pass 

1723 if not self.parse_body_line(): 

1724 return False 

1725 while self.parse_body_line(): 

1726 pass 

1727 return True 

1728 

1729 def parse_header_line(self): 

1730 return \ 

1731 self.parse_empty() or \ 

1732 self.parse_comment() or \ 

1733 self.parse_part_detail() or \ 

1734 self.parse_description() or \ 

1735 self.parse_event_specification() or \ 

1736 self.parse_cost_line_def() or \ 

1737 self.parse_cost_summary() 

1738 

1739 _detail_keys = set(('cmd', 'pid', 'thread', 'part')) 

1740 

1741 def parse_part_detail(self): 

1742 return self.parse_keys(self._detail_keys) 

1743 

1744 def parse_description(self): 

1745 return self.parse_key('desc') is not None 

1746 

1747 def parse_event_specification(self): 

1748 event = self.parse_key('event') 

1749 if event is None: 

1750 return False 

1751 return True 

1752 

1753 def parse_cost_line_def(self): 

1754 pair = self.parse_keys(('events', 'positions')) 

1755 if pair is None: 

1756 return False 

1757 key, value = pair 

1758 items = value.split() 

1759 if key == 'events': 

1760 self.num_events = len(items) 

1761 self.cost_events = items 

1762 if key == 'positions': 

1763 self.num_positions = len(items) 

1764 self.cost_positions = items 

1765 self.last_positions = [0]*self.num_positions 

1766 return True 

1767 

1768 def parse_cost_summary(self): 

1769 pair = self.parse_keys(('summary', 'totals')) 

1770 if pair is None: 

1771 return False 

1772 return True 

1773 

1774 def parse_body_line(self): 

1775 return \ 

1776 self.parse_empty() or \ 

1777 self.parse_comment() or \ 

1778 self.parse_cost_line() or \ 

1779 self.parse_position_spec() or \ 

1780 self.parse_association_spec() 

1781 

1782 __subpos_re = r'(0x[0-9a-fA-F]+|\d+|\+\d+|-\d+|\*)' 

1783 _cost_re = re.compile(r'^' + 

1784 __subpos_re + r'( +' + __subpos_re + r')*' + 

1785 r'( +\d+)*' + 

1786 '$') 

1787 

1788 def parse_cost_line(self, calls=None): 

1789 line = self.lookahead().rstrip() 

1790 mo = self._cost_re.match(line) 

1791 if not mo: 

1792 return False 

1793 

1794 function = self.get_function() 

1795 

1796 if calls is None: 

1797 # Unlike other aspects, call object (cob) is relative not to the 

1798 # last call object, but to the caller's object (ob), so try to 

1799 # update it when processing a functions cost line 

1800 try: 

1801 self.positions['cob'] = self.positions['ob'] 

1802 except KeyError: 

1803 pass 

1804 

1805 values = line.split() 

1806 assert len(values) <= self.num_positions + self.num_events 

1807 

1808 positions = values[0 : self.num_positions] 

1809 events = values[self.num_positions : ] 

1810 events += ['0']*(self.num_events - len(events)) 

1811 

1812 for i in range(self.num_positions): 

1813 position = positions[i] 

1814 if position == '*': 

1815 position = self.last_positions[i] 

1816 elif position[0] in '-+': 

1817 position = self.last_positions[i] + int(position) 

1818 elif position.startswith('0x'): 

1819 position = int(position, 16) 

1820 else: 

1821 position = int(position) 

1822 self.last_positions[i] = position 

1823 

1824 events = [float(event) for event in events] 

1825 

1826 if calls is None: 

1827 function[SAMPLES] += events[0] 

1828 self.profile[SAMPLES] += events[0] 

1829 else: 

1830 callee = self.get_callee() 

1831 callee.called += calls 

1832 

1833 try: 

1834 call = function.calls[callee.id] 

1835 except KeyError: 

1836 call = Call(callee.id) 

1837 call[CALLS] = calls 

1838 call[SAMPLES2] = events[0] 

1839 function.add_call(call) 

1840 else: 

1841 call[CALLS] += calls 

1842 call[SAMPLES2] += events[0] 

1843 

1844 self.consume() 

1845 return True 

1846 

1847 def parse_association_spec(self): 

1848 line = self.lookahead() 

1849 if not line.startswith('calls='): 

1850 return False 

1851 

1852 _, values = line.split('=', 1) 

1853 values = values.strip().split() 

1854 calls = int(values[0]) 

1855 call_position = values[1:] 

1856 self.consume() 

1857 

1858 self.parse_cost_line(calls) 

1859 

1860 return True 

1861 

1862 _position_re = re.compile(r'^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?') 

1863 

1864 _position_table_map = { 

1865 'ob': 'ob', 

1866 'fl': 'fl', 

1867 'fi': 'fl', 

1868 'fe': 'fl', 

1869 'fn': 'fn', 

1870 'cob': 'ob', 

1871 'cfl': 'fl', 

1872 'cfi': 'fl', 

1873 'cfe': 'fl', 

1874 'cfn': 'fn', 

1875 'jfi': 'fl', 

1876 } 

1877 

1878 _position_map = { 

1879 'ob': 'ob', 

1880 'fl': 'fl', 

1881 'fi': 'fl', 

1882 'fe': 'fl', 

1883 'fn': 'fn', 

1884 'cob': 'cob', 

1885 'cfl': 'cfl', 

1886 'cfi': 'cfl', 

1887 'cfe': 'cfl', 

1888 'cfn': 'cfn', 

1889 'jfi': 'jfi', 

1890 } 

1891 

1892 def parse_position_spec(self): 

1893 line = self.lookahead() 

1894 

1895 if line.startswith('jump=') or line.startswith('jcnd='): 

1896 self.consume() 

1897 return True 

1898 

1899 mo = self._position_re.match(line) 

1900 if not mo: 

1901 return False 

1902 

1903 position, id, name = mo.groups() 

1904 if id: 

1905 table = self._position_table_map[position] 

1906 if name: 

1907 self.position_ids[(table, id)] = name 

1908 else: 

1909 name = self.position_ids.get((table, id), '') 

1910 self.positions[self._position_map[position]] = name 

1911 

1912 self.consume() 

1913 return True 

1914 

1915 def parse_empty(self): 

1916 if self.eof(): 

1917 return False 

1918 line = self.lookahead() 

1919 if line.strip(): 

1920 return False 

1921 self.consume() 

1922 return True 

1923 

1924 def parse_comment(self): 

1925 line = self.lookahead() 

1926 if not line.startswith('#'): 

1927 return False 

1928 self.consume() 

1929 return True 

1930 

1931 _key_re = re.compile(r'^(\w+):') 

1932 

1933 def parse_key(self, key): 

1934 pair = self.parse_keys((key,)) 

1935 if not pair: 

1936 return None 

1937 key, value = pair 

1938 return value 

1939 

1940 def parse_keys(self, keys): 

1941 line = self.lookahead() 

1942 mo = self._key_re.match(line) 

1943 if not mo: 

1944 return None 

1945 key, value = line.split(':', 1) 

1946 if key not in keys: 

1947 return None 

1948 value = value.strip() 

1949 self.consume() 

1950 return key, value 

1951 

1952 def make_function(self, module, filename, name): 

1953 # FIXME: module and filename are not being tracked reliably 

1954 #id = '|'.join((module, filename, name)) 

1955 id = name 

1956 try: 

1957 function = self.profile.functions[id] 

1958 except KeyError: 

1959 function = Function(id, name) 

1960 if module: 

1961 function.module = os.path.basename(module) 

1962 function[SAMPLES] = 0 

1963 function.called = 0 

1964 self.profile.add_function(function) 

1965 return function 

1966 

1967 def get_function(self): 

1968 module = self.positions.get('ob', '') 

1969 filename = self.positions.get('fl', '') 

1970 function = self.positions.get('fn', '') 

1971 return self.make_function(module, filename, function) 

1972 

1973 def get_callee(self): 

1974 module = self.positions.get('cob', '') 

1975 filename = self.positions.get('cfi', '') 

1976 function = self.positions.get('cfn', '') 

1977 return self.make_function(module, filename, function) 

1978 

1979 def readline(self): 

1980 # Override LineParser.readline to ignore comment lines 

1981 while True: 

1982 LineParser.readline(self) 

1983 if self.eof() or not self.lookahead().startswith('#'): 

1984 break 

1985 

1986 

1987class PerfParser(LineParser): 

1988 """Parser for linux perf callgraph output. 

1989 

1990 It expects output generated with 

1991 

1992 perf record -g 

1993 perf script | gprof2dot.py --format=perf 

1994 """ 

1995 

1996 def __init__(self, infile): 

1997 LineParser.__init__(self, infile) 

1998 self.profile = Profile() 

1999 

2000 def readline(self): 

2001 # Override LineParser.readline to ignore comment lines 

2002 while True: 

2003 LineParser.readline(self) 

2004 if self.eof() or not self.lookahead().startswith('#'): 

2005 break 

2006 

2007 def parse(self): 

2008 # read lookahead 

2009 self.readline() 

2010 

2011 profile = self.profile 

2012 profile[SAMPLES] = 0 

2013 while not self.eof(): 

2014 self.parse_event() 

2015 

2016 # compute derived data 

2017 profile.validate() 

2018 profile.find_cycles() 

2019 profile.ratio(TIME_RATIO, SAMPLES) 

2020 profile.call_ratios(SAMPLES2) 

2021 if totalMethod == "callratios": 

2022 # Heuristic approach. TOTAL_SAMPLES is unused. 

2023 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2024 elif totalMethod == "callstacks": 

2025 # Use the actual call chains for functions. 

2026 profile[TOTAL_SAMPLES] = profile[SAMPLES] 

2027 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES) 

2028 # Then propagate that total time to the calls. 

2029 for function in profile.functions.values(): 

2030 for call in function.calls.values(): 

2031 if call.ratio is not None: 

2032 callee = profile.functions[call.callee_id] 

2033 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] 

2034 else: 

2035 assert False 

2036 

2037 return profile 

2038 

2039 def parse_event(self): 

2040 if self.eof(): 

2041 return 

2042 

2043 line = self.consume() 

2044 assert line 

2045 

2046 callchain = self.parse_callchain() 

2047 if not callchain: 

2048 return 

2049 

2050 callee = callchain[0] 

2051 callee[SAMPLES] += 1 

2052 self.profile[SAMPLES] += 1 

2053 

2054 for caller in callchain[1:]: 

2055 try: 

2056 call = caller.calls[callee.id] 

2057 except KeyError: 

2058 call = Call(callee.id) 

2059 call[SAMPLES2] = 1 

2060 caller.add_call(call) 

2061 else: 

2062 call[SAMPLES2] += 1 

2063 

2064 callee = caller 

2065 

2066 # Increment TOTAL_SAMPLES only once on each function. 

2067 stack = set(callchain) 

2068 for function in stack: 

2069 function[TOTAL_SAMPLES] += 1 

2070 

2071 def parse_callchain(self): 

2072 callchain = [] 

2073 while self.lookahead(): 

2074 function = self.parse_call() 

2075 if function is None: 

2076 break 

2077 callchain.append(function) 

2078 if self.lookahead() == '': 

2079 self.consume() 

2080 return callchain 

2081 

2082 call_re = re.compile(r'^\s+(?P<address>[0-9a-fA-F]+)\s+(?P<symbol>.*)\s+\((?P<module>.*)\)$') 

2083 addr2_re = re.compile(r'\+0x[0-9a-fA-F]+$') 

2084 

2085 def parse_call(self): 

2086 line = self.consume() 

2087 mo = self.call_re.match(line) 

2088 assert mo 

2089 if not mo: 

2090 return None 

2091 

2092 function_name = mo.group('symbol') 

2093 

2094 # If present, amputate program counter from function name. 

2095 if function_name: 

2096 function_name = re.sub(self.addr2_re, '', function_name) 

2097 

2098 if not function_name or function_name == '[unknown]': 

2099 function_name = mo.group('address') 

2100 

2101 module = mo.group('module') 

2102 

2103 function_id = function_name + ':' + module 

2104 

2105 try: 

2106 function = self.profile.functions[function_id] 

2107 except KeyError: 

2108 function = Function(function_id, function_name) 

2109 function.module = os.path.basename(module) 

2110 function[SAMPLES] = 0 

2111 function[TOTAL_SAMPLES] = 0 

2112 self.profile.add_function(function) 

2113 

2114 return function 

2115 

2116 

2117class HProfParser(LineParser): 

2118 """Parser for java hprof output 

2119 

2120 See also: 

2121 - http://java.sun.com/developer/technicalArticles/Programming/HPROF.html 

2122 """ 

2123 

2124 trace_re = re.compile(r'\t(.*)\((.*):(.*)\)') 

2125 trace_id_re = re.compile(r'^TRACE (\d+):$') 

2126 

2127 def __init__(self, infile): 

2128 LineParser.__init__(self, infile) 

2129 self.traces = {} 

2130 self.samples = {} 

2131 

2132 def parse(self): 

2133 # read lookahead 

2134 self.readline() 

2135 

2136 while not self.lookahead().startswith('------'): self.consume() 

2137 while not self.lookahead().startswith('TRACE '): self.consume() 

2138 

2139 self.parse_traces() 

2140 

2141 while not self.lookahead().startswith('CPU'): 

2142 self.consume() 

2143 

2144 self.parse_samples() 

2145 

2146 # populate the profile 

2147 profile = Profile() 

2148 profile[SAMPLES] = 0 

2149 

2150 functions = {} 

2151 

2152 # build up callgraph 

2153 for id, trace in self.traces.items(): 

2154 if not id in self.samples: continue 

2155 mtime = self.samples[id][0] 

2156 last = None 

2157 

2158 for func, file, line in trace: 

2159 if not func in functions: 

2160 function = Function(func, func) 

2161 function[SAMPLES] = 0 

2162 profile.add_function(function) 

2163 functions[func] = function 

2164 

2165 function = functions[func] 

2166 # allocate time to the deepest method in the trace 

2167 if not last: 

2168 function[SAMPLES] += mtime 

2169 profile[SAMPLES] += mtime 

2170 else: 

2171 c = function.get_call(last) 

2172 c[SAMPLES2] += mtime 

2173 

2174 last = func 

2175 

2176 # compute derived data 

2177 profile.validate() 

2178 profile.find_cycles() 

2179 profile.ratio(TIME_RATIO, SAMPLES) 

2180 profile.call_ratios(SAMPLES2) 

2181 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2182 

2183 return profile 

2184 

2185 def parse_traces(self): 

2186 while self.lookahead().startswith('TRACE '): 

2187 self.parse_trace() 

2188 

2189 def parse_trace(self): 

2190 l = self.consume() 

2191 mo = self.trace_id_re.match(l) 

2192 tid = mo.group(1) 

2193 last = None 

2194 trace = [] 

2195 

2196 while self.lookahead().startswith('\t'): 

2197 l = self.consume() 

2198 match = self.trace_re.search(l) 

2199 if not match: 

2200 #sys.stderr.write('Invalid line: %s\n' % l) 

2201 break 

2202 else: 

2203 function_name, file, line = match.groups() 

2204 trace += [(function_name, file, line)] 

2205 

2206 self.traces[int(tid)] = trace 

2207 

2208 def parse_samples(self): 

2209 self.consume() 

2210 self.consume() 

2211 

2212 while not self.lookahead().startswith('CPU'): 

2213 rank, percent_self, percent_accum, count, traceid, method = self.lookahead().split() 

2214 self.samples[int(traceid)] = (int(count), method) 

2215 self.consume() 

2216 

2217 

2218class SysprofParser(XmlParser): 

2219 

2220 def __init__(self, stream): 

2221 XmlParser.__init__(self, stream) 

2222 

2223 def parse(self): 

2224 objects = {} 

2225 nodes = {} 

2226 

2227 self.element_start('profile') 

2228 while self.token.type == XML_ELEMENT_START: 

2229 if self.token.name_or_data == 'objects': 

2230 assert not objects 

2231 objects = self.parse_items('objects') 

2232 elif self.token.name_or_data == 'nodes': 

2233 assert not nodes 

2234 nodes = self.parse_items('nodes') 

2235 else: 

2236 self.parse_value(self.token.name_or_data) 

2237 self.element_end('profile') 

2238 

2239 return self.build_profile(objects, nodes) 

2240 

2241 def parse_items(self, name): 

2242 assert name[-1] == 's' 

2243 items = {} 

2244 self.element_start(name) 

2245 while self.token.type == XML_ELEMENT_START: 

2246 id, values = self.parse_item(name[:-1]) 

2247 assert id not in items 

2248 items[id] = values 

2249 self.element_end(name) 

2250 return items 

2251 

2252 def parse_item(self, name): 

2253 attrs = self.element_start(name) 

2254 id = int(attrs['id']) 

2255 values = self.parse_values() 

2256 self.element_end(name) 

2257 return id, values 

2258 

2259 def parse_values(self): 

2260 values = {} 

2261 while self.token.type == XML_ELEMENT_START: 

2262 name = self.token.name_or_data 

2263 value = self.parse_value(name) 

2264 assert name not in values 

2265 values[name] = value 

2266 return values 

2267 

2268 def parse_value(self, tag): 

2269 self.element_start(tag) 

2270 value = self.character_data() 

2271 self.element_end(tag) 

2272 if value.isdigit(): 

2273 return int(value) 

2274 if value.startswith('"') and value.endswith('"'): 

2275 return value[1:-1] 

2276 return value 

2277 

2278 def build_profile(self, objects, nodes): 

2279 profile = Profile() 

2280 

2281 profile[SAMPLES] = 0 

2282 for id, object in objects.items(): 

2283 # Ignore fake objects (process names, modules, "Everything", "kernel", etc.) 

2284 if object['self'] == 0: 

2285 continue 

2286 

2287 function = Function(id, object['name']) 

2288 function[SAMPLES] = object['self'] 

2289 profile.add_function(function) 

2290 profile[SAMPLES] += function[SAMPLES] 

2291 

2292 for id, node in nodes.items(): 

2293 # Ignore fake calls 

2294 if node['self'] == 0: 

2295 continue 

2296 

2297 # Find a non-ignored parent 

2298 parent_id = node['parent'] 

2299 while parent_id != 0: 

2300 parent = nodes[parent_id] 

2301 caller_id = parent['object'] 

2302 if objects[caller_id]['self'] != 0: 

2303 break 

2304 parent_id = parent['parent'] 

2305 if parent_id == 0: 

2306 continue 

2307 

2308 callee_id = node['object'] 

2309 

2310 assert objects[caller_id]['self'] 

2311 assert objects[callee_id]['self'] 

2312 

2313 function = profile.functions[caller_id] 

2314 

2315 samples = node['self'] 

2316 try: 

2317 call = function.calls[callee_id] 

2318 except KeyError: 

2319 call = Call(callee_id) 

2320 call[SAMPLES2] = samples 

2321 function.add_call(call) 

2322 else: 

2323 call[SAMPLES2] += samples 

2324 

2325 # Compute derived events 

2326 profile.validate() 

2327 profile.find_cycles() 

2328 profile.ratio(TIME_RATIO, SAMPLES) 

2329 profile.call_ratios(SAMPLES2) 

2330 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2331 

2332 return profile 

2333 

2334 

2335class XPerfParser(Parser): 

2336 """Parser for CSVs generated by XPerf, from Microsoft Windows Performance Tools. 

2337 """ 

2338 

2339 def __init__(self, stream): 

2340 Parser.__init__(self) 

2341 self.stream = stream 

2342 self.profile = Profile() 

2343 self.profile[SAMPLES] = 0 

2344 self.column = {} 

2345 

2346 def parse(self): 

2347 import csv 

2348 reader = csv.reader( 

2349 self.stream, 

2350 delimiter = ',', 

2351 quotechar = None, 

2352 escapechar = None, 

2353 doublequote = False, 

2354 skipinitialspace = True, 

2355 lineterminator = '\r\n', 

2356 quoting = csv.QUOTE_NONE) 

2357 header = True 

2358 for row in reader: 

2359 if header: 

2360 self.parse_header(row) 

2361 header = False 

2362 else: 

2363 self.parse_row(row) 

2364 

2365 # compute derived data 

2366 self.profile.validate() 

2367 self.profile.find_cycles() 

2368 self.profile.ratio(TIME_RATIO, SAMPLES) 

2369 self.profile.call_ratios(SAMPLES2) 

2370 self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2371 

2372 return self.profile 

2373 

2374 def parse_header(self, row): 

2375 for column in range(len(row)): 

2376 name = row[column] 

2377 assert name not in self.column 

2378 self.column[name] = column 

2379 

2380 def parse_row(self, row): 

2381 fields = {} 

2382 for name, column in self.column.items(): 

2383 value = row[column] 

2384 for factory in int, float: 

2385 try: 

2386 value = factory(value) 

2387 except ValueError: 

2388 pass 

2389 else: 

2390 break 

2391 fields[name] = value 

2392 

2393 process = fields['Process Name'] 

2394 symbol = fields['Module'] + '!' + fields['Function'] 

2395 weight = fields['Weight'] 

2396 count = fields['Count'] 

2397 

2398 if process == 'Idle': 

2399 return 

2400 

2401 function = self.get_function(process, symbol) 

2402 function[SAMPLES] += weight * count 

2403 self.profile[SAMPLES] += weight * count 

2404 

2405 stack = fields['Stack'] 

2406 if stack != '?': 

2407 stack = stack.split('/') 

2408 assert stack[0] == '[Root]' 

2409 if stack[-1] != symbol: 

2410 # XXX: some cases the sampled function does not appear in the stack 

2411 stack.append(symbol) 

2412 caller = None 

2413 for symbol in stack[1:]: 

2414 callee = self.get_function(process, symbol) 

2415 if caller is not None: 

2416 try: 

2417 call = caller.calls[callee.id] 

2418 except KeyError: 

2419 call = Call(callee.id) 

2420 call[SAMPLES2] = count 

2421 caller.add_call(call) 

2422 else: 

2423 call[SAMPLES2] += count 

2424 caller = callee 

2425 

2426 def get_function(self, process, symbol): 

2427 function_id = process + '!' + symbol 

2428 

2429 try: 

2430 function = self.profile.functions[function_id] 

2431 except KeyError: 

2432 module, name = symbol.split('!', 1) 

2433 function = Function(function_id, name) 

2434 function.process = process 

2435 function.module = module 

2436 function[SAMPLES] = 0 

2437 self.profile.add_function(function) 

2438 

2439 return function 

2440 

2441 

2442class SleepyParser(Parser): 

2443 """Parser for GNU gprof output. 

2444 

2445 See also: 

2446 - http://www.codersnotes.com/sleepy/ 

2447 - http://sleepygraph.sourceforge.net/ 

2448 """ 

2449 

2450 stdinInput = False 

2451 

2452 def __init__(self, filename): 

2453 Parser.__init__(self) 

2454 

2455 from zipfile import ZipFile 

2456 

2457 self.database = ZipFile(filename) 

2458 

2459 self.symbols = {} 

2460 self.calls = {} 

2461 

2462 self.profile = Profile() 

2463 

2464 _symbol_re = re.compile( 

2465 r'^(?P<id>\w+)' + 

2466 r'\s+"(?P<module>[^"]*)"' + 

2467 r'\s+"(?P<procname>[^"]*)"' + 

2468 r'\s+"(?P<sourcefile>[^"]*)"' + 

2469 r'\s+(?P<sourceline>\d+)$' 

2470 ) 

2471 

2472 def openEntry(self, name): 

2473 # Some versions of verysleepy use lowercase filenames 

2474 for database_name in self.database.namelist(): 

2475 if name.lower() == database_name.lower(): 

2476 name = database_name 

2477 break 

2478 

2479 return self.database.open(name, 'r') 

2480 

2481 def parse_symbols(self): 

2482 for line in self.openEntry('Symbols.txt'): 

2483 line = line.decode('UTF-8').rstrip('\r\n') 

2484 

2485 mo = self._symbol_re.match(line) 

2486 if mo: 

2487 symbol_id, module, procname, sourcefile, sourceline = mo.groups() 

2488 

2489 function_id = ':'.join([module, procname]) 

2490 

2491 try: 

2492 function = self.profile.functions[function_id] 

2493 except KeyError: 

2494 function = Function(function_id, procname) 

2495 function.module = module 

2496 function[SAMPLES] = 0 

2497 self.profile.add_function(function) 

2498 

2499 self.symbols[symbol_id] = function 

2500 

2501 def parse_callstacks(self): 

2502 for line in self.openEntry('Callstacks.txt'): 

2503 line = line.decode('UTF-8').rstrip('\r\n') 

2504 

2505 fields = line.split() 

2506 samples = float(fields[0]) 

2507 callstack = fields[1:] 

2508 

2509 callstack = [self.symbols[symbol_id] for symbol_id in callstack] 

2510 

2511 callee = callstack[0] 

2512 

2513 callee[SAMPLES] += samples 

2514 self.profile[SAMPLES] += samples 

2515 

2516 for caller in callstack[1:]: 

2517 try: 

2518 call = caller.calls[callee.id] 

2519 except KeyError: 

2520 call = Call(callee.id) 

2521 call[SAMPLES2] = samples 

2522 caller.add_call(call) 

2523 else: 

2524 call[SAMPLES2] += samples 

2525 

2526 callee = caller 

2527 

2528 def parse(self): 

2529 profile = self.profile 

2530 profile[SAMPLES] = 0 

2531 

2532 self.parse_symbols() 

2533 self.parse_callstacks() 

2534 

2535 # Compute derived events 

2536 profile.validate() 

2537 profile.find_cycles() 

2538 profile.ratio(TIME_RATIO, SAMPLES) 

2539 profile.call_ratios(SAMPLES2) 

2540 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2541 

2542 return profile 

2543 

2544 

2545class PstatsParser: 

2546 """Parser python profiling statistics saved with te pstats module.""" 

2547 

2548 stdinInput = False 

2549 multipleInput = True 

2550 

2551 def __init__(self, *filename): 

2552 import pstats 

2553 try: 

2554 self.stats = pstats.Stats(*filename) 

2555 except ValueError: 

2556 sys.stderr.write('error: failed to load %s, maybe they are generated by different python version?\n' % ', '.join(filename)) 

2557 sys.exit(1) 

2558 self.profile = Profile() 

2559 self.function_ids = {} 

2560 

2561 def get_function_name(self, key): 

2562 filename, line, name = key 

2563 module = os.path.splitext(filename)[0] 

2564 module = os.path.basename(module) 

2565 return "%s:%d:%s" % (module, line, name) 

2566 

2567 def get_function(self, key): 

2568 try: 

2569 id = self.function_ids[key] 

2570 except KeyError: 

2571 id = len(self.function_ids) 

2572 name = self.get_function_name(key) 

2573 function = Function(id, name) 

2574 function.filename = key[0] 

2575 self.profile.functions[id] = function 

2576 self.function_ids[key] = id 

2577 else: 

2578 function = self.profile.functions[id] 

2579 return function 

2580 

2581 def parse(self): 

2582 self.profile[TIME] = 0.0 

2583 self.profile[TOTAL_TIME] = self.stats.total_tt 

2584 for fn, (cc, nc, tt, ct, callers) in self.stats.stats.items(): 

2585 callee = self.get_function(fn) 

2586 callee.called = nc 

2587 callee[TOTAL_TIME] = ct 

2588 callee[TIME] = tt 

2589 self.profile[TIME] += tt 

2590 self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct) 

2591 for fn, value in callers.items(): 

2592 caller = self.get_function(fn) 

2593 call = Call(callee.id) 

2594 if isinstance(value, tuple): 

2595 for i in range(0, len(value), 4): 

2596 nc, cc, tt, ct = value[i:i+4] 

2597 if CALLS in call: 

2598 call[CALLS] += cc 

2599 else: 

2600 call[CALLS] = cc 

2601 

2602 if TOTAL_TIME in call: 

2603 call[TOTAL_TIME] += ct 

2604 else: 

2605 call[TOTAL_TIME] = ct 

2606 

2607 else: 

2608 call[CALLS] = value 

2609 call[TOTAL_TIME] = ratio(value, nc)*ct 

2610 

2611 caller.add_call(call) 

2612 

2613 if False: 

2614 self.stats.print_stats() 

2615 self.stats.print_callees() 

2616 

2617 # Compute derived events 

2618 self.profile.validate() 

2619 self.profile.ratio(TIME_RATIO, TIME) 

2620 self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) 

2621 

2622 return self.profile 

2623 

2624class DtraceParser(LineParser): 

2625 """Parser for linux perf callgraph output. 

2626 

2627 It expects output generated with 

2628 

2629 # Refer to https://github.com/brendangregg/FlameGraph#dtrace 

2630 # 60 seconds of user-level stacks, including time spent in-kernel, for PID 12345 at 97 Hertz 

2631 sudo dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks 

2632 

2633 # The dtrace output 

2634 gprof2dot.py -f dtrace out.user_stacks 

2635 

2636 # Notice: sometimes, the dtrace outputs format may be latin-1, and gprof2dot will fail to parse it. 

2637 # To solve this problem, you should use iconv to convert to UTF-8 explicitly. 

2638 # TODO: add an encoding flag to tell gprof2dot how to decode the profile file. 

2639 iconv -f ISO-8859-1 -t UTF-8 out.user_stacks | gprof2dot.py -f dtrace 

2640 """ 

2641 

2642 def __init__(self, infile): 

2643 LineParser.__init__(self, infile) 

2644 self.profile = Profile() 

2645 

2646 def readline(self): 

2647 # Override LineParser.readline to ignore comment lines 

2648 while True: 

2649 LineParser.readline(self) 

2650 if self.eof(): 

2651 break 

2652 

2653 line = self.lookahead().strip() 

2654 if line.startswith('CPU'): 

2655 # The format likes: 

2656 # CPU ID FUNCTION:NAME 

2657 # 1 29684 :tick-60s 

2658 # Skip next line 

2659 LineParser.readline(self) 

2660 elif not line == '': 

2661 break 

2662 

2663 

2664 def parse(self): 

2665 # read lookahead 

2666 self.readline() 

2667 

2668 profile = self.profile 

2669 profile[SAMPLES] = 0 

2670 while not self.eof(): 

2671 self.parse_event() 

2672 

2673 # compute derived data 

2674 profile.validate() 

2675 profile.find_cycles() 

2676 profile.ratio(TIME_RATIO, SAMPLES) 

2677 profile.call_ratios(SAMPLES2) 

2678 if totalMethod == "callratios": 

2679 # Heuristic approach. TOTAL_SAMPLES is unused. 

2680 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2681 elif totalMethod == "callstacks": 

2682 # Use the actual call chains for functions. 

2683 profile[TOTAL_SAMPLES] = profile[SAMPLES] 

2684 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES) 

2685 # Then propagate that total time to the calls. 

2686 for function in profile.functions.values(): 

2687 for call in function.calls.values(): 

2688 if call.ratio is not None: 

2689 callee = profile.functions[call.callee_id] 

2690 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] 

2691 else: 

2692 assert False 

2693 

2694 return profile 

2695 

2696 def parse_event(self): 

2697 if self.eof(): 

2698 return 

2699 

2700 callchain, count = self.parse_callchain() 

2701 if not callchain: 

2702 return 

2703 

2704 callee = callchain[0] 

2705 callee[SAMPLES] += count 

2706 self.profile[SAMPLES] += count 

2707 

2708 for caller in callchain[1:]: 

2709 try: 

2710 call = caller.calls[callee.id] 

2711 except KeyError: 

2712 call = Call(callee.id) 

2713 call[SAMPLES2] = count 

2714 caller.add_call(call) 

2715 else: 

2716 call[SAMPLES2] += count 

2717 

2718 callee = caller 

2719 

2720 # Increment TOTAL_SAMPLES only once on each function. 

2721 stack = set(callchain) 

2722 for function in stack: 

2723 function[TOTAL_SAMPLES] += count 

2724 

2725 

2726 def parse_callchain(self): 

2727 callchain = [] 

2728 count = 0 

2729 while self.lookahead(): 

2730 function, count = self.parse_call() 

2731 if function is None: 

2732 break 

2733 callchain.append(function) 

2734 return callchain, count 

2735 

2736 call_re = re.compile(r'^\s+(?P<module>.*)`(?P<symbol>.*)') 

2737 addr2_re = re.compile(r'\+0x[0-9a-fA-F]+$') 

2738 

2739 def parse_call(self): 

2740 line = self.consume() 

2741 mo = self.call_re.match(line) 

2742 if not mo: 

2743 # The line must be the stack count 

2744 return None, int(line.strip()) 

2745 

2746 function_name = mo.group('symbol') 

2747 

2748 # If present, amputate program counter from function name. 

2749 if function_name: 

2750 function_name = re.sub(self.addr2_re, '', function_name) 

2751 

2752 # if not function_name or function_name == '[unknown]': 

2753 # function_name = mo.group('address') 

2754 

2755 module = mo.group('module') 

2756 

2757 function_id = function_name + ':' + module 

2758 

2759 try: 

2760 function = self.profile.functions[function_id] 

2761 except KeyError: 

2762 function = Function(function_id, function_name) 

2763 function.module = os.path.basename(module) 

2764 function[SAMPLES] = 0 

2765 function[TOTAL_SAMPLES] = 0 

2766 self.profile.add_function(function) 

2767 

2768 return function, None 

2769 

2770 

2771class CollapseParser(LineParser): 

2772 """Parser for the output of stackcollapse 

2773 

2774 (from https://github.com/brendangregg/FlameGraph) 

2775 """ 

2776 

2777 def __init__(self, infile): 

2778 LineParser.__init__(self, infile) 

2779 self.profile = Profile() 

2780 

2781 def parse(self): 

2782 profile = self.profile 

2783 profile[SAMPLES] = 0 

2784 

2785 self.readline() 

2786 while not self.eof(): 

2787 self.parse_event() 

2788 

2789 # compute derived data 

2790 profile.validate() 

2791 profile.find_cycles() 

2792 profile.ratio(TIME_RATIO, SAMPLES) 

2793 profile.call_ratios(SAMPLES2) 

2794 if totalMethod == "callratios": 

2795 # Heuristic approach. TOTAL_SAMPLES is unused. 

2796 profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) 

2797 elif totalMethod == "callstacks": 

2798 # Use the actual call chains for functions. 

2799 profile[TOTAL_SAMPLES] = profile[SAMPLES] 

2800 profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES) 

2801 # Then propagate that total time to the calls. 

2802 for function in compat_itervalues(profile.functions): 

2803 for call in compat_itervalues(function.calls): 

2804 if call.ratio is not None: 

2805 callee = profile.functions[call.callee_id] 

2806 call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] 

2807 else: 

2808 assert False 

2809 

2810 return profile 

2811 

2812 def parse_event(self): 

2813 line = self.consume() 

2814 

2815 stack, count = line.rsplit(' ',maxsplit=1) 

2816 count=int(count) 

2817 self.profile[SAMPLES] += count 

2818 

2819 calls = stack.split(';') 

2820 functions = [self._make_function(call) for call in calls] 

2821 

2822 functions[-1][SAMPLES] += count 

2823 

2824 # TOTAL_SAMPLES excludes loops 

2825 for func in set(functions): 

2826 func[TOTAL_SAMPLES] += count 

2827 

2828 # add call data 

2829 callee = functions[-1] 

2830 for caller in reversed(functions[:-1]): 

2831 call = caller.calls.get(callee.id) 

2832 if call is None: 

2833 call = Call(callee.id) 

2834 call[SAMPLES2] = 0 

2835 caller.add_call(call) 

2836 call[SAMPLES2] += count 

2837 callee = caller 

2838 

2839 call_re = re.compile(r'^(?P<func>[^ ]+) \((?P<file>.*):(?P<line>[0-9]+)\)$') 

2840 

2841 def _make_function(self, call): 

2842 """turn a call str into a Function 

2843 

2844 takes a call site, as found between semicolons in the input, and returns 

2845 a Function definition corresponding to that call site. 

2846 """ 

2847 mo = self.call_re.match(call) 

2848 if mo: 

2849 name, module, line = mo.groups() 

2850 func_id = "%s:%s" % (module, name) 

2851 else: 

2852 name = func_id = call 

2853 module = None 

2854 

2855 func = self.profile.functions.get(func_id) 

2856 if func is None: 

2857 func = Function(func_id, name) 

2858 func.module = module 

2859 func[SAMPLES] = 0 

2860 func[TOTAL_SAMPLES] = 0 

2861 self.profile.add_function(func) 

2862 return func 

2863 

2864 

2865formats = { 

2866 "axe": AXEParser, 

2867 "callgrind": CallgrindParser, 

2868 "collapse": CollapseParser, 

2869 "hprof": HProfParser, 

2870 "json": JsonParser, 

2871 "perf": PerfParser, 

2872 "prof": GprofParser, 

2873 "pstats": PstatsParser, 

2874 "sleepy": SleepyParser, 

2875 "sysprof": SysprofParser, 

2876 "xperf": XPerfParser, 

2877 "dtrace": DtraceParser, 

2878} 

2879 

2880 

2881######################################################################## 

2882# Output 

2883 

2884 

2885class Theme: 

2886 

2887 def __init__(self, 

2888 bgcolor = (0.0, 0.0, 1.0), 

2889 mincolor = (0.0, 0.0, 0.0), 

2890 maxcolor = (0.0, 0.0, 1.0), 

2891 fontname = "Arial", 

2892 fontcolor = "white", 

2893 nodestyle = "filled", 

2894 minfontsize = 10.0, 

2895 maxfontsize = 10.0, 

2896 minpenwidth = 0.5, 

2897 maxpenwidth = 4.0, 

2898 gamma = 2.2, 

2899 skew = 1.0): 

2900 self.bgcolor = bgcolor 

2901 self.mincolor = mincolor 

2902 self.maxcolor = maxcolor 

2903 self.fontname = fontname 

2904 self.fontcolor = fontcolor 

2905 self.nodestyle = nodestyle 

2906 self.minfontsize = minfontsize 

2907 self.maxfontsize = maxfontsize 

2908 self.minpenwidth = minpenwidth 

2909 self.maxpenwidth = maxpenwidth 

2910 self.gamma = gamma 

2911 self.skew = skew 

2912 

2913 def graph_bgcolor(self): 

2914 return self.hsl_to_rgb(*self.bgcolor) 

2915 

2916 def graph_fontname(self): 

2917 return self.fontname 

2918 

2919 def graph_fontcolor(self): 

2920 return self.fontcolor 

2921 

2922 def node_bgcolor(self, weight): 

2923 return self.color(weight) 

2924 

2925 def node_fgcolor(self, weight): 

2926 if self.nodestyle == "filled": 

2927 return self.graph_bgcolor() 

2928 else: 

2929 return self.color(weight) 

2930 

2931 def node_fontsize(self, weight): 

2932 return self.fontsize(weight) 

2933 

2934 def node_style(self): 

2935 return self.nodestyle 

2936 

2937 def edge_color(self, weight): 

2938 return self.color(weight) 

2939 

2940 def edge_fontsize(self, weight): 

2941 return self.fontsize(weight) 

2942 

2943 def edge_penwidth(self, weight): 

2944 return max(weight*self.maxpenwidth, self.minpenwidth) 

2945 

2946 def edge_arrowsize(self, weight): 

2947 return 0.5 * math.sqrt(self.edge_penwidth(weight)) 

2948 

2949 def fontsize(self, weight): 

2950 return max(weight**2 * self.maxfontsize, self.minfontsize) 

2951 

2952 def color(self, weight): 

2953 weight = min(max(weight, 0.0), 1.0) 

2954 

2955 hmin, smin, lmin = self.mincolor 

2956 hmax, smax, lmax = self.maxcolor 

2957 

2958 if self.skew < 0: 

2959 raise ValueError("Skew must be greater than 0") 

2960 elif self.skew == 1.0: 

2961 h = hmin + weight*(hmax - hmin) 

2962 s = smin + weight*(smax - smin) 

2963 l = lmin + weight*(lmax - lmin) 

2964 else: 

2965 base = self.skew 

2966 h = hmin + ((hmax-hmin)*(-1.0 + (base ** weight)) / (base - 1.0)) 

2967 s = smin + ((smax-smin)*(-1.0 + (base ** weight)) / (base - 1.0)) 

2968 l = lmin + ((lmax-lmin)*(-1.0 + (base ** weight)) / (base - 1.0)) 

2969 

2970 return self.hsl_to_rgb(h, s, l) 

2971 

2972 def hsl_to_rgb(self, h, s, l): 

2973 """Convert a color from HSL color-model to RGB. 

2974 

2975 See also: 

2976 - http://www.w3.org/TR/css3-color/#hsl-color 

2977 """ 

2978 

2979 h = h % 1.0 

2980 s = min(max(s, 0.0), 1.0) 

2981 l = min(max(l, 0.0), 1.0) 

2982 

2983 if l <= 0.5: 

2984 m2 = l*(s + 1.0) 

2985 else: 

2986 m2 = l + s - l*s 

2987 m1 = l*2.0 - m2 

2988 r = self._hue_to_rgb(m1, m2, h + 1.0/3.0) 

2989 g = self._hue_to_rgb(m1, m2, h) 

2990 b = self._hue_to_rgb(m1, m2, h - 1.0/3.0) 

2991 

2992 # Apply gamma correction 

2993 r **= self.gamma 

2994 g **= self.gamma 

2995 b **= self.gamma 

2996 

2997 return (r, g, b) 

2998 

2999 def _hue_to_rgb(self, m1, m2, h): 

3000 if h < 0.0: 

3001 h += 1.0 

3002 elif h > 1.0: 

3003 h -= 1.0 

3004 if h*6 < 1.0: 

3005 return m1 + (m2 - m1)*h*6.0 

3006 elif h*2 < 1.0: 

3007 return m2 

3008 elif h*3 < 2.0: 

3009 return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0 

3010 else: 

3011 return m1 

3012 

3013 

3014TEMPERATURE_COLORMAP = Theme( 

3015 mincolor = (2.0/3.0, 0.80, 0.25), # dark blue 

3016 maxcolor = (0.0, 1.0, 0.5), # satured red 

3017 gamma = 1.0 

3018) 

3019 

3020PINK_COLORMAP = Theme( 

3021 mincolor = (0.0, 1.0, 0.90), # pink 

3022 maxcolor = (0.0, 1.0, 0.5), # satured red 

3023) 

3024 

3025GRAY_COLORMAP = Theme( 

3026 mincolor = (0.0, 0.0, 0.85), # light gray 

3027 maxcolor = (0.0, 0.0, 0.0), # black 

3028) 

3029 

3030BW_COLORMAP = Theme( 

3031 minfontsize = 8.0, 

3032 maxfontsize = 24.0, 

3033 mincolor = (0.0, 0.0, 0.0), # black 

3034 maxcolor = (0.0, 0.0, 0.0), # black 

3035 minpenwidth = 0.1, 

3036 maxpenwidth = 8.0, 

3037) 

3038 

3039PRINT_COLORMAP = Theme( 

3040 minfontsize = 18.0, 

3041 maxfontsize = 30.0, 

3042 fontcolor = "black", 

3043 nodestyle = "solid", 

3044 mincolor = (0.0, 0.0, 0.0), # black 

3045 maxcolor = (0.0, 0.0, 0.0), # black 

3046 minpenwidth = 0.1, 

3047 maxpenwidth = 8.0, 

3048) 

3049 

3050 

3051themes = { 

3052 "color": TEMPERATURE_COLORMAP, 

3053 "pink": PINK_COLORMAP, 

3054 "gray": GRAY_COLORMAP, 

3055 "bw": BW_COLORMAP, 

3056 "print": PRINT_COLORMAP, 

3057} 

3058 

3059 

3060def sorted_iteritems(d): 

3061 # Used mostly for result reproducibility (while testing.) 

3062 keys = list(d.keys()) 

3063 keys.sort() 

3064 for key in keys: 

3065 value = d[key] 

3066 yield key, value 

3067 

3068 

3069class DotWriter: 

3070 """Writer for the DOT language. 

3071 

3072 See also: 

3073 - "The DOT Language" specification 

3074 http://www.graphviz.org/doc/info/lang.html 

3075 """ 

3076 

3077 strip = False 

3078 wrap = False 

3079 

3080 def __init__(self, fp): 

3081 self.fp = fp 

3082 

3083 def wrap_function_name(self, name): 

3084 """Split the function name on multiple lines.""" 

3085 

3086 if len(name) > 32: 

3087 ratio = 2.0/3.0 

3088 height = max(int(len(name)/(1.0 - ratio) + 0.5), 1) 

3089 width = max(len(name)/height, 32) 

3090 # TODO: break lines in symbols 

3091 name = textwrap.fill(name, width, break_long_words=False) 

3092 

3093 # Take away spaces 

3094 name = name.replace(", ", ",") 

3095 name = name.replace("> >", ">>") 

3096 name = name.replace("> >", ">>") # catch consecutive 

3097 

3098 return name 

3099 

3100 show_function_events = [TOTAL_TIME_RATIO, TIME_RATIO] 

3101 show_edge_events = [TOTAL_TIME_RATIO, CALLS] 

3102 

3103 def graphs_compare(self, profile1, profile2, theme, options): 

3104 self.begin_graph() 

3105 

3106 fontname = theme.graph_fontname() 

3107 fontcolor = theme.graph_fontcolor() 

3108 nodestyle = theme.node_style() 

3109 

3110 tolerance, only_slower, only_faster, color_by_difference = ( 

3111 options.tolerance, options.only_slower, options.only_faster, options.color_by_difference) 

3112 self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125) 

3113 self.attr('node', fontname=fontname, style=nodestyle, fontcolor=fontcolor, width=0, height=0) 

3114 self.attr('edge', fontname=fontname) 

3115 

3116 functions2 = {function.name: function for _, function in sorted_iteritems(profile2.functions)} 

3117 

3118 for _, function1 in sorted_iteritems(profile1.functions): 

3119 labels = [] 

3120 

3121 name = function1.name 

3122 try: 

3123 function2 = functions2[name] 

3124 if self.wrap: 

3125 name = self.wrap_function_name(name) 

3126 if color_by_difference: 

3127 min_diff, max_diff = min_max_difference(profile1, profile2) 

3128 labels.append(name) 

3129 weight_difference = 0 

3130 shape = 'box' 

3131 orientation = '0' 

3132 for event in self.show_function_events: 

3133 if event in function1.events: 

3134 event1 = function1[event] 

3135 event2 = function2[event] 

3136 

3137 difference = abs(event1 - event2) * 100 

3138 

3139 if event == TOTAL_TIME_RATIO: 

3140 weight_difference = difference 

3141 if difference >= tolerance: 

3142 if event2 > event1 and not only_faster: 

3143 shape = 'cds' 

3144 label = (f'{event.format(event1)} +' 

3145 f' {round_difference(difference, tolerance)}%') 

3146 elif event2 < event1 and not only_slower: 

3147 orientation = "90" 

3148 shape = 'cds' 

3149 label = (f'{event.format(event1)} - ' 

3150 f'{round_difference(difference, tolerance)}%') 

3151 else: 

3152 # protection to not color by difference if we choose to show only_faster/only_slower 

3153 weight_difference = 0 

3154 label = event.format(function1[event]) 

3155 else: 

3156 weight_difference = 0 

3157 label = event.format(function1[event]) 

3158 else: 

3159 if difference >= tolerance: 

3160 if event2 > event1: 

3161 label = (f'{event.format(event1)} +' 

3162 f' {round_difference(difference, tolerance)}%') 

3163 elif event2 < event1: 

3164 label = (f'{event.format(event1)} - ' 

3165 f'{round_difference(difference, tolerance)}%') 

3166 else: 

3167 label = event.format(function1[event]) 

3168 

3169 labels.append(label) 

3170 if function1.called is not None: 

3171 labels.append(f"{function1.called} {MULTIPLICATION_SIGN}/ {function2.called} {MULTIPLICATION_SIGN}") 

3172 

3173 except KeyError: 

3174 shape = 'box' 

3175 orientation = '0' 

3176 weight_difference = 0 

3177 if function1.process is not None: 

3178 labels.append(function1.process) 

3179 if function1.module is not None: 

3180 labels.append(function1.module) 

3181 

3182 if self.strip: 

3183 function_name = function1.stripped_name() 

3184 else: 

3185 function_name = function1.name 

3186 if color_by_difference: 

3187 min_diff, max_diff = 0, 0 

3188 

3189 # dot can't parse quoted strings longer than YY_BUF_SIZE, which 

3190 # defaults to 16K. But some annotated C++ functions (e.g., boost, 

3191 # https://github.com/jrfonseca/gprof2dot/issues/30) can exceed that 

3192 MAX_FUNCTION_NAME = 4096 

3193 if len(function_name) >= MAX_FUNCTION_NAME: 

3194 sys.stderr.write('warning: truncating function name with %u chars (%s)\n' % (len(function_name), function_name[:32] + '...')) 

3195 function_name = function_name[:MAX_FUNCTION_NAME - 1] + chr(0x2026) 

3196 

3197 if self.wrap: 

3198 function_name = self.wrap_function_name(function_name) 

3199 labels.append(function_name) 

3200 

3201 for event in self.show_function_events: 

3202 if event in function1.events: 

3203 label = event.format(function1[event]) 

3204 labels.append(label) 

3205 if function1.called is not None: 

3206 labels.append("%u%s" % (function1.called, MULTIPLICATION_SIGN)) 

3207 

3208 if color_by_difference and weight_difference: 

3209 # min and max is calculated whe color_by_difference is true 

3210 weight = rescale_difference(weight_difference, min_diff, max_diff) 

3211 

3212 elif function1.weight is not None and not color_by_difference: 

3213 weight = function1.weight 

3214 else: 

3215 weight = 0.0 

3216 

3217 label = '\n'.join(labels) 

3218 

3219 self.node(function1.id, 

3220 label=label, 

3221 orientation=orientation, 

3222 color=self.color(theme.node_bgcolor(weight)), 

3223 shape=shape, 

3224 fontcolor=self.color(theme.node_fgcolor(weight)), 

3225 fontsize="%f" % theme.node_fontsize(weight), 

3226 tooltip=function1.filename, 

3227 ) 

3228 

3229 calls2 = {call.callee_id: call for _, call in sorted_iteritems(function2.calls)} 

3230 functions_by_id1 = {function.id: function for _, function in sorted_iteritems(profile1.functions)} 

3231 

3232 for _, call1 in sorted_iteritems(function1.calls): 

3233 labels = [] 

3234 try: 

3235 # if profiles do not have identical setups, callee_id will not be identical either 

3236 call_id1 = call1.callee_id 

3237 call_name = functions_by_id1[call_id1].name 

3238 call_id2 = functions2[call_name].id 

3239 call2 = calls2[call_id2] 

3240 for event in self.show_edge_events: 

3241 if event in call1.events: 

3242 label = f'{event.format(call1[event])} / {event.format(call2[event])}' 

3243 labels.append(label) 

3244 except KeyError: 

3245 for event in self.show_edge_events: 

3246 if event in call1.events: 

3247 label = f'{event.format(call1[event])}' 

3248 labels.append(label) 

3249 

3250 weight = 0 if color_by_difference else call1.weight 

3251 label = '\n'.join(labels) 

3252 self.edge(function1.id, call1.callee_id, 

3253 label=label, 

3254 color=self.color(theme.edge_color(weight)), 

3255 fontcolor=self.color(theme.edge_color(weight)), 

3256 fontsize="%.2f" % theme.edge_fontsize(weight), 

3257 penwidth="%.2f" % theme.edge_penwidth(weight), 

3258 labeldistance="%.2f" % theme.edge_penwidth(weight), 

3259 arrowsize="%.2f" % theme.edge_arrowsize(weight), 

3260 ) 

3261 self.end_graph() 

3262 

3263 def graph(self, profile, theme): 

3264 self.begin_graph() 

3265 

3266 fontname = theme.graph_fontname() 

3267 fontcolor = theme.graph_fontcolor() 

3268 nodestyle = theme.node_style() 

3269 

3270 self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125) 

3271 self.attr('node', fontname=fontname, shape="box", style=nodestyle, fontcolor=fontcolor, width=0, height=0) 

3272 self.attr('edge', fontname=fontname) 

3273 

3274 for _, function in sorted_iteritems(profile.functions): 

3275 labels = [] 

3276 if function.process is not None: 

3277 labels.append(function.process) 

3278 if function.module is not None: 

3279 labels.append(function.module) 

3280 

3281 if self.strip: 

3282 function_name = function.stripped_name() 

3283 else: 

3284 function_name = function.name 

3285 

3286 # dot can't parse quoted strings longer than YY_BUF_SIZE, which 

3287 # defaults to 16K. But some annotated C++ functions (e.g., boost, 

3288 # https://github.com/jrfonseca/gprof2dot/issues/30) can exceed that 

3289 MAX_FUNCTION_NAME = 4096 

3290 if len(function_name) >= MAX_FUNCTION_NAME: 

3291 sys.stderr.write('warning: truncating function name with %u chars (%s)\n' % (len(function_name), function_name[:32] + '...')) 

3292 function_name = function_name[:MAX_FUNCTION_NAME - 1] + chr(0x2026) 

3293 

3294 if self.wrap: 

3295 function_name = self.wrap_function_name(function_name) 

3296 labels.append(function_name) 

3297 

3298 for event in self.show_function_events: 

3299 if event in function.events: 

3300 label = event.format(function[event]) 

3301 labels.append(label) 

3302 if function.called is not None: 

3303 labels.append("%u%s" % (function.called, MULTIPLICATION_SIGN)) 

3304 

3305 if function.weight is not None: 

3306 weight = function.weight 

3307 else: 

3308 weight = 0.0 

3309 

3310 label = '\n'.join(labels) 

3311 self.node(function.id, 

3312 label = label, 

3313 color = self.color(theme.node_bgcolor(weight)), 

3314 fontcolor = self.color(theme.node_fgcolor(weight)), 

3315 fontsize = "%.2f" % theme.node_fontsize(weight), 

3316 tooltip = function.filename, 

3317 ) 

3318 

3319 for _, call in sorted_iteritems(function.calls): 

3320 callee = profile.functions[call.callee_id] 

3321 

3322 labels = [] 

3323 for event in self.show_edge_events: 

3324 if event in call.events: 

3325 label = event.format(call[event]) 

3326 labels.append(label) 

3327 

3328 if call.weight is not None: 

3329 weight = call.weight 

3330 elif callee.weight is not None: 

3331 weight = callee.weight 

3332 else: 

3333 weight = 0.0 

3334 

3335 label = '\n'.join(labels) 

3336 

3337 self.edge(function.id, call.callee_id, 

3338 label = label, 

3339 color = self.color(theme.edge_color(weight)), 

3340 fontcolor = self.color(theme.edge_color(weight)), 

3341 fontsize = "%.2f" % theme.edge_fontsize(weight), 

3342 penwidth = "%.2f" % theme.edge_penwidth(weight), 

3343 labeldistance = "%.2f" % theme.edge_penwidth(weight), 

3344 arrowsize = "%.2f" % theme.edge_arrowsize(weight), 

3345 ) 

3346 

3347 self.end_graph() 

3348 

3349 def begin_graph(self): 

3350 self.write('digraph {\n') 

3351 # Work-around graphviz bug[1]: unnamed graphs have "%3" tooltip in SVG 

3352 # output. The bug was fixed upstream, but graphviz shipped in recent 

3353 # Linux distros (for example, Ubuntu 24.04) still has the bug. 

3354 # [1] https://gitlab.com/graphviz/graphviz/-/issues/1376 

3355 self.write('\ttooltip=" "\n') 

3356 

3357 def end_graph(self): 

3358 self.write('}\n') 

3359 

3360 def attr(self, what, **attrs): 

3361 self.write("\t") 

3362 self.write(what) 

3363 self.attr_list(attrs) 

3364 self.write(";\n") 

3365 

3366 def node(self, node, **attrs): 

3367 self.write("\t") 

3368 self.node_id(node) 

3369 self.attr_list(attrs) 

3370 self.write(";\n") 

3371 

3372 def edge(self, src, dst, **attrs): 

3373 self.write("\t") 

3374 self.node_id(src) 

3375 self.write(" -> ") 

3376 self.node_id(dst) 

3377 self.attr_list(attrs) 

3378 self.write(";\n") 

3379 

3380 def attr_list(self, attrs): 

3381 if not attrs: 

3382 return 

3383 self.write(' [') 

3384 first = True 

3385 for name, value in sorted_iteritems(attrs): 

3386 if value is None: 

3387 continue 

3388 if first: 

3389 first = False 

3390 else: 

3391 self.write(", ") 

3392 assert isinstance(name, str) 

3393 assert name.isidentifier() 

3394 self.write(name) 

3395 self.write('=') 

3396 self.id(value) 

3397 self.write(']') 

3398 

3399 def node_id(self, id): 

3400 # Node IDs need to be unique (can't be truncated) but dot doesn't allow 

3401 # IDs longer than 16384 characters, so use an hash instead for the huge 

3402 # C++ symbols that can arise, as seen in 

3403 # https://github.com/jrfonseca/gprof2dot/issues/99 

3404 if isinstance(id, str) and len(id) > 1024: 

3405 id = '_' + hashlib.sha1(id.encode('utf-8'), usedforsecurity=False).hexdigest() 

3406 self.id(id) 

3407 

3408 def id(self, id): 

3409 if isinstance(id, (int, float)): 

3410 s = str(id) 

3411 elif isinstance(id, str): 

3412 if id.isalnum() and not id.startswith('0x'): 

3413 s = id 

3414 else: 

3415 s = self.escape(id) 

3416 else: 

3417 raise TypeError 

3418 self.write(s) 

3419 

3420 def color(self, rgb): 

3421 r, g, b = rgb 

3422 

3423 def float2int(f): 

3424 if f <= 0.0: 

3425 return 0 

3426 if f >= 1.0: 

3427 return 255 

3428 return int(255.0*f + 0.5) 

3429 

3430 return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)]) 

3431 

3432 def escape(self, s): 

3433 s = s.replace('\\', r'\\') 

3434 s = s.replace('\n', r'\n') 

3435 s = s.replace('\t', r'\t') 

3436 s = s.replace('"', r'\"') 

3437 return '"' + s + '"' 

3438 

3439 def write(self, s): 

3440 self.fp.write(s) 

3441 

3442 

3443 

3444######################################################################## 

3445# Main program 

3446 

3447 

3448def naturalJoin(values): 

3449 if len(values) >= 2: 

3450 return ', '.join(values[:-1]) + ' or ' + values[-1] 

3451 

3452 else: 

3453 return ''.join(values) 

3454 

3455 

3456def main(argv=sys.argv[1:]): 

3457 """Main program.""" 

3458 

3459 global totalMethod, timeFormat 

3460 

3461 formatNames = list(formats.keys()) 

3462 formatNames.sort() 

3463 

3464 themeNames = list(themes.keys()) 

3465 themeNames.sort() 

3466 

3467 labelNames = list(labels.keys()) 

3468 labelNames.sort() 

3469 

3470 argparser = argparse.ArgumentParser( 

3471 usage="\n %(prog)s [options] [INPUT] ...") 

3472 argparser.add_argument( 

3473 '-o', '--output', metavar='OUTPUT', 

3474 dest="output", 

3475 help="output filename [stdout]") 

3476 argparser.add_argument( 

3477 '-n', '--node-thres', metavar='PERCENTAGE', 

3478 type=float, dest="node_thres", default=0.5, 

3479 help="eliminate nodes below this threshold [default: %(default)s]") 

3480 argparser.add_argument( 

3481 '-e', '--edge-thres', metavar='PERCENTAGE', 

3482 type=float, dest="edge_thres", default=0.1, 

3483 help="eliminate edges below this threshold [default: %(default)s]") 

3484 argparser.add_argument( 

3485 '-f', '--format', 

3486 choices=formatNames, 

3487 dest="format", default="prof", 

3488 help="profile format: %s [default: %%(default)s]" % naturalJoin(formatNames)) 

3489 argparser.add_argument( 

3490 '--total', 

3491 choices=('callratios', 'callstacks'), 

3492 dest="totalMethod", default=totalMethod, 

3493 help="preferred method of calculating total time: callratios or callstacks (currently affects only perf format) [default: %(default)s]") 

3494 argparser.add_argument( 

3495 '-c', '--colormap', 

3496 choices=themeNames, 

3497 dest="theme", default="color", 

3498 help="color map: %s [default: %%(default)s]" % naturalJoin(themeNames)) 

3499 argparser.add_argument( 

3500 '-s', '--strip', 

3501 action="store_true", 

3502 dest="strip", default=False, 

3503 help="strip function parameters, template parameters, and const modifiers from demangled C++ function names") 

3504 argparser.add_argument( 

3505 '--color-nodes-by-selftime', 

3506 action="store_true", 

3507 dest="color_nodes_by_selftime", default=False, 

3508 help="color nodes by self time, rather than by total time (sum of self and descendants)") 

3509 argparser.add_argument( 

3510 '--colour-nodes-by-selftime', 

3511 action="store_true", 

3512 dest="color_nodes_by_selftime", 

3513 help=argparse.SUPPRESS) 

3514 argparser.add_argument( 

3515 '-w', '--wrap', 

3516 action="store_true", 

3517 dest="wrap", default=False, 

3518 help="wrap function names") 

3519 argparser.add_argument( 

3520 '--show-samples', 

3521 action="store_true", 

3522 dest="show_samples", default=False, 

3523 help="show function samples") 

3524 argparser.add_argument( 

3525 '--time-format', 

3526 default=timeFormat, 

3527 help="format to use for showing time values [default: %(default)s]") 

3528 argparser.add_argument( 

3529 '--node-label', metavar='MEASURE', 

3530 choices=labelNames, 

3531 action='append', 

3532 dest='node_labels', 

3533 help="measurements to on show the node (can be specified multiple times): %s [default: %s]" % ( 

3534 naturalJoin(labelNames), ', '.join(defaultLabelNames))) 

3535 # add option to show information on available entries () 

3536 argparser.add_argument( 

3537 '--list-functions', 

3538 dest="list_functions", default=None, 

3539 help="""\ 

3540list functions available for selection in -z or -l, requires selector argument 

3541( use '+' to select all). 

3542Recall that the selector argument is used with Unix/Bash globbing/pattern matching, 

3543and that entries are formatted '<pkg>:<linenum>:<function>'. When argument starts 

3544with '%%', a dump of all available information is performed for selected entries, 

3545 after removal of leading '%%'. 

3546""") 

3547 # add option to create subtree or show paths 

3548 argparser.add_argument( 

3549 '-z', '--root', 

3550 dest="root", default="", 

3551 help="prune call graph to show only descendants of specified root function") 

3552 argparser.add_argument( 

3553 '-l', '--leaf', 

3554 dest="leaf", default="", 

3555 help="prune call graph to show only ancestors of specified leaf function") 

3556 argparser.add_argument( 

3557 '--depth', 

3558 type=int, 

3559 dest="depth", default=-1, 

3560 help="prune call graph to show only descendants or ancestors until specified depth") 

3561 # add a new option to control skew of the colorization curve 

3562 argparser.add_argument( 

3563 '--skew', 

3564 type=float, dest="theme_skew", default=1.0, 

3565 help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Values > 1.0 give less variety to lower percentages") 

3566 # add option for filtering by file path 

3567 argparser.add_argument( 

3568 '-p', '--path', action="append", 

3569 dest="filter_paths", 

3570 help="filter all modules not in a specified path") 

3571 argparser.add_argument( 

3572 '--compare', 

3573 action="store_true", 

3574 dest="compare", default=False, 

3575 help="compare two graphs with almost identical structure. With this option two files should be provided." 

3576 "gprof2dot.py [options] --compare [file1] [file2] ...") 

3577 argparser.add_argument( 

3578 '--compare-tolerance', 

3579 type=float, dest="tolerance", default=0.001, 

3580 help="tolerance threshold for node difference (default=0.001%%)." 

3581 "If the difference is below this value the nodes are considered identical.") 

3582 argparser.add_argument( 

3583 '--compare-only-slower', 

3584 action="store_true", 

3585 dest="only_slower", default=False, 

3586 help="display comparison only for function which are slower in second graph.") 

3587 argparser.add_argument( 

3588 '--compare-only-faster', 

3589 action="store_true", 

3590 dest="only_faster", default=False, 

3591 help="display comparison only for function which are faster in second graph.") 

3592 argparser.add_argument( 

3593 '--compare-color-by-difference', 

3594 action="store_true", 

3595 dest="color_by_difference", default=False, 

3596 help="color nodes based on the value of the difference. " 

3597 "Nodes with the largest differences represent the hot spots.") 

3598 argparser.add_argument('input', nargs='*', metavar='INPUT', help='input stats') 

3599 options = argparser.parse_args(argv) 

3600 args = options.input 

3601 

3602 if len(args) > 1 and options.format != 'pstats' and not options.compare: 

3603 argparser.error('incorrect number of arguments') 

3604 

3605 try: 

3606 theme = themes[options.theme] 

3607 except KeyError: 

3608 argparser.error('invalid colormap \'%s\'' % options.theme) 

3609 

3610 # set skew on the theme now that it has been picked. 

3611 if options.theme_skew: 

3612 theme.skew = options.theme_skew 

3613 

3614 totalMethod = options.totalMethod 

3615 timeFormat = options.time_format 

3616 

3617 try: 

3618 Format = formats[options.format] 

3619 except KeyError: 

3620 argparser.error('invalid format \'%s\'' % options.format) 

3621 

3622 if Format.stdinInput: 

3623 if not args: 

3624 fp = sys.stdin 

3625 parser = Format(fp) 

3626 elif options.compare: 

3627 fp1 = open(args[0], 'rt', encoding='UTF-8') 

3628 fp2 = open(args[1], 'rt', encoding='UTF-8') 

3629 parser1 = Format(fp1) 

3630 parser2 = Format(fp2) 

3631 else: 

3632 fp = open(args[0], 'rb') 

3633 bom = fp.read(2) 

3634 if bom == codecs.BOM_UTF16_LE: 

3635 # Default on Windows PowerShell (https://github.com/jrfonseca/gprof2dot/issues/88) 

3636 encoding = 'utf-16le' 

3637 else: 

3638 encoding = 'utf-8' 

3639 fp.seek(0) 

3640 fp = io.TextIOWrapper(fp, encoding=encoding) 

3641 parser = Format(fp) 

3642 elif Format.multipleInput: 

3643 if not args: 

3644 argparser.error('at least a file must be specified for %s input' % options.format) 

3645 if options.compare: 

3646 parser1 = Format(args[-2]) 

3647 parser2 = Format(args[-1]) 

3648 else: 

3649 parser = Format(*args) 

3650 else: 

3651 if len(args) != 1: 

3652 argparser.error('exactly one file must be specified for %s input' % options.format) 

3653 parser = Format(args[0]) 

3654 

3655 if options.compare: 

3656 profile1 = parser1.parse() 

3657 profile2 = parser2.parse() 

3658 else: 

3659 profile = parser.parse() 

3660 

3661 if options.output is None: 

3662 output = open(sys.stdout.fileno(), mode='wt', encoding='UTF-8', closefd=False) 

3663 else: 

3664 output = open(options.output, 'wt', encoding='UTF-8') 

3665 

3666 dot = DotWriter(output) 

3667 dot.strip = options.strip 

3668 dot.wrap = options.wrap 

3669 

3670 labelNames = options.node_labels or defaultLabelNames 

3671 dot.show_function_events = [labels[l] for l in labelNames] 

3672 if options.show_samples: 

3673 dot.show_function_events.append(SAMPLES) 

3674 

3675 if options.compare: 

3676 profile1.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths, 

3677 options.color_nodes_by_selftime) 

3678 profile2.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths, 

3679 options.color_nodes_by_selftime) 

3680 

3681 if options.root: 

3682 profile1.prune_root(profile1.getFunctionIds(options.root), options.depth) 

3683 profile2.prune_root(profile2.getFunctionIds(options.root), options.depth) 

3684 else: 

3685 profile.prune(options.node_thres/100.0, options.edge_thres/100.0, options.filter_paths, 

3686 options.color_nodes_by_selftime) 

3687 if options.root: 

3688 rootIds = profile.getFunctionIds(options.root) 

3689 if not rootIds: 

3690 sys.stderr.write('root node ' + options.root + ' not found (might already be pruned : try -e0 -n0 flags)\n') 

3691 sys.exit(1) 

3692 profile.prune_root(rootIds, options.depth) 

3693 

3694 if options.list_functions: 

3695 profile.printFunctionIds(selector=options.list_functions) 

3696 sys.exit(0) 

3697 

3698 if options.leaf: 

3699 leafIds = profile.getFunctionIds(options.leaf) 

3700 if not leafIds: 

3701 sys.stderr.write('leaf node ' + options.leaf + ' not found (maybe already pruned : try -e0 -n0 flags)\n') 

3702 sys.exit(1) 

3703 profile.prune_leaf(leafIds, options.depth) 

3704 

3705 if options.compare: 

3706 dot.graphs_compare(profile1, profile2, theme, options) 

3707 else: 

3708 dot.graph(profile, theme) 

3709 

3710 

3711if __name__ == '__main__': 

3712 main()