Title: [136051] trunk/Tools
Revision
136051
Author
[email protected]
Date
2012-11-28 13:21:23 -0800 (Wed, 28 Nov 2012)

Log Message

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):

Modified Paths

Added Paths

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
_______________________________________________
webkit-changes mailing list
[email protected]
http://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to