Bug 1190376 - move the sps profilling stuff in another module.r =jmaher
authorJulien Pagès <j.parkouss@gmail.com>
Wed, 12 Aug 2015 10:50:42 +0200
changeset 991 e8854f96ade3
parent 990 6e62b920e8d8
child 992 7fe8298e0aa2
push id681
push userj.parkouss@gmail.com
push dateWed, 12 Aug 2015 14:10:50 +0000
bugs1190376
Bug 1190376 - move the sps profilling stuff in another module.r =jmaher
talos/ffsetup.py
talos/run_tests.py
talos/sps_profile.py
talos/ttest.py
--- a/talos/ffsetup.py
+++ b/talos/ffsetup.py
@@ -9,29 +9,32 @@ Set up a browser environment before runn
 import os
 import re
 import tempfile
 import logging
 import mozfile
 from mozprocess import ProcessHandler
 from mozprofile.profile import Profile
 
-from utils import TalosError
-import utils
+from talos import utils
+from talos.utils import TalosError
+from talos.sps_profile import SpsProfile
 
 
 class FFSetup(object):
     """
     Initialize the browser environment before running a test.
 
     This prepares:
      - the environment vars for running the test in the browser,
        available via the instance member *env*.
      - the profile used to run the test, available via the
        instance member *profile_dir*.
+     - sps profiling, available via the instance member *sps_profile*
+       of type :class:`SpsProfile` or None if not used.
 
     Note that the browser will be run once with the profile, to ensure
     this is basically working and negate any performance noise with the
     real test run (installing the profile the first time takes time).
 
     This class should be used as a context manager::
 
       with FFSetup(browser_config, test_config) as setup:
@@ -45,16 +48,17 @@ class FFSetup(object):
 
     def __init__(self, browser_config, test_config):
         self.browser_config, self.test_config = browser_config, test_config
         self._tmp_dir = tempfile.mkdtemp()
         self.env = None
         # The profile dir must be named 'profile' because of xperf analysis
         # (in etlparser.py). TODO fix that ?
         self.profile_dir = os.path.join(self._tmp_dir, 'profile')
+        self.sps_profile = None
 
     def _init_env(self):
         self.env = dict(os.environ)
         for k, v in self.browser_config['env'].iteritems():
             self.env[k] = str(v)
         self.env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
         # for winxp e10s logging:
         # https://bugzilla.mozilla.org/show_bug.cgi?id=1037445
@@ -116,23 +120,37 @@ class FFSetup(object):
         results_raw = '\n'.join(browser.output)
 
         if not self.PROFILE_REGEX.search(results_raw):
             logging.info("Could not find %s in browser output",
                          self.PROFILE_REGEX.pattern)
             logging.info("Raw results:%s", results_raw)
             raise TalosError("browser failed to close after being initialized")
 
+    def _init_sps_profile(self):
+        upload_dir = os.getenv('MOZ_UPLOAD_DIR')
+        if self.test_config.get('sps_profile') and not upload_dir:
+            logging.critical("Profiling ignored because MOZ_UPLOAD_DIR was not"
+                             " set")
+        if upload_dir and self.test_config.get('sps_profile'):
+            self.sps_profile = SpsProfile(upload_dir,
+                                          self.browser_config,
+                                          self.test_config)
+            self.sps_profile.update_env(self.env)
+
     def clean(self):
         mozfile.remove(self._tmp_dir)
+        if self.sps_profile:
+            self.sps_profile.clean()
 
     def __enter__(self):
         self._init_env()
         self._init_profile()
         try:
             self._run_profile()
         except:
             self.clean()
             raise
+        self._init_sps_profile()
         return self
 
     def __exit__(self, type, value, tb):
         self.clean()
--- a/talos/run_tests.py
+++ b/talos/run_tests.py
@@ -2,29 +2,28 @@
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import mozversion
 import mozfile
 import logging
-import filter
 import os
 import sys
 import time
 import traceback
 import urllib
 import urlparse
 import utils
 
-from results import TalosResults
-from ttest import TTest
-from utils import TalosError, TalosCrash, TalosRegression
-from config import get_configs, ConfigurationError
+from talos.results import TalosResults
+from talos.ttest import TTest
+from talos.utils import TalosError, TalosCrash, TalosRegression
+from talos.config import get_configs, ConfigurationError
 
 # directory of this file
 here = os.path.dirname(os.path.realpath(__file__))
 
 
 def useBaseTestDefaults(base, tests):
     for test in tests:
         for item in base:
new file mode 100644
--- /dev/null
+++ b/talos/sps_profile.py
@@ -0,0 +1,205 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+module to handle sps profilling.
+"""
+
+import os
+import tempfile
+import logging
+import zipfile
+import json
+
+import mozfile
+
+from talos.profiler import symbolication, sps
+
+
+class SpsProfile(object):
+    """
+    Handle sps profilling.
+
+    This allow to collect sps profiling data and to zip results in one file.
+    """
+    def __init__(self, upload_dir, browser_config, test_config):
+        self.upload_dir = upload_dir
+        self.browser_config, self.test_config = browser_config, test_config
+
+        # Create a temporary directory into which the tests can put
+        # their profiles. These files will be assembled into one big
+        # zip file later on, which is put into the MOZ_UPLOAD_DIR.
+        sps_profile_dir = tempfile.mkdtemp()
+
+        sps_profile_interval = test_config.get('sps_profile_interval', 1)
+        sps_profile_entries = test_config.get('sps_profile_entries', 1000000)
+        sps_profile_threads = 'GeckoMain,Compositor'
+
+        # Make sure no archive already exists in the location where
+        # we plan to output our profiler archive
+        self.profile_arcname = os.path.join(
+            self.upload_dir,
+            "profile_{0}.sps.zip".format(test_config['name'])
+        )
+        logging.info("Clearing archive {0}".format(self.profile_arcname))
+        mozfile.remove(self.profile_arcname)
+
+        self.symbol_paths = {
+            'FIREFOX': tempfile.mkdtemp(),
+            'THUNDERBIRD': tempfile.mkdtemp(),
+            'WINDOWS': tempfile.mkdtemp()
+        }
+
+        logging.info("Activating Gecko Profiling. Temp. profile dir:"
+                     " {0}, interval: {1}, entries: {2}"
+                     .format(sps_profile_dir,
+                             sps_profile_interval,
+                             sps_profile_entries))
+
+        self.profiling_info = {
+            "sps_profile_interval": sps_profile_interval,
+            "sps_profile_entries": sps_profile_entries,
+            "sps_profile_dir": sps_profile_dir,
+            "sps_profile_threads": sps_profile_threads
+        }
+
+    def option(self, name):
+        return self.profiling_info["sps_profile_" + name]
+
+    def update_env(self, env):
+        """
+        update the given env to update some env vars if required.
+        """
+        if not self.test_config.get('sps_profile_startup'):
+            return
+        # Set environment variables which will cause profiling to
+        # start as early as possible. These are consumed by Gecko
+        # itself, not by Talos JS code.
+        env.update({
+            'MOZ_PROFILER_STARTUP': '1',
+            'MOZ_PROFILER_INTERVAL': str(self.option('interval')),
+            'MOZ_PROFILER_ENTRIES': str(self.option('entries')),
+            "MOZ_PROFILER_THREADS": str(self.option('threads'))
+        })
+
+    def _save_sps_profile(self, cycle, symbolicator, missing_symbols_zip,
+                          profile_path):
+        try:
+            with open(profile_path, 'r') as profile_file:
+                profile = json.load(profile_file)
+            symbolicator.dump_and_integrate_missing_symbols(
+                profile,
+                missing_symbols_zip)
+            symbolicator.symbolicate_profile(profile)
+            sps.save_profile(profile, profile_path)
+        except MemoryError:
+            logging.exception(
+                "Ran out of memory while trying"
+                " to symbolicate profile {0} (cycle {1})"
+                .format(profile_path, cycle)
+            )
+        except Exception:
+            logging.exception("Encountered an exception during profile"
+                              " symbolication {0} (cycle {1})"
+                              .format(profile_path, cycle))
+
+    def symbolicate(self, cycle):
+        """
+        Symbolicate sps profiling data for one cycle.
+
+        :param cycle: the number of the cycle of the test currently run.
+        """
+        symbolicator = symbolication.ProfileSymbolicator({
+            # Trace-level logging (verbose)
+            "enableTracing": 0,
+            # Fallback server if symbol is not found locally
+            "remoteSymbolServer":
+                "http://symbolapi.mozilla.org:80/talos/",
+            # Maximum number of symbol files to keep in memory
+            "maxCacheEntries": 2000000,
+            # Frequency of checking for recent symbols to
+            # cache (in hours)
+            "prefetchInterval": 12,
+            # Oldest file age to prefetch (in hours)
+            "prefetchThreshold": 48,
+            # Maximum number of library versions to pre-fetch
+            # per library
+            "prefetchMaxSymbolsPerLib": 3,
+            # Default symbol lookup directories
+            "defaultApp": "FIREFOX",
+            "defaultOs": "WINDOWS",
+            # Paths to .SYM files, expressed internally as a
+            # mapping of app or platform names to directories
+            # Note: App & OS names from requests are converted
+            # to all-uppercase internally
+            "symbolPaths": self.symbol_paths
+        })
+
+        if self.browser_config['symbols_path']:
+            if mozfile.is_url(self.browser_config['symbols_path']):
+                symbolicator.integrate_symbol_zip_from_url(
+                    self.browser_config['symbols_path']
+                )
+            else:
+                symbolicator.integrate_symbol_zip_from_file(
+                    self.browser_config['symbols_path']
+                )
+
+        missing_symbols_zip = os.path.join(self.upload_dir,
+                                           "missingsymbols.zip")
+
+        try:
+            mode = zipfile.ZIP_DEFLATED
+        except NameError:
+            mode = zipfile.ZIP_STORED
+
+        sps_profile_dir = self.option('dir')
+
+        with zipfile.ZipFile(self.profile_arcname, 'a', mode) as arc:
+            # Collect all individual profiles that the test
+            # has put into sps_profile_dir.
+            for profile_filename in os.listdir(sps_profile_dir):
+                testname = profile_filename
+                if testname.endswith(".sps"):
+                    testname = testname[0:-4]
+                profile_path = os.path.join(sps_profile_dir, profile_filename)
+                self._save_sps_profile(cycle, symbolicator,
+                                       missing_symbols_zip,
+                                       profile_path)
+
+                # Our zip will contain one directory per subtest,
+                # and each subtest directory will contain one or
+                # more cycle_i.sps files. For example, with
+                # test_config['name'] == 'tscrollx',
+                # profile_filename == 'iframe.svg.sps', i == 0,
+                # we'll get path_in_zip ==
+                # 'profile_tscrollx/iframe.svg/cycle_0.sps'.
+                cycle_name = "cycle_{0}.sps".format(cycle)
+                path_in_zip = \
+                    os.path.join(
+                        "profile_{0}".format(self.test_config['name']),
+                        testname,
+                        cycle_name
+                    )
+                logging.info(
+                    "Adding profile {0} to archive {1}"
+                    .format(path_in_zip, self.profile_arcname)
+                )
+                try:
+                    arc.write(profile_path, path_in_zip)
+                except Exception:
+                    logging.exception(
+                        "Failed to copy profile {0} as {1} to"
+                        " archive {2}".format(profile_path,
+                                              path_in_zip,
+                                              self.profile_arcname)
+                    )
+
+    def clean(self):
+        """
+        Clean up temp folders created with the instance creation.
+        """
+        mozfile.remove(self.option('dir'))
+        for symbol_path in self.symbol_paths.values():
+            mozfile.remove(symbol_path)
--- a/talos/ttest.py
+++ b/talos/ttest.py
@@ -12,34 +12,28 @@
      - waits for a 'dump' from the browser
 """
 
 import os
 import platform
 import results
 import traceback
 import subprocess
-import tempfile
 import time
 import utils
 import mozcrash
 import talosconfig
 import shutil
-import zipfile
-import json
-from profiler import symbolication
-from profiler import sps
 import mozfile
 import logging
 from threading import Thread
 
-from utils import TalosError, TalosCrash, TalosRegression
-import ffprocess
-from ffsetup import FFSetup
-import TalosProcess
+from talos.utils import TalosError, TalosCrash, TalosRegression
+from talos import ffprocess, TalosProcess
+from talos.ffsetup import FFSetup
 
 
 class TTest(object):
 
     _pids = []
     platform_type = ''
 
     def __init__(self):
@@ -173,79 +167,16 @@ class TTest(object):
                 setup.env['MOZ_MAIN_THREAD_IO_LOG'] = mainthread_io
 
             test_config['url'] = utils.interpolate(
                 test_config['url'],
                 profile=setup.profile_dir,
                 firefox=browser_config['browser_path']
             )
 
-            upload_dir = os.environ.get('MOZ_UPLOAD_DIR', None)
-            sps_profile = upload_dir and test_config.get('sps_profile', False)
-
-            if test_config.get('sps_profile', False) and not upload_dir:
-                print "Profiling ignored because MOZ_UPLOAD_DIR was not set"
-
-            sps_profile_dir = None
-            profile_arcname = None
-            profiling_info = None
-
-            symbol_paths = {}
-
-            if sps_profile:
-                # Create a temporary directory into which the tests can put
-                # their profiles. These files will be assembled into one big
-                # zip file later on, which is put into the MOZ_UPLOAD_DIR.
-                sps_profile_dir = tempfile.mkdtemp()
-
-                sps_profile_interval = test_config.get('sps_profile_interval',
-                                                       1)
-                sps_profile_entries = test_config.get('sps_profile_entries',
-                                                      1000000)
-                sps_profile_threads = 'GeckoMain,Compositor'
-
-                # Make sure no archive already exists in the location where
-                # we plan to output our profiler archive
-                profile_arcname = os.path.join(
-                    upload_dir,
-                    "profile_{0}.sps.zip".format(test_config['name'])
-                )
-                logging.info("Clearing archive {0}".format(profile_arcname))
-                mozfile.remove(profile_arcname)
-
-                symbol_paths = {
-                    'FIREFOX': tempfile.mkdtemp(),
-                    'THUNDERBIRD': tempfile.mkdtemp(),
-                    'WINDOWS': tempfile.mkdtemp()
-                }
-
-                logging.info("Activating Gecko Profiling. Temp. profile dir:"
-                             " {0}, interval: {1}, entries: {2}"
-                             .format(sps_profile_dir,
-                                     sps_profile_interval,
-                                     sps_profile_entries))
-
-                profiling_info = {
-                    "sps_profile_interval": sps_profile_interval,
-                    "sps_profile_entries": sps_profile_entries,
-                    "sps_profile_dir": sps_profile_dir,
-                    "sps_profile_threads": sps_profile_threads
-                }
-
-                if test_config.get('sps_profile_startup', False):
-                    # Set environment variables which will cause profiling to
-                    # start as early as possible. These are consumed by Gecko
-                    # itself, not by Talos JS code.
-                    setup.env.update({
-                        'MOZ_PROFILER_STARTUP': '1',
-                        'MOZ_PROFILER_INTERVAL': str(sps_profile_interval),
-                        'MOZ_PROFILER_ENTRIES': str(sps_profile_entries),
-                        "MOZ_PROFILER_THREADS": str(sps_profile_threads)
-                    })
-
             logging.debug("initialized %s", browser_config['process'])
 
             # setup global (cross-cycle) counters:
             # shutdown, responsiveness
             global_counters = {}
             if browser_config.get('xperf_path'):
                 for c in test_config.get('xperf_counters', []):
                     global_counters[c] = []
@@ -285,27 +216,28 @@ class TTest(object):
                         shutil.copy(origin, dest)
 
                 # check to see if the previous cycle is still hanging around
                 if (i > 0) and ffprocess.running_processes(self._pids):
                     raise TalosError("previous cycle still running")
 
                 # Run the test
                 timeout = test_config.get('timeout', 7200)  # 2 hours default
-                if sps_profile:
+                if setup.sps_profile:
                     # When profiling, give the browser some extra time
                     # to dump the profile.
                     timeout += 5 * 60
 
                 command_args = utils.GenerateBrowserCommandLine(
                     browser_config["browser_path"],
                     browser_config["extra_args"],
                     setup.profile_dir,
                     test_config['url'],
-                    profiling_info=profiling_info
+                    profiling_info=(setup.sps_profile.profiling_info
+                                    if setup.sps_profile else None)
                 )
 
                 self.counter_results = None
                 mainthread_error_count = 0
                 if test_config['setup']:
                     # Generate bcontroller.json for xperf
                     talosconfig.generateTalosConfig(command_args,
                                                     browser_config,
@@ -425,137 +357,23 @@ class TTest(object):
                     test_results.add(browser_log_filename,
                                      counter_results=self.counter_results)
                 except Exception as e:
                     # Log the exception, but continue. One way to get here
                     # is if the browser hangs, and we'd still like to get
                     # symbolicated profiles in that case.
                     logging.info(e)
 
-                if sps_profile:
-                    symbolicator = symbolication.ProfileSymbolicator({
-                        # Trace-level logging (verbose)
-                        "enableTracing": 0,
-                        # Fallback server if symbol is not found locally
-                        "remoteSymbolServer":
-                            "http://symbolapi.mozilla.org:80/talos/",
-                        # Maximum number of symbol files to keep in memory
-                        "maxCacheEntries": 2000000,
-                        # Frequency of checking for recent symbols to
-                        # cache (in hours)
-                        "prefetchInterval": 12,
-                        # Oldest file age to prefetch (in hours)
-                        "prefetchThreshold": 48,
-                        # Maximum number of library versions to pre-fetch
-                        # per library
-                        "prefetchMaxSymbolsPerLib": 3,
-                        # Default symbol lookup directories
-                        "defaultApp": "FIREFOX",
-                        "defaultOs": "WINDOWS",
-                        # Paths to .SYM files, expressed internally as a
-                        # mapping of app or platform names to directories
-                        # Note: App & OS names from requests are converted
-                        # to all-uppercase internally
-                        "symbolPaths": symbol_paths
-                    })
-
-                    if browser_config['symbols_path']:
-                        if mozfile.is_url(browser_config['symbols_path']):
-                            symbolicator\
-                                .integrate_symbol_zip_from_url(
-                                    browser_config['symbols_path']
-                                )
-                        else:
-                            symbolicator\
-                                .integrate_symbol_zip_from_file(
-                                    browser_config['symbols_path']
-                                )
-
-                    missing_symbols_zip = os.path.join(upload_dir,
-                                                       "missingsymbols.zip")
-
-                    try:
-                        mode = zipfile.ZIP_DEFLATED
-                    except NameError:
-                        mode = zipfile.ZIP_STORED
-                    with zipfile.ZipFile(profile_arcname, 'a', mode) as arc:
-                        # Collect all individual profiles that the test
-                        # has put into sps_profile_dir.
-                        for profile_filename in os.listdir(sps_profile_dir):
-                            testname = profile_filename
-                            if testname.endswith(".sps"):
-                                testname = testname[0:-4]
-                            profile_path = os.path.join(sps_profile_dir,
-                                                        profile_filename)
-                            try:
-                                with open(profile_path, 'r') as profile_file:
-                                    profile = json.load(profile_file)
-                                symbolicator\
-                                    .dump_and_integrate_missing_symbols(
-                                        profile,
-                                        missing_symbols_zip
-                                    )
-                                symbolicator.symbolicate_profile(profile)
-                                sps.save_profile(profile, profile_path)
-                                profile = None  # Free up memory
-                            except MemoryError as e:
-                                logging.info(
-                                    "Ran out of memory while trying"
-                                    " to symbolicate profile {0} (cycle {1})"
-                                    .format(profile_path, i)
-                                )
-                            except Exception as e:
-                                logging.info(e)
-                                logging.info(
-                                    "Encountered an exception during profile"
-                                    " symbolication {0} (cycle {1})"
-                                    .format(profile_path, i)
-                                )
-                            profile = None  # Free up memory
-
-                            # Our zip will contain one directory per subtest,
-                            # and each subtest directory will contain one or
-                            # more cycle_i.sps files. For example, with
-                            # test_config['name'] == 'tscrollx',
-                            # profile_filename == 'iframe.svg.sps', i == 0,
-                            # we'll get path_in_zip ==
-                            # 'profile_tscrollx/iframe.svg/cycle_0.sps'.
-                            cycle_name = "cycle_{0}.sps".format(i)
-                            path_in_zip = \
-                                os.path.join(
-                                    "profile_{0}".format(test_config['name']),
-                                    testname,
-                                    cycle_name
-                                )
-                            logging.info(
-                                "Adding profile {0} to archive {1}"
-                                .format(path_in_zip, profile_arcname)
-                            )
-                            try:
-                                arc.write(profile_path, path_in_zip)
-                            except Exception as e:
-                                logging.info(e)
-                                logging.info(
-                                    "Failed to copy profile {0} as {1} to"
-                                    " archive {2}".format(profile_path,
-                                                          path_in_zip,
-                                                          profile_arcname)
-                                )
+                if setup.sps_profile:
+                    setup.sps_profile.symbolicate(i)
 
                 # clean up any stray browser processes
                 self.cleanupAndCheckForCrashes(browser_config,
                                                setup.profile_dir,
                                                test_config['name'])
-                # clean up the bcontroller process
-
-            # cleanup
-            if sps_profile:
-                mozfile.remove(sps_profile_dir)
-                for symbol_path in symbol_paths.values():
-                    mozfile.remove(symbol_path)
 
             # include global (cross-cycle) counters
             test_results.all_counter_results.extend(
                 [{key: value} for key, value in global_counters.items()]
             )
             for c in test_results.all_counter_results:
                 for key, value in c.items():
                     print "COUNTER: %s" % key