Source

mercurial-crew-mq-inotify / kcachegrind

profiling: Adding support for kcachegrind output format, using lsprofcalltree

diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt
--- a/doc/hgrc.5.txt
+++ b/doc/hgrc.5.txt
@@ -551,6 +551,10 @@
       Use lsprof for profiling, and generates a profiling report.
       When saving to a file, it should be noted that only the report is saved,
       and that the profiling data is not kept.
+    kcachegrind;;
+      Use lsprof for profiling, and format profiling data for kcachegrind use:
+      when saving to a file, the generated file can directly be loaded
+      into kcachegrind.
   output;;
     File path where profiling data or report should be saved.
     If the file exists, it is replaced.
diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -381,7 +381,7 @@
     if options['profile']:
         format = ui.config('profiling', 'format', default='text')
 
-        if not format in ['text']:
+        if not format in ['text', 'kcachegrind']:
             ui.warn(_("unrecognized profiling format '%s'"
                         " - Ignored\n") % format)
             format = 'text'
@@ -407,9 +407,16 @@
             return checkargs()
         finally:
             p.disable()
-            stats = lsprof.Stats(p.getstats())
-            stats.sort()
-            stats.pprint(top=10, file=ostream, climit=5)
+
+            if format == 'kcachegrind':
+                import lsprofcalltree
+                calltree = lsprofcalltree.KCacheGrind(p)
+                calltree.output(ostream)
+            else:
+                # format == 'text'
+                stats = lsprof.Stats(p.getstats())
+                stats.sort()
+                stats.pprint(top=10, file=ostream, climit=5)
 
             if output:
                 ostream.close()
diff --git a/mercurial/lsprofcalltree.py b/mercurial/lsprofcalltree.py
new file mode 100644
--- /dev/null
+++ b/mercurial/lsprofcalltree.py
@@ -0,0 +1,90 @@
+"""
+lsprofcalltree.py - lsprof output which is readable by kcachegrind
+
+Authors:
+    * David Allouche <david <at> allouche.net>
+    * Jp Calderone & Itamar Shtull-Trauring
+    * Johan Dahlin
+
+This software may be used and distributed according to the terms
+of the GNU General Public License, incorporated herein by reference.
+"""
+
+import optparse
+import os
+import sys
+
+def label(code):
+    if isinstance(code, str):
+        return '~' + code    # built-in functions ('~' sorts at the end)
+    else:
+        return '%s %s:%d' % (code.co_name,
+                             code.co_filename,
+                             code.co_firstlineno)
+
+class KCacheGrind(object):
+    def __init__(self, profiler):
+        self.data = profiler.getstats()
+        self.out_file = None
+
+    def output(self, out_file):
+        self.out_file = out_file
+        print >> out_file, 'events: Ticks'
+        self._print_summary()
+        for entry in self.data:
+            self._entry(entry)
+
+    def _print_summary(self):
+        max_cost = 0
+        for entry in self.data:
+            totaltime = int(entry.totaltime * 1000)
+            max_cost = max(max_cost, totaltime)
+        print >> self.out_file, 'summary: %d' % (max_cost,)
+
+    def _entry(self, entry):
+        out_file = self.out_file
+
+        code = entry.code
+        #print >> out_file, 'ob=%s' % (code.co_filename,)
+        if isinstance(code, str):
+            print >> out_file, 'fi=~'
+        else:
+            print >> out_file, 'fi=%s' % (code.co_filename,)
+        print >> out_file, 'fn=%s' % (label(code),)
+
+        inlinetime = int(entry.inlinetime * 1000)
+        if isinstance(code, str):
+            print >> out_file, '0 ', inlinetime
+        else:
+            print >> out_file, '%d %d' % (code.co_firstlineno, inlinetime)
+
+        # recursive calls are counted in entry.calls
+        if entry.calls:
+            calls = entry.calls
+        else:
+            calls = []
+
+        if isinstance(code, str):
+            lineno = 0
+        else:
+            lineno = code.co_firstlineno
+
+        for subentry in calls:
+            self._subentry(lineno, subentry)
+        print >> out_file
+
+    def _subentry(self, lineno, subentry):
+        out_file = self.out_file
+        code = subentry.code
+        #print >> out_file, 'cob=%s' % (code.co_filename,)
+        print >> out_file, 'cfn=%s' % (label(code),)
+        if isinstance(code, str):
+            print >> out_file, 'cfi=~'
+            print >> out_file, 'calls=%d 0' % (subentry.callcount,)
+        else:
+            print >> out_file, 'cfi=%s' % (code.co_filename,)
+            print >> out_file, 'calls=%d %d' % (
+                subentry.callcount, code.co_firstlineno)
+
+        totaltime = int(subentry.totaltime * 1000)
+        print >> out_file, '%d %d' % (lineno, totaltime)
diff --git a/tests/test-profile b/tests/test-profile
--- a/tests/test-profile
+++ b/tests/test-profile
@@ -18,4 +18,15 @@
 
     hg --profile --config profiling.format=text st 2>&1 \
         | grep CallCount > /dev/null || echo --profile format=text failed
+
+    echo "[profiling]" >> $HGRCPATH
+    echo "format=kcachegrind" >> $HGRCPATH
+
+    hg --profile st 2>../out || echo --profile format=kcachegrind failed
+    grep 'events: Ticks' < ../out > /dev/null || echo --profile output is wrong
+
+    hg --profile --config profiling.output=../out st 2>&1 \
+        || echo --profile format=kcachegrind + output to file failed
+    grep 'events: Ticks' < ../out > /dev/null \
+        || echo --profile output is wrong
 fi