diff options
Diffstat (limited to 'Tools/Scripts/webkitpy/common/system/profiler.py')
-rw-r--r-- | Tools/Scripts/webkitpy/common/system/profiler.py | 143 |
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() |