1 # Copyright (C) 2012 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the Google name nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 import logging 30 import re 31 import itertools 32 33 _log = logging.getLogger(__name__) 34 35 36 class ProfilerFactory(object): 37 @classmethod 38 def create_profiler(cls, host, executable_path, output_dir, profiler_name=None, identifier=None): 39 profilers = cls.profilers_for_platform(host.platform) 40 if not profilers: 41 return None 42 profiler_name = profiler_name or cls.default_profiler_name(host.platform) 43 profiler_class = next(itertools.ifilter(lambda profiler: profiler.name == profiler_name, profilers), None) 44 if not profiler_class: 45 return None 46 return profilers[0](host, executable_path, output_dir, identifier) 47 48 @classmethod 49 def default_profiler_name(cls, platform): 50 profilers = cls.profilers_for_platform(platform) 51 return profilers[0].name if profilers else None 52 53 @classmethod 54 def profilers_for_platform(cls, platform): 55 # GooglePProf requires TCMalloc/google-perftools, but is available everywhere. 56 profilers_by_os_name = { 57 'mac': [IProfiler, Sample, GooglePProf], 58 'linux': [Perf, GooglePProf], 59 # Note: freebsd, win32 have no profilers defined yet, thus --profile will be ignored 60 # by default, but a profiler can be selected with --profiler=PROFILER explicitly. 61 } 62 return profilers_by_os_name.get(platform.os_name, []) 63 64 65 class Profiler(object): 66 # Used by ProfilerFactory to lookup a profiler from the --profiler=NAME option. 67 name = None 68 69 def __init__(self, host, executable_path, output_dir, identifier=None): 70 self._host = host 71 self._executable_path = executable_path 72 self._output_dir = output_dir 73 self._identifier = "test" 74 self._host.filesystem.maybe_make_directory(self._output_dir) 75 76 def adjusted_environment(self, env): 77 return env 78 79 def attach_to_pid(self, pid): 80 pass 81 82 def profile_after_exit(self): 83 pass 84 85 86 class SingleFileOutputProfiler(Profiler): 87 def __init__(self, host, executable_path, output_dir, output_suffix, identifier=None): 88 super(SingleFileOutputProfiler, self).__init__(host, executable_path, output_dir, identifier) 89 # FIXME: Currently all reports are kept as test.*, until we fix that, search up to 1000 names before giving up. 90 self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix, search_limit=1000) 91 assert(self._output_path) 92 93 94 class GooglePProf(SingleFileOutputProfiler): 95 name = 'pprof' 96 97 def __init__(self, host, executable_path, output_dir, identifier=None): 98 super(GooglePProf, self).__init__(host, executable_path, output_dir, "pprof", identifier) 99 100 def adjusted_environment(self, env): 101 env['CPUPROFILE'] = self._output_path 102 return env 103 104 def _first_ten_lines_of_profile(self, pprof_output): 105 match = re.search("^Total:[^\n]*\n((?:[^\n]*\n){0,10})", pprof_output, re.MULTILINE) 106 return match.group(1) if match else None 107 108 def _pprof_path(self): 109 # FIXME: We should have code to find the right google-pprof executable, some Googlers have 110 # google-pprof installed as "pprof" on their machines for them. 111 return '/usr/bin/google-pprof' 112 113 def profile_after_exit(self): 114 # google-pprof doesn't check its arguments, so we have to. 115 if not (self._host.filesystem.exists(self._output_path)): 116 print "Failed to gather profile, %s does not exist." % self._output_path 117 return 118 119 pprof_args = [self._pprof_path(), '--text', self._executable_path, self._output_path] 120 profile_text = self._host.executive.run_command(pprof_args) 121 print "First 10 lines of pprof --text:" 122 print self._first_ten_lines_of_profile(profile_text) 123 print "http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html documents output." 124 print 125 print "To interact with the the full profile, including produce graphs:" 126 print ' '.join([self._pprof_path(), self._executable_path, self._output_path]) 127 128 129 class Perf(SingleFileOutputProfiler): 130 name = 'perf' 131 132 def __init__(self, host, executable_path, output_dir, identifier=None): 133 super(Perf, self).__init__(host, executable_path, output_dir, "data", identifier) 134 self._perf_process = None 135 self._pid_being_profiled = None 136 137 def _perf_path(self): 138 # FIXME: We may need to support finding the perf binary in other locations. 139 return 'perf' 140 141 def attach_to_pid(self, pid): 142 assert(not self._perf_process and not self._pid_being_profiled) 143 self._pid_being_profiled = pid 144 cmd = [self._perf_path(), "record", "--call-graph", "--pid", pid, "--output", self._output_path] 145 self._perf_process = self._host.executive.popen(cmd) 146 147 def _first_ten_lines_of_profile(self, perf_output): 148 match = re.search("^#[^\n]*\n((?: [^\n]*\n){1,10})", perf_output, re.MULTILINE) 149 return match.group(1) if match else None 150 151 def profile_after_exit(self): 152 # Perf doesn't automatically watch the attached pid for death notifications, 153 # so we have to do it for it, and then tell it its time to stop sampling. :( 154 self._host.executive.wait_limited(self._pid_being_profiled, limit_in_seconds=10) 155 perf_exitcode = self._perf_process.poll() 156 if perf_exitcode is None: # This should always be the case, unless perf error'd out early. 157 self._host.executive.interrupt(self._perf_process.pid) 158 159 perf_exitcode = self._perf_process.wait() 160 if perf_exitcode not in (0, -2): # The exit code should always be -2, as we're always interrupting perf. 161 print "'perf record' failed (exit code: %i), can't process results:" % perf_exitcode 162 return 163 164 perf_args = [self._perf_path(), 'report', '--call-graph', 'none', '--input', self._output_path] 165 print "First 10 lines of 'perf report --call-graph=none':" 166 167 print " ".join(perf_args) 168 perf_output = self._host.executive.run_command(perf_args) 169 print self._first_ten_lines_of_profile(perf_output) 170 171 print "To view the full profile, run:" 172 print ' '.join([self._perf_path(), 'report', '-i', self._output_path]) 173 print # An extra line between tests looks nicer. 174 175 176 class Sample(SingleFileOutputProfiler): 177 name = 'sample' 178 179 def __init__(self, host, executable_path, output_dir, identifier=None): 180 super(Sample, self).__init__(host, executable_path, output_dir, "txt", identifier) 181 self._profiler_process = None 182 183 def attach_to_pid(self, pid): 184 cmd = ["sample", pid, "-mayDie", "-file", self._output_path] 185 self._profiler_process = self._host.executive.popen(cmd) 186 187 def profile_after_exit(self): 188 self._profiler_process.wait() 189 190 191 class IProfiler(SingleFileOutputProfiler): 192 name = 'iprofiler' 193 194 def __init__(self, host, executable_path, output_dir, identifier=None): 195 super(IProfiler, self).__init__(host, executable_path, output_dir, "dtps", identifier) 196 self._profiler_process = None 197 198 def attach_to_pid(self, pid): 199 # FIXME: iprofiler requires us to pass the directory separately 200 # from the basename of the file, with no control over the extension. 201 fs = self._host.filesystem 202 cmd = ["iprofiler", "-timeprofiler", "-a", pid, 203 "-d", fs.dirname(self._output_path), "-o", fs.splitext(fs.basename(self._output_path))[0]] 204 # FIXME: Consider capturing instead of letting instruments spam to stderr directly. 205 self._profiler_process = self._host.executive.popen(cmd) 206 207 def profile_after_exit(self): 208 # It seems like a nicer user experiance to wait on the profiler to exit to prevent 209 # it from spewing to stderr at odd times. 210 self._profiler_process.wait() 211