# 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,memory"
        )
        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)
