summaryrefslogtreecommitdiff
path: root/Tools/Scripts/webkitpy/common/system/profiler.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Scripts/webkitpy/common/system/profiler.py')
-rw-r--r--Tools/Scripts/webkitpy/common/system/profiler.py143
1 files changed, 127 insertions, 16 deletions
diff --git a/Tools/Scripts/webkitpy/common/system/profiler.py b/Tools/Scripts/webkitpy/common/system/profiler.py
index 264a4e238..0208cf898 100644
--- a/Tools/Scripts/webkitpy/common/system/profiler.py
+++ b/Tools/Scripts/webkitpy/common/system/profiler.py
@@ -28,19 +28,44 @@
import logging
import re
+import itertools
_log = logging.getLogger(__name__)
class ProfilerFactory(object):
@classmethod
- def create_profiler(cls, host, executable_path, output_dir, identifier=None):
- if host.platform.is_mac():
- return Instruments(host, executable_path, output_dir, identifier)
- return GooglePProf(host, executable_path, output_dir, identifier)
+ def create_profiler(cls, host, executable_path, output_dir, profiler_name=None, identifier=None):
+ profilers = cls.profilers_for_platform(host.platform)
+ if not profilers:
+ return None
+ profiler_name = profiler_name or cls.default_profiler_name(host.platform)
+ profiler_class = next(itertools.ifilter(lambda profiler: profiler.name == profiler_name, profilers), None)
+ if not profiler_class:
+ return None
+ return profilers[0](host, executable_path, output_dir, identifier)
+
+ @classmethod
+ def default_profiler_name(cls, platform):
+ profilers = cls.profilers_for_platform(platform)
+ return profilers[0].name if profilers else None
+
+ @classmethod
+ def profilers_for_platform(cls, platform):
+ # GooglePProf requires TCMalloc/google-perftools, but is available everywhere.
+ profilers_by_os_name = {
+ 'mac': [IProfiler, Sample, GooglePProf],
+ 'linux': [Perf, GooglePProf],
+ # Note: freebsd, win32 have no profilers defined yet, thus --profile will be ignored
+ # by default, but a profiler can be selected with --profiler=PROFILER explicitly.
+ }
+ return profilers_by_os_name.get(platform.os_name, [])
class Profiler(object):
+ # Used by ProfilerFactory to lookup a profiler from the --profiler=NAME option.
+ name = None
+
def __init__(self, host, executable_path, output_dir, identifier=None):
self._host = host
self._executable_path = executable_path
@@ -61,10 +86,14 @@ class Profiler(object):
class SingleFileOutputProfiler(Profiler):
def __init__(self, host, executable_path, output_dir, output_suffix, identifier=None):
super(SingleFileOutputProfiler, self).__init__(host, executable_path, output_dir, identifier)
- self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix)
+ # FIXME: Currently all reports are kept as test.*, until we fix that, search up to 1000 names before giving up.
+ self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix, search_limit=1000)
+ assert(self._output_path)
class GooglePProf(SingleFileOutputProfiler):
+ name = 'pprof'
+
def __init__(self, host, executable_path, output_dir, identifier=None):
super(GooglePProf, self).__init__(host, executable_path, output_dir, "pprof", identifier)
@@ -76,24 +105,106 @@ class GooglePProf(SingleFileOutputProfiler):
match = re.search("^Total:[^\n]*\n((?:[^\n]*\n){0,10})", pprof_output, re.MULTILINE)
return match.group(1) if match else None
- def profile_after_exit(self):
+ def _pprof_path(self):
# FIXME: We should have code to find the right google-pprof executable, some Googlers have
# google-pprof installed as "pprof" on their machines for them.
- # FIXME: Similarly we should find the right perl!
- pprof_args = ['/usr/bin/perl', '/usr/bin/google-pprof', '--text', self._executable_path, self._output_path]
+ return '/usr/bin/google-pprof'
+
+ def profile_after_exit(self):
+ # google-pprof doesn't check its arguments, so we have to.
+ if not (self._host.filesystem.exists(self._output_path)):
+ print "Failed to gather profile, %s does not exist." % self._output_path
+ return
+
+ pprof_args = [self._pprof_path(), '--text', self._executable_path, self._output_path]
profile_text = self._host.executive.run_command(pprof_args)
+ print "First 10 lines of pprof --text:"
print self._first_ten_lines_of_profile(profile_text)
+ print "http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html documents output."
+ print
+ print "To interact with the the full profile, including produce graphs:"
+ print ' '.join([self._pprof_path(), self._executable_path, self._output_path])
+
+class Perf(SingleFileOutputProfiler):
+ name = 'perf'
-# FIXME: iprofile is a newer commandline interface to replace /usr/bin/instruments.
-class Instruments(SingleFileOutputProfiler):
def __init__(self, host, executable_path, output_dir, identifier=None):
- super(Instruments, self).__init__(host, executable_path, output_dir, "trace", identifier)
+ super(Perf, self).__init__(host, executable_path, output_dir, "data", identifier)
+ self._perf_process = None
+ self._pid_being_profiled = None
- # FIXME: We may need a way to find this tracetemplate on the disk
- _time_profile = "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/Resources/templates/Time Profiler.tracetemplate"
+ def _perf_path(self):
+ # FIXME: We may need to support finding the perf binary in other locations.
+ return 'perf'
def attach_to_pid(self, pid):
- cmd = ["instruments", "-t", self._time_profile, "-D", self._output_path, "-p", pid]
- cmd = map(unicode, cmd)
- self._host.executive.popen(cmd)
+ assert(not self._perf_process and not self._pid_being_profiled)
+ self._pid_being_profiled = pid
+ cmd = [self._perf_path(), "record", "--call-graph", "--pid", pid, "--output", self._output_path]
+ self._perf_process = self._host.executive.popen(cmd)
+
+ def _first_ten_lines_of_profile(self, perf_output):
+ match = re.search("^#[^\n]*\n((?: [^\n]*\n){1,10})", perf_output, re.MULTILINE)
+ return match.group(1) if match else None
+
+ def profile_after_exit(self):
+ # Perf doesn't automatically watch the attached pid for death notifications,
+ # so we have to do it for it, and then tell it its time to stop sampling. :(
+ self._host.executive.wait_limited(self._pid_being_profiled, limit_in_seconds=10)
+ perf_exitcode = self._perf_process.poll()
+ if perf_exitcode is None: # This should always be the case, unless perf error'd out early.
+ self._host.executive.interrupt(self._perf_process.pid)
+
+ perf_exitcode = self._perf_process.wait()
+ if perf_exitcode not in (0, -2): # The exit code should always be -2, as we're always interrupting perf.
+ print "'perf record' failed (exit code: %i), can't process results:" % perf_exitcode
+ return
+
+ perf_args = [self._perf_path(), 'report', '--call-graph', 'none', '--input', self._output_path]
+ print "First 10 lines of 'perf report --call-graph=none':"
+
+ print " ".join(perf_args)
+ perf_output = self._host.executive.run_command(perf_args)
+ print self._first_ten_lines_of_profile(perf_output)
+
+ print "To view the full profile, run:"
+ print ' '.join([self._perf_path(), 'report', '-i', self._output_path])
+ print # An extra line between tests looks nicer.
+
+
+class Sample(SingleFileOutputProfiler):
+ name = 'sample'
+
+ def __init__(self, host, executable_path, output_dir, identifier=None):
+ super(Sample, self).__init__(host, executable_path, output_dir, "txt", identifier)
+ self._profiler_process = None
+
+ def attach_to_pid(self, pid):
+ cmd = ["sample", pid, "-mayDie", "-file", self._output_path]
+ self._profiler_process = self._host.executive.popen(cmd)
+
+ def profile_after_exit(self):
+ self._profiler_process.wait()
+
+
+class IProfiler(SingleFileOutputProfiler):
+ name = 'iprofiler'
+
+ def __init__(self, host, executable_path, output_dir, identifier=None):
+ super(IProfiler, self).__init__(host, executable_path, output_dir, "dtps", identifier)
+ self._profiler_process = None
+
+ def attach_to_pid(self, pid):
+ # FIXME: iprofiler requires us to pass the directory separately
+ # from the basename of the file, with no control over the extension.
+ fs = self._host.filesystem
+ cmd = ["iprofiler", "-timeprofiler", "-a", pid,
+ "-d", fs.dirname(self._output_path), "-o", fs.splitext(fs.basename(self._output_path))[0]]
+ # FIXME: Consider capturing instead of letting instruments spam to stderr directly.
+ self._profiler_process = self._host.executive.popen(cmd)
+
+ def profile_after_exit(self):
+ # It seems like a nicer user experiance to wait on the profiler to exit to prevent
+ # it from spewing to stderr at odd times.
+ self._profiler_process.wait()