summaryrefslogtreecommitdiffstats
path: root/testing/talos/talos/gecko_profile.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/talos/talos/gecko_profile.py')
-rw-r--r--testing/talos/talos/gecko_profile.py246
1 files changed, 246 insertions, 0 deletions
diff --git a/testing/talos/talos/gecko_profile.py b/testing/talos/talos/gecko_profile.py
new file mode 100644
index 0000000000..f7bbbaf597
--- /dev/null
+++ b/testing/talos/talos/gecko_profile.py
@@ -0,0 +1,246 @@
+# 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 Gecko profiling.
+"""
+import json
+import os
+import tempfile
+import zipfile
+
+import mozfile
+from mozgeckoprofiler import ProfileSymbolicator, save_gecko_profile
+from mozlog import get_proxy_logger
+
+LOG = get_proxy_logger()
+
+
+class GeckoProfile(object):
+ """
+ Handle Gecko profiling.
+
+ This allow to collect Gecko 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
+ self.cleanup = True
+
+ # 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.
+ gecko_profile_dir = tempfile.mkdtemp()
+
+ gecko_profile_interval = test_config.get("gecko_profile_interval", 1)
+ # Default number of entries is 128MiB.
+ # This value is calculated by dividing the 128MiB of memory by 8 because
+ # the profiler uses 8 bytes per entry.
+ gecko_profile_entries = test_config.get(
+ "gecko_profile_entries", int(128 * 1024 * 1024 / 8)
+ )
+ gecko_profile_features = test_config.get(
+ "gecko_profile_features", "js,stackwalk,cpu,screenshots"
+ )
+ gecko_profile_threads = test_config.get(
+ "gecko_profile_threads", "GeckoMain,Compositor,Renderer"
+ )
+
+ gecko_profile_extra_threads = test_config.get(
+ "gecko_profile_extra_threads", None
+ )
+ if gecko_profile_extra_threads:
+ gecko_profile_threads += "," + gecko_profile_extra_threads
+
+ # Make sure no archive already exists in the location where
+ # we plan to output our profiler archive
+ # If individual talos is ran (--activeTest) instead of suite (--suite)
+ # the "suite" key will be empty and we'll name the profile after
+ # the test name
+ self.profile_arcname = os.path.join(
+ self.upload_dir,
+ "profile_{0}.zip".format(test_config.get("suite", test_config["name"])),
+ )
+
+ # We delete the archive if the current test is the first in the suite
+ if test_config.get("is_first_test", False):
+ LOG.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(),
+ }
+
+ LOG.info(
+ "Activating Gecko Profiling. Temp. profile dir:"
+ " {0}, interval: {1}, entries: {2}".format(
+ gecko_profile_dir, gecko_profile_interval, gecko_profile_entries
+ )
+ )
+
+ self.profiling_info = {
+ "gecko_profile_interval": gecko_profile_interval,
+ "gecko_profile_entries": gecko_profile_entries,
+ "gecko_profile_dir": gecko_profile_dir,
+ "gecko_profile_features": gecko_profile_features,
+ "gecko_profile_threads": gecko_profile_threads,
+ }
+
+ def option(self, name):
+ return self.profiling_info["gecko_profile_" + name]
+
+ def update_env(self, env):
+ """
+ update the given env to update some env vars if required.
+ """
+ if not self.test_config.get("gecko_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",
+ # Temporary: Don't run Base Profiler, see bug 1630448.
+ # TODO: Remove when fix lands in bug 1648324 or bug 1648325.
+ "MOZ_PROFILER_STARTUP_NO_BASE": "1",
+ "MOZ_PROFILER_STARTUP_INTERVAL": str(self.option("interval")),
+ "MOZ_PROFILER_STARTUP_ENTRIES": str(self.option("entries")),
+ "MOZ_PROFILER_STARTUP_FEATURES": str(self.option("features")),
+ "MOZ_PROFILER_STARTUP_FILTERS": str(self.option("threads")),
+ }
+ )
+
+ def _save_gecko_profile(
+ self, cycle, symbolicator, missing_symbols_zip, profile_path
+ ):
+ try:
+ with open(profile_path, "r", encoding="utf-8") as profile_file:
+ profile = json.load(profile_file)
+ symbolicator.dump_and_integrate_missing_symbols(
+ profile, missing_symbols_zip
+ )
+ symbolicator.symbolicate_profile(profile)
+ save_gecko_profile(profile, profile_path)
+ except MemoryError:
+ LOG.critical(
+ "Ran out of memory while trying"
+ " to symbolicate profile {0} (cycle {1})".format(profile_path, cycle),
+ exc_info=True,
+ )
+ except Exception:
+ LOG.critical(
+ "Encountered an exception during profile"
+ " symbolication {0} (cycle {1})".format(profile_path, cycle),
+ exc_info=True,
+ )
+
+ def symbolicate(self, cycle):
+ """
+ Symbolicate Gecko profiling data for one cycle.
+
+ :param cycle: the number of the cycle of the test currently run.
+ """
+ symbolicator = ProfileSymbolicator(
+ {
+ # Trace-level logging (verbose)
+ "enableTracing": 0,
+ # Fallback server if symbol is not found locally
+ "remoteSymbolServer": "https://symbolication.services.mozilla.com/symbolicate/v4",
+ # 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"]
+ )
+ elif os.path.isfile(self.browser_config["symbols_path"]):
+ symbolicator.integrate_symbol_zip_from_file(
+ self.browser_config["symbols_path"]
+ )
+ elif os.path.isdir(self.browser_config["symbols_path"]):
+ sym_path = self.browser_config["symbols_path"]
+ symbolicator.options["symbolPaths"]["FIREFOX"] = sym_path
+ self.cleanup = False
+
+ missing_symbols_zip = os.path.join(self.upload_dir, "missingsymbols.zip")
+
+ try:
+ mode = zipfile.ZIP_DEFLATED
+ except NameError:
+ mode = zipfile.ZIP_STORED
+
+ gecko_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 gecko_profile_dir.
+ for profile_filename in os.listdir(gecko_profile_dir):
+ testname = profile_filename
+ if testname.endswith(".profile"):
+ testname = testname[0:-8]
+ profile_path = os.path.join(gecko_profile_dir, profile_filename)
+ self._save_gecko_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.profile files. For example, with
+ # test_config['name'] == 'tscrollx',
+ # profile_filename == 'iframe.svg.profile', i == 0,
+ # we'll get path_in_zip ==
+ # 'profile_tscrollx/iframe.svg/cycle_0.profile'.
+ cycle_name = "cycle_{0}.profile".format(cycle)
+ path_in_zip = os.path.join(
+ "profile_{0}".format(self.test_config["name"]), testname, cycle_name
+ )
+ LOG.info(
+ "Adding profile {0} to archive {1}".format(
+ path_in_zip, self.profile_arcname
+ )
+ )
+ try:
+ arc.write(profile_path, path_in_zip)
+ except Exception:
+ LOG.exception(
+ "Failed to copy profile {0} as {1} to"
+ " archive {2}".format(
+ profile_path, path_in_zip, self.profile_arcname
+ )
+ )
+ # save the latest gecko profile archive to an env var, so later on
+ # it can be viewed automatically via the view-gecko-profile tool
+ os.environ["TALOS_LATEST_GECKO_PROFILE_ARCHIVE"] = self.profile_arcname
+
+ def clean(self):
+ """
+ Clean up temp folders created with the instance creation.
+ """
+ mozfile.remove(self.option("dir"))
+ if self.cleanup:
+ for symbol_path in self.symbol_paths.values():
+ mozfile.remove(symbol_path)