Mercurial > hg
comparison mercurial/statprof.py @ 43076:2372284d9457
formatting: blacken the codebase
This is using my patch to black
(https://github.com/psf/black/pull/826) so we don't un-wrap collection
literals.
Done with:
hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S
# skip-blame mass-reformatting only
# no-check-commit reformats foo_bar functions
Differential Revision: https://phab.mercurial-scm.org/D6971
author | Augie Fackler <augie@google.com> |
---|---|
date | Sun, 06 Oct 2019 09:45:02 -0400 |
parents | 9a3be115fb78 |
children | 687b865b95ad |
comparison
equal
deleted
inserted
replaced
43075:57875cf423c9 | 43076:2372284d9457 |
---|---|
142 } | 142 } |
143 | 143 |
144 ########################################################################### | 144 ########################################################################### |
145 ## Utils | 145 ## Utils |
146 | 146 |
147 | |
147 def clock(): | 148 def clock(): |
148 times = os.times() | 149 times = os.times() |
149 return (times[0] + times[1], times[4]) | 150 return (times[0] + times[1], times[4]) |
150 | 151 |
151 | 152 |
152 ########################################################################### | 153 ########################################################################### |
153 ## Collection data structures | 154 ## Collection data structures |
155 | |
154 | 156 |
155 class ProfileState(object): | 157 class ProfileState(object): |
156 def __init__(self, frequency=None): | 158 def __init__(self, frequency=None): |
157 self.reset(frequency) | 159 self.reset(frequency) |
158 self.track = 'cpu' | 160 self.track = 'cpu' |
194 def timeidx(self): | 196 def timeidx(self): |
195 if self.track == 'real': | 197 if self.track == 'real': |
196 return 1 | 198 return 1 |
197 return 0 | 199 return 0 |
198 | 200 |
201 | |
199 state = ProfileState() | 202 state = ProfileState() |
200 | 203 |
201 | 204 |
202 class CodeSite(object): | 205 class CodeSite(object): |
203 cache = {} | 206 cache = {} |
212 self.function = function | 215 self.function = function |
213 self.source = None | 216 self.source = None |
214 | 217 |
215 def __eq__(self, other): | 218 def __eq__(self, other): |
216 try: | 219 try: |
217 return (self.lineno == other.lineno and | 220 return self.lineno == other.lineno and self.path == other.path |
218 self.path == other.path) | |
219 except: | 221 except: |
220 return False | 222 return False |
221 | 223 |
222 def __hash__(self): | 224 def __hash__(self): |
223 return hash((self.lineno, self.path)) | 225 return hash((self.lineno, self.path)) |
246 if self.source is None: | 248 if self.source is None: |
247 self.source = '' | 249 self.source = '' |
248 | 250 |
249 source = self.source | 251 source = self.source |
250 if len(source) > length: | 252 if len(source) > length: |
251 source = source[:(length - 3)] + "..." | 253 source = source[: (length - 3)] + "..." |
252 return source | 254 return source |
253 | 255 |
254 def filename(self): | 256 def filename(self): |
255 return os.path.basename(self.path) | 257 return os.path.basename(self.path) |
256 | 258 |
257 def skipname(self): | 259 def skipname(self): |
258 return r'%s:%s' % (self.filename(), self.function) | 260 return r'%s:%s' % (self.filename(), self.function) |
261 | |
259 | 262 |
260 class Sample(object): | 263 class Sample(object): |
261 __slots__ = (r'stack', r'time') | 264 __slots__ = (r'stack', r'time') |
262 | 265 |
263 def __init__(self, stack, time): | 266 def __init__(self, stack, time): |
267 @classmethod | 270 @classmethod |
268 def from_frame(cls, frame, time): | 271 def from_frame(cls, frame, time): |
269 stack = [] | 272 stack = [] |
270 | 273 |
271 while frame: | 274 while frame: |
272 stack.append(CodeSite.get( | 275 stack.append( |
273 pycompat.sysbytes(frame.f_code.co_filename), | 276 CodeSite.get( |
274 frame.f_lineno, | 277 pycompat.sysbytes(frame.f_code.co_filename), |
275 pycompat.sysbytes(frame.f_code.co_name))) | 278 frame.f_lineno, |
279 pycompat.sysbytes(frame.f_code.co_name), | |
280 ) | |
281 ) | |
276 frame = frame.f_back | 282 frame = frame.f_back |
277 | 283 |
278 return Sample(stack, time) | 284 return Sample(stack, time) |
285 | |
279 | 286 |
280 ########################################################################### | 287 ########################################################################### |
281 ## SIGPROF handler | 288 ## SIGPROF handler |
289 | |
282 | 290 |
283 def profile_signal_handler(signum, frame): | 291 def profile_signal_handler(signum, frame): |
284 if state.profile_level > 0: | 292 if state.profile_level > 0: |
285 now = clock() | 293 now = clock() |
286 state.accumulate_time(now) | 294 state.accumulate_time(now) |
287 | 295 |
288 timestamp = state.accumulated_time[state.timeidx] | 296 timestamp = state.accumulated_time[state.timeidx] |
289 state.samples.append(Sample.from_frame(frame, timestamp)) | 297 state.samples.append(Sample.from_frame(frame, timestamp)) |
290 | 298 |
291 signal.setitimer(signal.ITIMER_PROF, | 299 signal.setitimer(signal.ITIMER_PROF, state.sample_interval, 0.0) |
292 state.sample_interval, 0.0) | |
293 state.last_start_time = now | 300 state.last_start_time = now |
294 | 301 |
302 | |
295 stopthread = threading.Event() | 303 stopthread = threading.Event() |
304 | |
305 | |
296 def samplerthread(tid): | 306 def samplerthread(tid): |
297 while not stopthread.is_set(): | 307 while not stopthread.is_set(): |
298 now = clock() | 308 now = clock() |
299 state.accumulate_time(now) | 309 state.accumulate_time(now) |
300 | 310 |
306 state.last_start_time = now | 316 state.last_start_time = now |
307 time.sleep(state.sample_interval) | 317 time.sleep(state.sample_interval) |
308 | 318 |
309 stopthread.clear() | 319 stopthread.clear() |
310 | 320 |
321 | |
311 ########################################################################### | 322 ########################################################################### |
312 ## Profiling API | 323 ## Profiling API |
313 | 324 |
325 | |
314 def is_active(): | 326 def is_active(): |
315 return state.profile_level > 0 | 327 return state.profile_level > 0 |
316 | 328 |
329 | |
317 lastmechanism = None | 330 lastmechanism = None |
331 | |
332 | |
318 def start(mechanism='thread', track='cpu'): | 333 def start(mechanism='thread', track='cpu'): |
319 '''Install the profiling signal handler, and start profiling.''' | 334 '''Install the profiling signal handler, and start profiling.''' |
320 state.track = track # note: nesting different mode won't work | 335 state.track = track # note: nesting different mode won't work |
321 state.profile_level += 1 | 336 state.profile_level += 1 |
322 if state.profile_level == 1: | 337 if state.profile_level == 1: |
323 state.last_start_time = clock() | 338 state.last_start_time = clock() |
324 rpt = state.remaining_prof_time | 339 rpt = state.remaining_prof_time |
325 state.remaining_prof_time = None | 340 state.remaining_prof_time = None |
327 global lastmechanism | 342 global lastmechanism |
328 lastmechanism = mechanism | 343 lastmechanism = mechanism |
329 | 344 |
330 if mechanism == 'signal': | 345 if mechanism == 'signal': |
331 signal.signal(signal.SIGPROF, profile_signal_handler) | 346 signal.signal(signal.SIGPROF, profile_signal_handler) |
332 signal.setitimer(signal.ITIMER_PROF, | 347 signal.setitimer( |
333 rpt or state.sample_interval, 0.0) | 348 signal.ITIMER_PROF, rpt or state.sample_interval, 0.0 |
349 ) | |
334 elif mechanism == 'thread': | 350 elif mechanism == 'thread': |
335 frame = inspect.currentframe() | 351 frame = inspect.currentframe() |
336 tid = [k for k, f in sys._current_frames().items() if f == frame][0] | 352 tid = [k for k, f in sys._current_frames().items() if f == frame][0] |
337 state.thread = threading.Thread(target=samplerthread, | 353 state.thread = threading.Thread( |
338 args=(tid,), name="samplerthread") | 354 target=samplerthread, args=(tid,), name="samplerthread" |
355 ) | |
339 state.thread.start() | 356 state.thread.start() |
357 | |
340 | 358 |
341 def stop(): | 359 def stop(): |
342 '''Stop profiling, and uninstall the profiling signal handler.''' | 360 '''Stop profiling, and uninstall the profiling signal handler.''' |
343 state.profile_level -= 1 | 361 state.profile_level -= 1 |
344 if state.profile_level == 0: | 362 if state.profile_level == 0: |
356 if statprofpath: | 374 if statprofpath: |
357 save_data(statprofpath) | 375 save_data(statprofpath) |
358 | 376 |
359 return state | 377 return state |
360 | 378 |
379 | |
361 def save_data(path): | 380 def save_data(path): |
362 with open(path, 'w+') as file: | 381 with open(path, 'w+') as file: |
363 file.write("%f %f\n" % state.accumulated_time) | 382 file.write("%f %f\n" % state.accumulated_time) |
364 for sample in state.samples: | 383 for sample in state.samples: |
365 time = sample.time | 384 time = sample.time |
366 stack = sample.stack | 385 stack = sample.stack |
367 sites = ['\1'.join([s.path, b'%d' % s.lineno, s.function]) | 386 sites = [ |
368 for s in stack] | 387 '\1'.join([s.path, b'%d' % s.lineno, s.function]) for s in stack |
388 ] | |
369 file.write("%d\0%s\n" % (time, '\0'.join(sites))) | 389 file.write("%d\0%s\n" % (time, '\0'.join(sites))) |
390 | |
370 | 391 |
371 def load_data(path): | 392 def load_data(path): |
372 lines = open(path, 'rb').read().splitlines() | 393 lines = open(path, 'rb').read().splitlines() |
373 | 394 |
374 state.accumulated_time = [float(value) for value in lines[0].split()] | 395 state.accumulated_time = [float(value) for value in lines[0].split()] |
378 time = float(parts[0]) | 399 time = float(parts[0]) |
379 rawsites = parts[1:] | 400 rawsites = parts[1:] |
380 sites = [] | 401 sites = [] |
381 for rawsite in rawsites: | 402 for rawsite in rawsites: |
382 siteparts = rawsite.split('\1') | 403 siteparts = rawsite.split('\1') |
383 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]), | 404 sites.append( |
384 siteparts[2])) | 405 CodeSite.get(siteparts[0], int(siteparts[1]), siteparts[2]) |
406 ) | |
385 | 407 |
386 state.samples.append(Sample(sites, time)) | 408 state.samples.append(Sample(sites, time)) |
387 | |
388 | 409 |
389 | 410 |
390 def reset(frequency=None): | 411 def reset(frequency=None): |
391 '''Clear out the state of the profiler. Do not call while the | 412 '''Clear out the state of the profiler. Do not call while the |
392 profiler is running. | 413 profiler is running. |
409 | 430 |
410 | 431 |
411 ########################################################################### | 432 ########################################################################### |
412 ## Reporting API | 433 ## Reporting API |
413 | 434 |
435 | |
414 class SiteStats(object): | 436 class SiteStats(object): |
415 def __init__(self, site): | 437 def __init__(self, site): |
416 self.site = site | 438 self.site = site |
417 self.selfcount = 0 | 439 self.selfcount = 0 |
418 self.totalcount = 0 | 440 self.totalcount = 0 |
450 | 472 |
451 if i == 0: | 473 if i == 0: |
452 sitestat.addself() | 474 sitestat.addself() |
453 | 475 |
454 return [s for s in stats.itervalues()] | 476 return [s for s in stats.itervalues()] |
477 | |
455 | 478 |
456 class DisplayFormats: | 479 class DisplayFormats: |
457 ByLine = 0 | 480 ByLine = 0 |
458 ByMethod = 1 | 481 ByMethod = 1 |
459 AboutMethod = 2 | 482 AboutMethod = 2 |
460 Hotpath = 3 | 483 Hotpath = 3 |
461 FlameGraph = 4 | 484 FlameGraph = 4 |
462 Json = 5 | 485 Json = 5 |
463 Chrome = 6 | 486 Chrome = 6 |
464 | 487 |
488 | |
465 def display(fp=None, format=3, data=None, **kwargs): | 489 def display(fp=None, format=3, data=None, **kwargs): |
466 '''Print statistics, either to stdout or the given file object.''' | 490 '''Print statistics, either to stdout or the given file object.''' |
467 if data is None: | 491 if data is None: |
468 data = state | 492 data = state |
469 | 493 |
470 if fp is None: | 494 if fp is None: |
471 import sys | 495 import sys |
496 | |
472 fp = sys.stdout | 497 fp = sys.stdout |
473 if len(data.samples) == 0: | 498 if len(data.samples) == 0: |
474 fp.write(b'No samples recorded.\n') | 499 fp.write(b'No samples recorded.\n') |
475 return | 500 return |
476 | 501 |
494 if format not in (DisplayFormats.Json, DisplayFormats.Chrome): | 519 if format not in (DisplayFormats.Json, DisplayFormats.Chrome): |
495 fp.write(b'---\n') | 520 fp.write(b'---\n') |
496 fp.write(b'Sample count: %d\n' % len(data.samples)) | 521 fp.write(b'Sample count: %d\n' % len(data.samples)) |
497 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time) | 522 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time) |
498 | 523 |
524 | |
499 def display_by_line(data, fp): | 525 def display_by_line(data, fp): |
500 '''Print the profiler data with each sample line represented | 526 '''Print the profiler data with each sample line represented |
501 as one row in a table. Sorted by self-time per line.''' | 527 as one row in a table. Sorted by self-time per line.''' |
502 stats = SiteStats.buildstats(data.samples) | 528 stats = SiteStats.buildstats(data.samples) |
503 stats.sort(reverse=True, key=lambda x: x.selfseconds()) | 529 stats.sort(reverse=True, key=lambda x: x.selfseconds()) |
504 | 530 |
505 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % ( | 531 fp.write( |
506 b'% ', b'cumulative', b'self', b'')) | 532 b'%5.5s %10.10s %7.7s %-8.8s\n' |
507 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % ( | 533 % (b'% ', b'cumulative', b'self', b'') |
508 b"time", b"seconds", b"seconds", b"name")) | 534 ) |
535 fp.write( | |
536 b'%5.5s %9.9s %8.8s %-8.8s\n' | |
537 % (b"time", b"seconds", b"seconds", b"name") | |
538 ) | |
509 | 539 |
510 for stat in stats: | 540 for stat in stats: |
511 site = stat.site | 541 site = stat.site |
512 sitelabel = '%s:%d:%s' % (site.filename(), | 542 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function) |
513 site.lineno, | 543 fp.write( |
514 site.function) | 544 b'%6.2f %9.2f %9.2f %s\n' |
515 fp.write(b'%6.2f %9.2f %9.2f %s\n' % ( | 545 % ( |
516 stat.selfpercent(), stat.totalseconds(), | 546 stat.selfpercent(), |
517 stat.selfseconds(), sitelabel)) | 547 stat.totalseconds(), |
548 stat.selfseconds(), | |
549 sitelabel, | |
550 ) | |
551 ) | |
552 | |
518 | 553 |
519 def display_by_method(data, fp): | 554 def display_by_method(data, fp): |
520 '''Print the profiler data with each sample function represented | 555 '''Print the profiler data with each sample function represented |
521 as one row in a table. Important lines within that function are | 556 as one row in a table. Important lines within that function are |
522 output as nested rows. Sorted by self-time per line.''' | 557 output as nested rows. Sorted by self-time per line.''' |
523 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % | 558 fp.write( |
524 ('% ', 'cumulative', 'self', '')) | 559 b'%5.5s %10.10s %7.7s %-8.8s\n' % ('% ', 'cumulative', 'self', '') |
525 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % | 560 ) |
526 ("time", "seconds", "seconds", "name")) | 561 fp.write( |
562 b'%5.5s %9.9s %8.8s %-8.8s\n' | |
563 % ("time", "seconds", "seconds", "name") | |
564 ) | |
527 | 565 |
528 stats = SiteStats.buildstats(data.samples) | 566 stats = SiteStats.buildstats(data.samples) |
529 | 567 |
530 grouped = defaultdict(list) | 568 grouped = defaultdict(list) |
531 for stat in stats: | 569 for stat in stats: |
540 for stat in sitestats: | 578 for stat in sitestats: |
541 total_cum_sec += stat.totalseconds() | 579 total_cum_sec += stat.totalseconds() |
542 total_self_sec += stat.selfseconds() | 580 total_self_sec += stat.selfseconds() |
543 total_percent += stat.selfpercent() | 581 total_percent += stat.selfpercent() |
544 | 582 |
545 functiondata.append((fname, | 583 functiondata.append( |
546 total_cum_sec, | 584 (fname, total_cum_sec, total_self_sec, total_percent, sitestats) |
547 total_self_sec, | 585 ) |
548 total_percent, | |
549 sitestats)) | |
550 | 586 |
551 # sort by total self sec | 587 # sort by total self sec |
552 functiondata.sort(reverse=True, key=lambda x: x[2]) | 588 functiondata.sort(reverse=True, key=lambda x: x[2]) |
553 | 589 |
554 for function in functiondata: | 590 for function in functiondata: |
555 if function[3] < 0.05: | 591 if function[3] < 0.05: |
556 continue | 592 continue |
557 fp.write(b'%6.2f %9.2f %9.2f %s\n' % ( | 593 fp.write( |
558 function[3], # total percent | 594 b'%6.2f %9.2f %9.2f %s\n' |
559 function[1], # total cum sec | 595 % ( |
560 function[2], # total self sec | 596 function[3], # total percent |
561 function[0])) # file:function | 597 function[1], # total cum sec |
598 function[2], # total self sec | |
599 function[0], | |
600 ) | |
601 ) # file:function | |
562 | 602 |
563 function[4].sort(reverse=True, key=lambda i: i.selfseconds()) | 603 function[4].sort(reverse=True, key=lambda i: i.selfseconds()) |
564 for stat in function[4]: | 604 for stat in function[4]: |
565 # only show line numbers for significant locations (>1% time spent) | 605 # only show line numbers for significant locations (>1% time spent) |
566 if stat.selfpercent() > 1: | 606 if stat.selfpercent() > 1: |
567 source = stat.site.getsource(25) | 607 source = stat.site.getsource(25) |
568 if sys.version_info.major >= 3 and not isinstance(source, bytes): | 608 if sys.version_info.major >= 3 and not isinstance( |
609 source, bytes | |
610 ): | |
569 source = pycompat.bytestr(source) | 611 source = pycompat.bytestr(source) |
570 | 612 |
571 stattuple = (stat.selfpercent(), stat.selfseconds(), | 613 stattuple = ( |
572 stat.site.lineno, source) | 614 stat.selfpercent(), |
615 stat.selfseconds(), | |
616 stat.site.lineno, | |
617 source, | |
618 ) | |
573 | 619 |
574 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple) | 620 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple) |
621 | |
575 | 622 |
576 def display_about_method(data, fp, function=None, **kwargs): | 623 def display_about_method(data, fp, function=None, **kwargs): |
577 if function is None: | 624 if function is None: |
578 raise Exception("Invalid function") | 625 raise Exception("Invalid function") |
579 | 626 |
585 parents = {} | 632 parents = {} |
586 children = {} | 633 children = {} |
587 | 634 |
588 for sample in data.samples: | 635 for sample in data.samples: |
589 for i, site in enumerate(sample.stack): | 636 for i, site in enumerate(sample.stack): |
590 if site.function == function and (not filename | 637 if site.function == function and ( |
591 or site.filename() == filename): | 638 not filename or site.filename() == filename |
639 ): | |
592 relevant_samples += 1 | 640 relevant_samples += 1 |
593 if i != len(sample.stack) - 1: | 641 if i != len(sample.stack) - 1: |
594 parent = sample.stack[i + 1] | 642 parent = sample.stack[i + 1] |
595 if parent in parents: | 643 if parent in parents: |
596 parents[parent] = parents[parent] + 1 | 644 parents[parent] = parents[parent] + 1 |
603 children[site] = 1 | 651 children[site] = 1 |
604 | 652 |
605 parents = [(parent, count) for parent, count in parents.iteritems()] | 653 parents = [(parent, count) for parent, count in parents.iteritems()] |
606 parents.sort(reverse=True, key=lambda x: x[1]) | 654 parents.sort(reverse=True, key=lambda x: x[1]) |
607 for parent, count in parents: | 655 for parent, count in parents: |
608 fp.write(b'%6.2f%% %s:%s line %s: %s\n' % | 656 fp.write( |
609 (count / relevant_samples * 100, | 657 b'%6.2f%% %s:%s line %s: %s\n' |
610 pycompat.fsencode(parent.filename()), | 658 % ( |
611 pycompat.sysbytes(parent.function), | 659 count / relevant_samples * 100, |
612 parent.lineno, | 660 pycompat.fsencode(parent.filename()), |
613 pycompat.sysbytes(parent.getsource(50)))) | 661 pycompat.sysbytes(parent.function), |
662 parent.lineno, | |
663 pycompat.sysbytes(parent.getsource(50)), | |
664 ) | |
665 ) | |
614 | 666 |
615 stats = SiteStats.buildstats(data.samples) | 667 stats = SiteStats.buildstats(data.samples) |
616 stats = [s for s in stats | 668 stats = [ |
617 if s.site.function == function and | 669 s |
618 (not filename or s.site.filename() == filename)] | 670 for s in stats |
671 if s.site.function == function | |
672 and (not filename or s.site.filename() == filename) | |
673 ] | |
619 | 674 |
620 total_cum_sec = 0 | 675 total_cum_sec = 0 |
621 total_self_sec = 0 | 676 total_self_sec = 0 |
622 total_self_percent = 0 | 677 total_self_percent = 0 |
623 total_cum_percent = 0 | 678 total_cum_percent = 0 |
628 total_cum_percent += stat.totalpercent() | 683 total_cum_percent += stat.totalpercent() |
629 | 684 |
630 fp.write( | 685 fp.write( |
631 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n' | 686 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n' |
632 % ( | 687 % ( |
633 pycompat.sysbytes(filename or '___'), | 688 pycompat.sysbytes(filename or '___'), |
634 pycompat.sysbytes(function), | 689 pycompat.sysbytes(function), |
635 total_cum_sec, | 690 total_cum_sec, |
636 total_cum_percent, | 691 total_cum_percent, |
637 total_self_sec, | 692 total_self_sec, |
638 total_self_percent | 693 total_self_percent, |
639 )) | 694 ) |
695 ) | |
640 | 696 |
641 children = [(child, count) for child, count in children.iteritems()] | 697 children = [(child, count) for child, count in children.iteritems()] |
642 children.sort(reverse=True, key=lambda x: x[1]) | 698 children.sort(reverse=True, key=lambda x: x[1]) |
643 for child, count in children: | 699 for child, count in children: |
644 fp.write(b' %6.2f%% line %s: %s\n' % | 700 fp.write( |
645 (count / relevant_samples * 100, child.lineno, | 701 b' %6.2f%% line %s: %s\n' |
646 pycompat.sysbytes(child.getsource(50)))) | 702 % ( |
703 count / relevant_samples * 100, | |
704 child.lineno, | |
705 pycompat.sysbytes(child.getsource(50)), | |
706 ) | |
707 ) | |
708 | |
647 | 709 |
648 def display_hotpath(data, fp, limit=0.05, **kwargs): | 710 def display_hotpath(data, fp, limit=0.05, **kwargs): |
649 class HotNode(object): | 711 class HotNode(object): |
650 def __init__(self, site): | 712 def __init__(self, site): |
651 self.site = site | 713 self.site = site |
675 lasttime = sample.time | 737 lasttime = sample.time |
676 showtime = kwargs.get(r'showtime', True) | 738 showtime = kwargs.get(r'showtime', True) |
677 | 739 |
678 def _write(node, depth, multiple_siblings): | 740 def _write(node, depth, multiple_siblings): |
679 site = node.site | 741 site = node.site |
680 visiblechildren = [c for c in node.children.itervalues() | 742 visiblechildren = [ |
681 if c.count >= (limit * root.count)] | 743 c |
744 for c in node.children.itervalues() | |
745 if c.count >= (limit * root.count) | |
746 ] | |
682 if site: | 747 if site: |
683 indent = depth * 2 - 1 | 748 indent = depth * 2 - 1 |
684 filename = '' | 749 filename = '' |
685 function = '' | 750 function = '' |
686 if len(node.children) > 0: | 751 if len(node.children) > 0: |
687 childsite = list(node.children.itervalues())[0].site | 752 childsite = list(node.children.itervalues())[0].site |
688 filename = (childsite.filename() + ':').ljust(15) | 753 filename = (childsite.filename() + ':').ljust(15) |
689 function = childsite.function | 754 function = childsite.function |
690 | 755 |
691 # lots of string formatting | 756 # lots of string formatting |
692 listpattern = ''.ljust(indent) +\ | 757 listpattern = ( |
693 ('\\' if multiple_siblings else '|') +\ | 758 ''.ljust(indent) |
694 ' %4.1f%%' +\ | 759 + ('\\' if multiple_siblings else '|') |
695 (' %5.2fs' % node.count if showtime else '') +\ | 760 + ' %4.1f%%' |
696 ' %s %s' | 761 + (' %5.2fs' % node.count if showtime else '') |
697 liststring = listpattern % (node.count / root.count * 100, | 762 + ' %s %s' |
698 filename, function) | 763 ) |
764 liststring = listpattern % ( | |
765 node.count / root.count * 100, | |
766 filename, | |
767 function, | |
768 ) | |
699 codepattern = '%' + ('%d' % (55 - len(liststring))) + 's %d: %s' | 769 codepattern = '%' + ('%d' % (55 - len(liststring))) + 's %d: %s' |
700 codestring = codepattern % ('line', site.lineno, site.getsource(30)) | 770 codestring = codepattern % ('line', site.lineno, site.getsource(30)) |
701 | 771 |
702 finalstring = liststring + codestring | 772 finalstring = liststring + codestring |
703 childrensamples = sum([c.count for c in node.children.itervalues()]) | 773 childrensamples = sum([c.count for c in node.children.itervalues()]) |
718 _write(child, newdepth, len(visiblechildren) > 1) | 788 _write(child, newdepth, len(visiblechildren) > 1) |
719 | 789 |
720 if root.count > 0: | 790 if root.count > 0: |
721 _write(root, 0, False) | 791 _write(root, 0, False) |
722 | 792 |
793 | |
723 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs): | 794 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs): |
724 if scriptpath is None: | 795 if scriptpath is None: |
725 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl' | 796 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl' |
726 if not os.path.exists(scriptpath): | 797 if not os.path.exists(scriptpath): |
727 fp.write(b'error: missing %s\n' % scriptpath) | 798 fp.write(b'error: missing %s\n' % scriptpath) |
748 outputfile = '~/flamegraph.svg' | 819 outputfile = '~/flamegraph.svg' |
749 | 820 |
750 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile)) | 821 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile)) |
751 fp.write(b'Written to %s\n' % outputfile) | 822 fp.write(b'Written to %s\n' % outputfile) |
752 | 823 |
824 | |
753 _pathcache = {} | 825 _pathcache = {} |
826 | |
827 | |
754 def simplifypath(path): | 828 def simplifypath(path): |
755 '''Attempt to make the path to a Python module easier to read by | 829 '''Attempt to make the path to a Python module easier to read by |
756 removing whatever part of the Python search path it was found | 830 removing whatever part of the Python search path it was found |
757 on.''' | 831 on.''' |
758 | 832 |
760 return _pathcache[path] | 834 return _pathcache[path] |
761 hgpath = encoding.__file__.rsplit(os.sep, 2)[0] | 835 hgpath = encoding.__file__.rsplit(os.sep, 2)[0] |
762 for p in [hgpath] + sys.path: | 836 for p in [hgpath] + sys.path: |
763 prefix = p + os.sep | 837 prefix = p + os.sep |
764 if path.startswith(prefix): | 838 if path.startswith(prefix): |
765 path = path[len(prefix):] | 839 path = path[len(prefix) :] |
766 break | 840 break |
767 _pathcache[path] = path | 841 _pathcache[path] = path |
768 return path | 842 return path |
769 | 843 |
844 | |
770 def write_to_json(data, fp): | 845 def write_to_json(data, fp): |
771 samples = [] | 846 samples = [] |
772 | 847 |
773 for sample in data.samples: | 848 for sample in data.samples: |
774 stack = [] | 849 stack = [] |
775 | 850 |
776 for frame in sample.stack: | 851 for frame in sample.stack: |
777 stack.append( | 852 stack.append( |
778 (pycompat.sysstr(frame.path), | 853 ( |
779 frame.lineno, | 854 pycompat.sysstr(frame.path), |
780 pycompat.sysstr(frame.function))) | 855 frame.lineno, |
856 pycompat.sysstr(frame.function), | |
857 ) | |
858 ) | |
781 | 859 |
782 samples.append((sample.time, stack)) | 860 samples.append((sample.time, stack)) |
783 | 861 |
784 data = json.dumps(samples) | 862 data = json.dumps(samples) |
785 if not isinstance(data, bytes): | 863 if not isinstance(data, bytes): |
786 data = data.encode('utf-8') | 864 data = data.encode('utf-8') |
787 | 865 |
788 fp.write(data) | 866 fp.write(data) |
867 | |
789 | 868 |
790 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999): | 869 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999): |
791 samples = [] | 870 samples = [] |
792 laststack = collections.deque() | 871 laststack = collections.deque() |
793 lastseen = collections.deque() | 872 lastseen = collections.deque() |
794 | 873 |
795 # The Chrome tracing format allows us to use a compact stack | 874 # The Chrome tracing format allows us to use a compact stack |
796 # representation to save space. It's fiddly but worth it. | 875 # representation to save space. It's fiddly but worth it. |
797 # We maintain a bijection between stack and ID. | 876 # We maintain a bijection between stack and ID. |
798 stack2id = {} | 877 stack2id = {} |
799 id2stack = [] # will eventually be rendered | 878 id2stack = [] # will eventually be rendered |
800 | 879 |
801 def stackid(stack): | 880 def stackid(stack): |
802 if not stack: | 881 if not stack: |
803 return | 882 return |
804 if stack in stack2id: | 883 if stack in stack2id: |
839 oldtime, oldidx = lastseen.popleft() | 918 oldtime, oldidx = lastseen.popleft() |
840 duration = sample.time - oldtime | 919 duration = sample.time - oldtime |
841 if minthreshold <= duration <= maxthreshold: | 920 if minthreshold <= duration <= maxthreshold: |
842 # ensure no zero-duration events | 921 # ensure no zero-duration events |
843 sampletime = max(oldtime + clamp, sample.time) | 922 sampletime = max(oldtime + clamp, sample.time) |
844 samples.append(dict(ph=r'E', name=oldfunc, cat=oldcat, sf=oldsid, | 923 samples.append( |
845 ts=sampletime*1e6, pid=0)) | 924 dict( |
925 ph=r'E', | |
926 name=oldfunc, | |
927 cat=oldcat, | |
928 sf=oldsid, | |
929 ts=sampletime * 1e6, | |
930 pid=0, | |
931 ) | |
932 ) | |
846 else: | 933 else: |
847 blacklist.add(oldidx) | 934 blacklist.add(oldidx) |
848 | 935 |
849 # Much fiddling to synthesize correctly(ish) nested begin/end | 936 # Much fiddling to synthesize correctly(ish) nested begin/end |
850 # events given only stack snapshots. | 937 # events given only stack snapshots. |
851 | 938 |
852 for sample in data.samples: | 939 for sample in data.samples: |
853 stack = tuple(((r'%s:%d' % (simplifypath(pycompat.sysstr(frame.path)), | 940 stack = tuple( |
854 frame.lineno), | 941 ( |
855 pycompat.sysstr(frame.function)) | 942 ( |
856 for frame in sample.stack)) | 943 r'%s:%d' |
944 % (simplifypath(pycompat.sysstr(frame.path)), frame.lineno), | |
945 pycompat.sysstr(frame.function), | |
946 ) | |
947 for frame in sample.stack | |
948 ) | |
949 ) | |
857 qstack = collections.deque(stack) | 950 qstack = collections.deque(stack) |
858 if laststack == qstack: | 951 if laststack == qstack: |
859 continue | 952 continue |
860 while laststack and qstack and laststack[-1] == qstack[-1]: | 953 while laststack and qstack and laststack[-1] == qstack[-1]: |
861 laststack.pop() | 954 laststack.pop() |
865 for f in reversed(qstack): | 958 for f in reversed(qstack): |
866 lastseen.appendleft((sample.time, len(samples))) | 959 lastseen.appendleft((sample.time, len(samples))) |
867 laststack.appendleft(f) | 960 laststack.appendleft(f) |
868 path, name = f | 961 path, name = f |
869 sid = stackid(tuple(laststack)) | 962 sid = stackid(tuple(laststack)) |
870 samples.append(dict(ph=r'B', name=name, cat=path, | 963 samples.append( |
871 ts=sample.time*1e6, sf=sid, pid=0)) | 964 dict( |
965 ph=r'B', | |
966 name=name, | |
967 cat=path, | |
968 ts=sample.time * 1e6, | |
969 sf=sid, | |
970 pid=0, | |
971 ) | |
972 ) | |
872 laststack = collections.deque(stack) | 973 laststack = collections.deque(stack) |
873 while laststack: | 974 while laststack: |
874 poplast() | 975 poplast() |
875 events = [sample for idx, sample in enumerate(samples) | 976 events = [ |
876 if idx not in blacklist] | 977 sample for idx, sample in enumerate(samples) if idx not in blacklist |
877 frames = collections.OrderedDict((str(k), v) | 978 ] |
878 for (k,v) in enumerate(id2stack)) | 979 frames = collections.OrderedDict( |
980 (str(k), v) for (k, v) in enumerate(id2stack) | |
981 ) | |
879 data = json.dumps(dict(traceEvents=events, stackFrames=frames), indent=1) | 982 data = json.dumps(dict(traceEvents=events, stackFrames=frames), indent=1) |
880 if not isinstance(data, bytes): | 983 if not isinstance(data, bytes): |
881 data = data.encode('utf-8') | 984 data = data.encode('utf-8') |
882 fp.write(data) | 985 fp.write(data) |
883 fp.write('\n') | 986 fp.write('\n') |
884 | 987 |
988 | |
885 def printusage(): | 989 def printusage(): |
886 print(r""" | 990 print( |
991 r""" | |
887 The statprof command line allows you to inspect the last profile's results in | 992 The statprof command line allows you to inspect the last profile's results in |
888 the following forms: | 993 the following forms: |
889 | 994 |
890 usage: | 995 usage: |
891 hotpath [-l --limit percent] | 996 hotpath [-l --limit percent] |
898 function [filename:]functionname | 1003 function [filename:]functionname |
899 Shows the callers and callees of a particular function. | 1004 Shows the callers and callees of a particular function. |
900 flame [-s --script-path] [-o --output-file path] | 1005 flame [-s --script-path] [-o --output-file path] |
901 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg) | 1006 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg) |
902 Requires that ~/flamegraph.pl exist. | 1007 Requires that ~/flamegraph.pl exist. |
903 (Specify alternate script path with --script-path.)""") | 1008 (Specify alternate script path with --script-path.)""" |
1009 ) | |
1010 | |
904 | 1011 |
905 def main(argv=None): | 1012 def main(argv=None): |
906 if argv is None: | 1013 if argv is None: |
907 argv = sys.argv | 1014 argv = sys.argv |
908 | 1015 |
930 printusage() | 1037 printusage() |
931 return 0 | 1038 return 0 |
932 | 1039 |
933 # process options | 1040 # process options |
934 try: | 1041 try: |
935 opts, args = pycompat.getoptb(sys.argv[optstart:], "hl:f:o:p:", | 1042 opts, args = pycompat.getoptb( |
936 ["help", "limit=", "file=", "output-file=", "script-path="]) | 1043 sys.argv[optstart:], |
1044 "hl:f:o:p:", | |
1045 ["help", "limit=", "file=", "output-file=", "script-path="], | |
1046 ) | |
937 except getopt.error as msg: | 1047 except getopt.error as msg: |
938 print(msg) | 1048 print(msg) |
939 printusage() | 1049 printusage() |
940 return 2 | 1050 return 2 |
941 | 1051 |
964 | 1074 |
965 display(**pycompat.strkwargs(displayargs)) | 1075 display(**pycompat.strkwargs(displayargs)) |
966 | 1076 |
967 return 0 | 1077 return 0 |
968 | 1078 |
1079 | |
969 if __name__ == r"__main__": | 1080 if __name__ == r"__main__": |
970 sys.exit(main()) | 1081 sys.exit(main()) |