mercurial/statprof.py
changeset 43076 2372284d9457
parent 43053 9a3be115fb78
child 43077 687b865b95ad
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())