Diff
Modified: trunk/Tools/ChangeLog (136050 => 136051)
--- trunk/Tools/ChangeLog 2012-11-28 21:18:35 UTC (rev 136050)
+++ trunk/Tools/ChangeLog 2012-11-28 21:21:23 UTC (rev 136051)
@@ -1,3 +1,54 @@
+2012-11-28 Eric Seidel <[email protected]>
+
+ run-perf-tests should have a --profile option for easy profiling
+ https://bugs.webkit.org/show_bug.cgi?id=99517
+
+ Reviewed by Adam Barth.
+
+ This is a very basic implementation which works on Mac and Linux
+ and makes it trivial for anyone to profile a PerformanceTest.
+
+ Currently it's not "hard" to profile a PerformanceTest
+ but lowering the barriers to entry here allows all of us to focus
+ less on the tools and more on the code.
+
+ This also paves the way for adding easy mobile-profiling (e.g. chromium-android)
+ which is actually "hard", and this option will make much easier.
+
+ * Scripts/webkitpy/common/system/profiler.py: Added.
+ (ProfilerFactory):
+ (ProfilerFactory.create_profiler):
+ (Profiler):
+ (Profiler.__init__):
+ (Profiler.adjusted_environment):
+ (Profiler.attach_to_pid):
+ (Profiler.did_stop):
+ (SingleFileOutputProfiler):
+ (SingleFileOutputProfiler.__init__):
+ (GooglePProf):
+ (GooglePProf.__init__):
+ (GooglePProf.adjusted_environment):
+ (GooglePProf.did_stop):
+ (Instruments):
+ (Instruments.__init__):
+ (Instruments.attach_to_pid):
+ * Scripts/webkitpy/common/system/profiler_unittest.py: Added.
+ (ProfilerFactoryTest):
+ (ProfilerFactoryTest.test_basic):
+ * Scripts/webkitpy/layout_tests/port/driver.py:
+ (Driver.__init__):
+ (Driver._start):
+ (Driver.stop):
+ (Driver.cmd_line):
+ * Scripts/webkitpy/performance_tests/perftest.py:
+ (PerfTest.parse_output):
+ * Scripts/webkitpy/performance_tests/perftest_unittest.py:
+ (MainTest.test_parse_output):
+ (MainTest.test_parse_output_with_failing_line):
+ * Scripts/webkitpy/performance_tests/perftestsrunner.py:
+ (PerfTestsRunner._parse_args):
+ (PerfTestsRunner.run):
+
2012-11-28 Zan Dobersek <[email protected]>
Remove deprecated logging usage from QueueEngine
Added: trunk/Tools/Scripts/webkitpy/common/system/profiler.py (0 => 136051)
--- trunk/Tools/Scripts/webkitpy/common/system/profiler.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/common/system/profiler.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -0,0 +1,99 @@
+# Copyright (C) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import re
+
+_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.workspace, host.executive, executable_path, output_dir, identifier)
+ return GooglePProf(host.workspace, host.executive, executable_path, output_dir, identifier)
+
+
+class Profiler(object):
+ def __init__(self, workspace, executive, executable_path, output_dir, identifier=None):
+ self._workspace = workspace
+ self._executive = executive
+ self._executable_path = executable_path
+ self._output_dir = output_dir
+ self._identifier = "test"
+
+ def adjusted_environment(self, env):
+ return env
+
+ def attach_to_pid(self, pid):
+ pass
+
+ def profile_after_exit(self):
+ pass
+
+
+class SingleFileOutputProfiler(Profiler):
+ def __init__(self, workspace, executive, executable_path, output_dir, output_suffix, identifier=None):
+ super(SingleFileOutputProfiler, self).__init__(workspace, executive, executable_path, output_dir, identifier)
+ self._output_path = self._workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix)
+
+
+class GooglePProf(SingleFileOutputProfiler):
+ def __init__(self, workspace, executive, executable_path, output_dir, identifier=None):
+ super(GooglePProf, self).__init__(workspace, executive, executable_path, output_dir, "pprof", identifier)
+
+ def adjusted_environment(self, env):
+ env['CPUPROFILE'] = self._output_path
+ return env
+
+ def _first_ten_lines_of_profile(self, pprof_output):
+ 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):
+ # 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]
+ profile_text = self._executive.run_command(pprof_args)
+ print self._first_ten_lines_of_profile(profile_text)
+
+
+# FIXME: iprofile is a newer commandline interface to replace /usr/bin/instruments.
+class Instruments(SingleFileOutputProfiler):
+ def __init__(self, workspace, executive, executable_path, output_dir, identifier=None):
+ super(Instruments, self).__init__(workspace, executive, executable_path, output_dir, "trace", identifier)
+
+ # 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 attach_to_pid(self, pid):
+ cmd = ["instruments", "-t", self._time_profile, "-D", self._output_path, "-p", pid]
+ cmd = map(unicode, cmd)
+ self._executive.popen(cmd)
Added: trunk/Tools/Scripts/webkitpy/common/system/profiler_unittest.py (0 => 136051)
--- trunk/Tools/Scripts/webkitpy/common/system/profiler_unittest.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/common/system/profiler_unittest.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -0,0 +1,85 @@
+# Copyright (C) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+from .profiler import ProfilerFactory, Instruments, GooglePProf
+
+
+class ProfilerFactoryTest(unittest.TestCase):
+ def test_basic(self):
+ host = MockSystemHost()
+ profiler = ProfilerFactory.create_profiler(host, '/bin/executable', '/tmp/output')
+ self.assertEquals(profiler._output_path, "/tmp/output/test.trace")
+
+ host.platform.os_name = 'linux'
+ profiler = ProfilerFactory.create_profiler(host, '/bin/executable', '/tmp/output')
+ self.assertEquals(profiler._output_path, "/tmp/output/test.pprof")
+
+ def test_pprof_output_regexp(self):
+ pprof_output = """
+sometimes
+there
+is
+junk before the total line
+
+
+Total: 3770 samples
+ 76 2.0% 2.0% 104 2.8% lookup (inline)
+ 60 1.6% 3.6% 60 1.6% FL_SetPrevious (inline)
+ 56 1.5% 5.1% 56 1.5% MaskPtr (inline)
+ 51 1.4% 6.4% 222 5.9% WebCore::HTMLTokenizer::nextToken
+ 42 1.1% 7.6% 47 1.2% WTF::Vector::shrinkCapacity
+ 35 0.9% 8.5% 35 0.9% WTF::RefPtr::get (inline)
+ 33 0.9% 9.4% 43 1.1% append (inline)
+ 29 0.8% 10.1% 67 1.8% WTF::StringImpl::deref (inline)
+ 29 0.8% 10.9% 100 2.7% add (inline)
+ 28 0.7% 11.6% 28 0.7% WebCore::QualifiedName::localName (inline)
+ 25 0.7% 12.3% 27 0.7% WebCore::Private::addChildNodesToDeletionQueue
+ 24 0.6% 12.9% 24 0.6% __memcpy_ssse3_back
+ 23 0.6% 13.6% 23 0.6% intHash (inline)
+ 23 0.6% 14.2% 76 2.0% tcmalloc::FL_Next
+ 23 0.6% 14.8% 95 2.5% tcmalloc::FL_Push
+ 22 0.6% 15.4% 22 0.6% WebCore::MarkupTokenizerBase::InputStreamPreprocessor::peek (inline)
+"""
+ expected_first_ten_lines = """ 76 2.0% 2.0% 104 2.8% lookup (inline)
+ 60 1.6% 3.6% 60 1.6% FL_SetPrevious (inline)
+ 56 1.5% 5.1% 56 1.5% MaskPtr (inline)
+ 51 1.4% 6.4% 222 5.9% WebCore::HTMLTokenizer::nextToken
+ 42 1.1% 7.6% 47 1.2% WTF::Vector::shrinkCapacity
+ 35 0.9% 8.5% 35 0.9% WTF::RefPtr::get (inline)
+ 33 0.9% 9.4% 43 1.1% append (inline)
+ 29 0.8% 10.1% 67 1.8% WTF::StringImpl::deref (inline)
+ 29 0.8% 10.9% 100 2.7% add (inline)
+ 28 0.7% 11.6% 28 0.7% WebCore::QualifiedName::localName (inline)
+"""
+ host = MockSystemHost()
+ profiler = GooglePProf(host.workspace, host.executive, '/bin/executable', '/tmp/output')
+ self.assertEquals(profiler._first_ten_lines_of_profile(pprof_output), expected_first_ten_lines)
Modified: trunk/Tools/Scripts/webkitpy/layout_tests/port/driver.py (136050 => 136051)
--- trunk/Tools/Scripts/webkitpy/layout_tests/port/driver.py 2012-11-28 21:18:35 UTC (rev 136050)
+++ trunk/Tools/Scripts/webkitpy/layout_tests/port/driver.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -36,6 +36,7 @@
import os
from webkitpy.common.system import path
+from webkitpy.common.system.profiler import ProfilerFactory
_log = logging.getLogger(__name__)
@@ -140,6 +141,10 @@
self._server_process = None
self._measurements = {}
+ if self._port.get_option("profile"):
+ self._profiler = ProfilerFactory.create_profiler(self._port.host, self._port._path_to_driver(), self._port.results_directory())
+ else:
+ self._profiler = None
def __del__(self):
self.stop()
@@ -282,15 +287,21 @@
environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
if 'WEBKITOUTPUTDIR' in os.environ:
environment['WEBKITOUTPUTDIR'] = os.environ['WEBKITOUTPUTDIR']
+ if self._profiler:
+ environment = self._profiler.adjusted_environment(environment)
self._crashed_process_name = None
self._crashed_pid = None
self._server_process = self._port._server_process_constructor(self._port, server_name, self.cmd_line(pixel_tests, per_test_args), environment)
self._server_process.start()
+ if self._profiler:
+ self._profiler.attach_to_pid(self._server_process.pid())
def stop(self):
if self._server_process:
self._server_process.stop(self._port.driver_stop_timeout())
self._server_process = None
+ if self._profiler:
+ self._profiler.profile_after_exit()
if self._driver_tempdir:
self._port._filesystem.rmtree(str(self._driver_tempdir))
Modified: trunk/Tools/Scripts/webkitpy/performance_tests/perftest.py (136050 => 136051)
--- trunk/Tools/Scripts/webkitpy/performance_tests/perftest.py 2012-11-28 21:18:35 UTC (rev 136050)
+++ trunk/Tools/Scripts/webkitpy/performance_tests/perftest.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -183,11 +183,12 @@
_log.error("The test didn't report all statistics.")
return None
- for result_name in ordered_results_keys:
- if result_name == test_name:
- self.output_statistics(result_name, results[result_name], description_string)
- else:
- self.output_statistics(result_name, results[result_name])
+ if not self._port.get_option('profile'):
+ for result_name in ordered_results_keys:
+ if result_name == test_name:
+ self.output_statistics(result_name, results[result_name], description_string)
+ else:
+ self.output_statistics(result_name, results[result_name])
return results
def output_statistics(self, test_name, results, description_string=None):
Modified: trunk/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py (136050 => 136051)
--- trunk/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py 2012-11-28 21:18:35 UTC (rev 136050)
+++ trunk/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -63,7 +63,7 @@
output_capture = OutputCapture()
output_capture.capture_output()
try:
- test = PerfTest(None, 'some-test', '/path/some-dir/some-test')
+ test = PerfTest(MockPort(), 'some-test', '/path/some-dir/some-test')
self.assertEqual(test.parse_output(output),
{'some-test': {'avg': 1100.0, 'median': 1101.0, 'min': 1080.0, 'max': 1120.0, 'stdev': 11.0, 'unit': 'ms',
'values': [i for i in range(1, 20)]}})
@@ -91,7 +91,7 @@
output_capture = OutputCapture()
output_capture.capture_output()
try:
- test = PerfTest(None, 'some-test', '/path/some-dir/some-test')
+ test = PerfTest(MockPort(), 'some-test', '/path/some-dir/some-test')
self.assertEqual(test.parse_output(output), None)
finally:
actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
Modified: trunk/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py (136050 => 136051)
--- trunk/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py 2012-11-28 21:18:35 UTC (rev 136050)
+++ trunk/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py 2012-11-28 21:21:23 UTC (rev 136051)
@@ -121,6 +121,8 @@
help="Run replay tests."),
optparse.make_option("--force", dest="skipped", action="" default=False,
help="Run all tests, including the ones in the Skipped list."),
+ optparse.make_option("--profile", action=""
+ help="Output per-test profile information."),
]
return optparse.OptionParser(option_list=(perf_option_list)).parse_args(args)
@@ -187,7 +189,7 @@
finally:
self._stop_servers()
- if self._options.generate_results:
+ if self._options.generate_results and not self._options.profile:
exit_code = self._generate_and_show_results()
if exit_code:
return exit_code