tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e2e6e5a53d80ac144f4b5e64c5c5a369a941dcce
parent b2acc8ba3cd02c89d9706eafe804d4841aa3c585
Author: Serban Stanca <sstanca@mozilla.com>
Date:   Wed, 10 Dec 2025 01:56:49 +0200

Revert "Bug 1983904: Geckoprofile can now be captured in CI r=sparky,mozperftest-reviewers,mstange,perftest-reviewers" for causing python-mozperftest failures.

This reverts commit b713574b28a308c41b9cf9607bafa25081bf95e4.

Diffstat:
Mpython/mozperftest/mozperftest/profiler.py | 4++--
Mpython/mozperftest/mozperftest/system/__init__.py | 3---
Dpython/mozperftest/mozperftest/system/geckoprofiler.py | 259-------------------------------------------------------------------------------
Mpython/mozperftest/mozperftest/system/simpleperf.py | 86++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Dpython/mozperftest/mozperftest/tests/test_geckoprofiler.py | 485-------------------------------------------------------------------------------
Mpython/mozperftest/mozperftest/tests/test_simpleperf.py | 28++++++++++++----------------
Mpython/mozperftest/mozperftest/utils.py | 64----------------------------------------------------------------
Mtaskcluster/gecko_taskgraph/target_tasks.py | 2+-
Mtaskcluster/gecko_taskgraph/transforms/perftest.py | 4++--
Mtools/tryselect/selectors/perfselector/classification.py | 2+-
10 files changed, 66 insertions(+), 871 deletions(-)

diff --git a/python/mozperftest/mozperftest/profiler.py b/python/mozperftest/mozperftest/profiler.py @@ -2,10 +2,9 @@ # 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/. -from mozperftest.system.geckoprofiler import GeckoProfiler from mozperftest.system.simpleperf import SimpleperfProfiler -PROFILERS = [SimpleperfProfiler, GeckoProfiler] +PROFILERS = {SimpleperfProfiler} class ProfilingMediator: @@ -13,6 +12,7 @@ class ProfilingMediator: def __init__(self): self.active_profilers = [] + for profiler in PROFILERS: if profiler.is_enabled(): self.active_profilers.append(profiler.get_controller()) diff --git a/python/mozperftest/mozperftest/system/__init__.py b/python/mozperftest/mozperftest/system/__init__.py @@ -4,7 +4,6 @@ from mozperftest.layers import Layers from mozperftest.system.android import AndroidDevice from mozperftest.system.binarysetup import BinarySetup -from mozperftest.system.geckoprofiler import GeckoProfiler from mozperftest.system.macos import MacosDevice from mozperftest.system.pingserver import PingServer from mozperftest.system.profile import Profile @@ -21,7 +20,6 @@ def get_layers(): AndroidDevice, MacosDevice, SimpleperfProfiler, - GeckoProfiler, ) @@ -88,7 +86,6 @@ def pick_system(env, flavor, mach_cmd): MacosDevice, VersionProducer, SimpleperfProfiler, - GeckoProfiler, ] return Layers(env, mach_cmd, layers) if flavor == "alert": diff --git a/python/mozperftest/mozperftest/system/geckoprofiler.py b/python/mozperftest/mozperftest/system/geckoprofiler.py @@ -1,259 +0,0 @@ -# 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 gzip -import os -import shutil -import subprocess -import tempfile -from pathlib import Path - -from mozdevice import ADBDevice - -from mozperftest.layers import Layer -from mozperftest.utils import archive_files, extract_tgz_and_find_files - - -class GeckoProfilerError(Exception): - """Base class for Gecko profiler-related exceptions.""" - - pass - - -class GeckoProfilerAlreadyActiveError(GeckoProfilerError): - """Raised when attempting to start profiling while it's already active.""" - - pass - - -class GeckoProfilerNotActiveError(GeckoProfilerError): - """Raised when attempting to stop profiling when it's not active.""" - - pass - - -GECKOVIEW_CONFIG_PATH_PREFIX = "/data/local/tmp" - -DEFAULT_GECKOPROFILER_OPTS = { - "interval": 5, - "features": "js,stackwalk,screenshots,ipcmessages,java,cpu,memory", - "filters": "GeckoMain,Compositor,Renderer,IPDL Background,socket", -} - - -class GeckoProfilerController: - """Controller that starts and stops profiling on device.""" - - _package_id = None - - @classmethod - def set_package_id(cls, package_id: str): - cls._package_id = package_id - - def __init__(self): - self.device = ADBDevice() - self.profiling_active = False - self.package_id = None - self.config_filename = None - - def _resolve_package_id(self): - pkg = self._package_id or os.environ.get("BROWSER_BINARY") - if not pkg: - raise GeckoProfilerError("Package id not set for GeckoProfiler") - return pkg - - def start(self, geckoprofiler_opts=None): - """Create a temporary geckoview-config.yaml on device and enable startup profiling.""" - if geckoprofiler_opts is None: - geckoprofiler_opts = DEFAULT_GECKOPROFILER_OPTS - else: - opts = DEFAULT_GECKOPROFILER_OPTS.copy() - opts.update(geckoprofiler_opts) - geckoprofiler_opts = opts - - assert GeckoProfiler.is_enabled() - if self.profiling_active: - raise GeckoProfilerAlreadyActiveError("Gecko profiler already active") - - self.package_id = self._resolve_package_id() - self.config_filename = f"{self.package_id}-geckoview-config.yaml" - - config_content = f"""env: - MOZ_PROFILER_STARTUP: 1 - MOZ_PROFILER_STARTUP_INTERVAL: {geckoprofiler_opts['interval']} - MOZ_PROFILER_STARTUP_FEATURES: {geckoprofiler_opts['features']} - MOZ_PROFILER_STARTUP_FILTERS: {geckoprofiler_opts['filters']} -""".encode() - - with tempfile.NamedTemporaryFile(delete=False) as config_file: - config_file.write(config_content) - config_path = config_file.name - - device_config_path = f"{GECKOVIEW_CONFIG_PATH_PREFIX}/{self.config_filename}" - - subprocess.run( - ["adb", "push", config_path, device_config_path], - capture_output=True, - text=True, - check=True, - ) - - subprocess.run( - [ - "adb", - "shell", - "am", - "set-debug-app", - "--persistent", - self.package_id, - ], - capture_output=True, - text=True, - check=True, - ) - - self.profiling_active = True - - def stop(self, output_path, index): - assert GeckoProfiler.is_enabled() - if not self.profiling_active: - raise GeckoProfilerNotActiveError("No active profiling session found") - - # Use content provider to stop profiling and stream raw profile data - # This command blocks until the profile data is fully streamed - output_filename = f"profile-{index}.json" - local_output_path = Path(output_path) / output_filename - - with open(local_output_path, "wb") as output_file: - result = subprocess.run( - [ - "adb", - "shell", - "content", - "read", - "--uri", - f"content://{self.package_id}.profiler/stop-and-upload", - ], - stdout=output_file, - stderr=subprocess.PIPE, - check=False, - ) - - # Always reset state, even if the command failed - self.profiling_active = False - - if result.returncode != 0: - print( - f"Warning: Failed to stop profiler via content provider (exit code {result.returncode})" - ) - if result.stderr: - print(f"Error details:\n{result.stderr.decode()}") - return None - - with open(local_output_path, "rb") as f: - file_data = f.read() - - compressed_size = len(file_data) - try: - decompressed_data = gzip.decompress(file_data) - print( - f"Geckoprofile gzipped size ({compressed_size} bytes -> {len(decompressed_data)} bytes)" - ) - with open(local_output_path, "wb") as output_file: - output_file.write(decompressed_data) - except gzip.BadGzipFile: - print(f"Profile data is not compressed ({compressed_size} bytes)") - - file_size = local_output_path.stat().st_size - print(f"Profile saved to: {local_output_path} ({file_size} bytes)") - return local_output_path - - -class GeckoProfiler(Layer): - name = "geckoprofiler" - activated = False - - def __init__(self, env, mach_cmd): - super().__init__(env, mach_cmd) - self.device = ADBDevice() - self.output_dir = None - self.test_name = None - - @staticmethod - def is_enabled(): - return os.environ.get("MOZPERFTEST_GECKOPROFILE", "0") == "1" - - @staticmethod - def get_controller(): - return GeckoProfilerController() - - def _archive_profiles(self): - """Collect Gecko profiles and add to archive.""" - - if not self.output_dir: - self.info("No output directory set, skipping profile archiving") - return - - patterns = ["profile-*.json"] - self.info(f"geckoview output_dir {self.output_dir} and test {self.test_name}") - - profiles, work_dir = extract_tgz_and_find_files( - self.output_dir, self.test_name, patterns - ) - - try: - if profiles: - # Profiles are streamed directly from device and ready to use - profiles.sort() - archive_files( - profiles, - self.output_dir, - f"profile_{self.test_name}", - prefix="gecko", - ) - self.info("Archived gecko profiles") - finally: - if work_dir: - shutil.rmtree(work_dir) - - def _cleanup(self): - """Cleanup step, called during setup and teardown - - Remove the config files, clear debug app, and unset env flag. - """ - self.device.shell( - f"rm -f {GECKOVIEW_CONFIG_PATH_PREFIX}/*-geckoview-config.yaml" - ) - self.device.shell("am clear-debug-app") - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - GeckoProfilerController._package_id = None - - def setup(self): - self._cleanup() - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - - def teardown(self): - """Cleanup on teardown and add profiles to shared archive.""" - # Collect gecko profiles and add them to a zip file - self._archive_profiles() - self._cleanup() - - def run(self, metadata): - """Run the geckoprofiler layer. - - The run step of the geckoprofiler layer is a no-op since the expectation is that - the start/stop controls are manually called through the ProfilerMediator. - """ - - if not self.env.get_layer("android"): - raise GeckoProfilerError( - "GeckoProfiler is only supported on Android. Please enable the android layer." - ) - metadata.add_extra_options(["gecko-profile"]) - pkg = getattr(metadata, "binary", None) - if pkg: - GeckoProfilerController.set_package_id(pkg) - output = self.get_arg("output", self.output_dir) - self.output_dir = Path(output) if output else None - self.test_name = metadata.script["name"] - return metadata diff --git a/python/mozperftest/mozperftest/system/simpleperf.py b/python/mozperftest/mozperftest/system/simpleperf.py @@ -6,6 +6,8 @@ import os import shutil import signal import subprocess +import tarfile +import tempfile import time import zipfile from pathlib import Path @@ -15,7 +17,7 @@ from mozbuild.nodeutil import find_node_executable from mozdevice import ADBDevice from mozperftest.layers import Layer -from mozperftest.utils import ON_TRY, archive_files, extract_tgz_and_find_files +from mozperftest.utils import ON_TRY """The default Simpleperf options will collect a 30s system-wide profile that uses DWARF based call graph so that we can collect Java stacks. This requires root access. @@ -129,11 +131,10 @@ class SimpleperfController: raise SimpleperfExecutionError("failed to run simpleperf") self.profiler_process = None - profile_path = Path(output_path, f"perf-{index}.data") + output_path = str(Path(output_path, f"perf-{index}.data")) # Pull profiler data directly to the given output path. - self.device.pull("/data/local/tmp/perf.data", str(profile_path)) + self.device.pull("/data/local/tmp/perf.data", output_path) self.device.shell("rm -f /data/local/tmp/perf.data") - return Path(output_path, f"perf-{index}.data") class SimpleperfProfiler(Layer): @@ -158,7 +159,7 @@ class SimpleperfProfiler(Layer): } def __init__(self, env, mach_cmd): - super().__init__(env, mach_cmd) + super(SimpleperfProfiler, self).__init__(env, mach_cmd) self.device = ADBDevice() @staticmethod @@ -194,7 +195,7 @@ class SimpleperfProfiler(Layer): android.ensure_android_ndk(os_name) - self.set_arg("path", Path(android.NDK_PATH, "simpleperf")) + self.set_arg("path", str(Path(android.NDK_PATH, "simpleperf"))) # Make sure the arm64 binary exists in the NDK path. binary_path = Path( @@ -267,14 +268,15 @@ class SimpleperfProfiler(Layer): :return bool: Returns True if preparation is successful, False otherwise. """ - output = self.get_arg("output") - self.output_dir = Path(output) if output else None + self.output_dir = self.get_arg("output") + self.work_dir = Path(tempfile.mkdtemp()) if ON_TRY: moz_fetch = os.environ["MOZ_FETCHES_DIR"] self.breakpad_symbol_dir = Path(moz_fetch, "target.crashreporter-symbols") self.samply_path = Path(moz_fetch, "samply", "samply") self.node_path = Path(moz_fetch, "node", "bin", "node") + self.tgz_path = Path(self.output_dir, self.test_name) self.symbolicator_dir = Path(moz_fetch, "symbolicator-cli") # Extracting crashreporter symbols @@ -291,12 +293,35 @@ class SimpleperfProfiler(Layer): ) ) - def _convert_perf_to_json(self, perf_data, work_dir): + def _get_perf_data(self): + """Retrieve all the perf.data profiles generated by simpleperf. On CI, + .tgz file containing the profiles needs to be extracted first. + + :return list[pathlib.Path]: Returns list of paths to perf.data files + """ + data_dir = self.output_dir + + if ON_TRY: + # Extract perf.data files + tgz_file = Path(f"{self.tgz_path}.tgz") + with tarfile.open(tgz_file, "r:gz") as tar: + tar.extractall(path=self.work_dir) + + data_dir = self.work_dir + + perf_data = [ + data_file + for data_file in Path(data_dir).rglob("*.data") + if data_file.is_file() + ] + + return perf_data + + def _convert_perf_to_json(self, perf_data): """Convert perf.data files into .json files into the Firefox Profiler's processed profile format. :param perf_data list[pathlib.Path]: list of paths to perf.data files - :param work_dir pathlib.Path: working directory for output files :return list[pathlib.Path]: Returns list of paths to .json profiles """ @@ -306,10 +331,7 @@ class SimpleperfProfiler(Layer): for file_path in perf_data: filename = file_path.stem number = filename.split("-")[-1] - output_path = Path( - work_dir if work_dir else self.output_dir, - f"profile-{number}-unsymbolicated.json", - ) + output_path = Path(self.work_dir, f"profile-{number}-unsymbolicated.json") # Run samply import as a blocking command to ensure perf.data # is processed to profile.json before proceeding @@ -337,14 +359,13 @@ class SimpleperfProfiler(Layer): return unsymbolicated_profiles - def _symbolicate_profiles(self, unsymbolicated_profiles, work_dir): + def _symbolicate_profiles(self, unsymbolicated_profiles): """Symbolicate .json profiles. This involves loading the profiles with samply, capturing the symbol server url, and processing the files with symbolicator-cli. :param unsymbolicated_profiles list[pathlib.Path]: list of paths to unsymbolicated profile.json files in processed profile format. - :param work_dir pathlib.Path: working directory for output files :raises SimpleperfSymbolicationTimeoutError: Error if obtaining the symbol server URL from the samply process exceeds SYMBOL_SERVER_TIMEOUT seconds. @@ -390,9 +411,7 @@ class SimpleperfProfiler(Layer): # Symbolicate profiles with a blocking symbolicator-cli call input_profile_path = file_path filename = file_path.stem.replace("-unsymbolicated", "") - output_profile_path = Path( - work_dir if work_dir else self.output_dir, f"{filename}.json" - ) + output_profile_path = Path(self.work_dir, f"{filename}.json") with subprocess.Popen( [ str(self.node_path), @@ -422,7 +441,7 @@ class SimpleperfProfiler(Layer): return symbolicated_profiles def _archive_profiles(self, symbolicated_profiles): - """Archive all symbolicated profiles into a compressed .zip file + """Archive all symbolicated profiles into a compressed .zip file. :param symbolicated_profiles list[pathlib.Path]: List of paths to symbolicated profile.json files to be archived. @@ -430,12 +449,10 @@ class SimpleperfProfiler(Layer): # Archive and export symbolicated profiles symbolicated_profiles.sort() - archive_files( - symbolicated_profiles, - self.output_dir, - f"profile_{self.test_name}", - prefix="simpleperf", - ) + output_zip_path = Path(self.output_dir, f"profile_{self.test_name}.zip") + with zipfile.ZipFile(output_zip_path, "w") as zipf: + for file_path in symbolicated_profiles: + zipf.write(file_path, arcname=file_path.name) def _symbolicate(self): """Convert perf data to symbolicated profiles. @@ -447,25 +464,18 @@ class SimpleperfProfiler(Layer): symbolicator-cli must be provided via command-line arguments for local symbolication. """ - work_dir = None try: self.info("Preparing symbolication environment") self._prepare_symbolication_environment() self.info("Obtaining perf.data files") - perf_data, work_dir = extract_tgz_and_find_files( - self.output_dir, self.test_name, ["*.data"] - ) - - # For local runs, work_dir will be None, + perf_data = self._get_perf_data() self.info("Converting perf.data files to profile.json files") - unsymbolicated_profiles = self._convert_perf_to_json(perf_data, work_dir) + unsymbolicated_profiles = self._convert_perf_to_json(perf_data) self.info("Symbolicating profile.json files") - symbolicated_profiles = self._symbolicate_profiles( - unsymbolicated_profiles, work_dir - ) + symbolicated_profiles = self._symbolicate_profiles(unsymbolicated_profiles) self.info("Archiving symbolicated profile.json files") self._archive_profiles(symbolicated_profiles) @@ -481,8 +491,8 @@ class SimpleperfProfiler(Layer): ) finally: - if work_dir: - shutil.rmtree(work_dir) # Ensure cleanup + if self.work_dir.exists(): + shutil.rmtree(self.work_dir) # Ensure cleanup def teardown(self): self._symbolicate() diff --git a/python/mozperftest/mozperftest/tests/test_geckoprofiler.py b/python/mozperftest/mozperftest/tests/test_geckoprofiler.py @@ -1,485 +0,0 @@ -#!/usr/bin/env python -import gzip -import os -import tarfile -from functools import partial -from pathlib import Path -from unittest import mock -from unittest.mock import MagicMock - -import mozunit -import pytest - -from mozperftest.system.geckoprofiler import ( - DEFAULT_GECKOPROFILER_OPTS, - GeckoProfiler, - GeckoProfilerAlreadyActiveError, - GeckoProfilerController, - GeckoProfilerError, - GeckoProfilerNotActiveError, -) -from mozperftest.tests.support import EXAMPLE_SHELL_TEST, get_running_env - - -def running_env(**kw): - return get_running_env(flavor="custom-script", **kw) - - -class FakeDevice: - def __init__(self): - self.pushed_files = {} - self.commands = [] - self.pulled_files = {} - self.files_on_device = set() - - def push(self, source, destination): - self.pushed_files[destination] = source - - def shell(self, command): - self.commands.append(command) - return "" - - def pull(self, source, destination): - self.pulled_files[destination] = source - - def exists(self, path): - return path in self.files_on_device - - -def mock_subprocess_run(data, returncode, cmd, stdout=None, **kwargs): - """Mock subprocess.run that writes data to a file. - - Args: - data: bytes to write to the file - returncode: exit code to return - cmd: Command being run passed by subprocess.run - stdout: File handle to write to passed by subprocess.run - **kwargs: Other subprocess.run arguments - - Returns: - Mock object with returncode and stderr attributes - """ - if stdout: - stdout.write(data) - result = mock.Mock() - result.returncode = returncode - result.stderr = b"" - return result - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_geckoprofiler_setup(): - mach_cmd, metadata, env = running_env( - app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None - ) - - profiler = GeckoProfiler(env, mach_cmd) - - profiler.setup() - - assert os.environ.get("MOZPERFTEST_GECKOPROFILE") == "1" - - # Mock get_layer to simulate Android layer being present - profiler.env.get_layer = mock.Mock(return_value=True) - result = profiler.run(metadata) - assert result == metadata - assert metadata.get_extra_options() == ["gecko-profile"] - - # Mock _archive_profiles to avoid file operations - profiler._archive_profiles = mock.Mock() - profiler.teardown() - - assert "MOZPERFTEST_GECKOPROFILE" not in os.environ - cleanup_cmd = "rm -f /data/local/tmp/*-geckoview-config.yaml" - assert cleanup_cmd in profiler.device.commands - assert "am clear-debug-app" in profiler.device.commands - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_geckoprofiler_run_requires_android_layer(): - """Test to verify that running without the Android layer will throw an error.""" - mach_cmd, metadata, env = running_env( - app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None - ) - - profiler = GeckoProfiler(env, mach_cmd) - - with pytest.raises(GeckoProfilerError) as excinfo: - profiler.run(metadata) - - assert "only supported on Android" in str(excinfo.value) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_geckoprofiler_is_enabled(): - """Test that verifies that profiling is enabled if MOZPERFTEST_GECKOPROFILE is set to 1 .""" - assert not GeckoProfiler.is_enabled() - - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - assert GeckoProfiler.is_enabled() - - os.environ.pop("MOZPERFTEST_GECKOPROFILE") - assert not GeckoProfiler.is_enabled() - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_geckoprofiler_cleanup_resets_state(): - """Test to make sure cleanup removes the GECKOPROFILE environment variable and resets package ID in the controller""" - mach_cmd, metadata, env = running_env( - app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None - ) - - profiler = GeckoProfiler(env, mach_cmd) - - GeckoProfilerController.set_package_id("org.mozilla.firefox") - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - - profiler._cleanup() - - assert "MOZPERFTEST_GECKOPROFILE" not in os.environ - assert GeckoProfilerController._package_id is None - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -@mock.patch("tempfile.NamedTemporaryFile") -def test_controller_start_with_default_options(mock_temp, mock_run): - """Test starting the controller with no options uses the default settings and pushes them as a YAML config file to the device""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.fenix") - mock_temp.return_value.__enter__.return_value.name = "/tmp/test_config" - - controller = GeckoProfilerController() - controller.start() - - assert controller.profiling_active - assert controller.package_id == "org.mozilla.fenix" - assert controller.config_filename == "org.mozilla.fenix-geckoview-config.yaml" - - push_call = None - set_debug_call = None - for call_args in mock_run.call_args_list: - args = call_args[0][0] - if args[0] == "adb" and args[1] == "push": - push_call = args - elif args[0] == "adb" and "set-debug-app" in args: - set_debug_call = args - - assert push_call is not None - assert set_debug_call is not None - assert "org.mozilla.fenix" in set_debug_call - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -@mock.patch("tempfile.NamedTemporaryFile") -def test_controller_start_with_custom_options(mock_temp, mock_run): - """Test starting the controller with custom options generates a YAML config file with those values""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - custom_opts = { - "interval": 10, - "features": "js,stackwalk", - "filters": "GeckoMain", - } - - mock_file = MagicMock() - mock_file.name = "/tmp/test_config" - mock_temp.return_value.__enter__.return_value = mock_file - - controller = GeckoProfilerController() - controller.start(custom_opts) - - written_content = b"" - for call_args in mock_file.write.call_args_list: - written_content += call_args[0][0] - - config_str = written_content.decode() - assert "MOZ_PROFILER_STARTUP_INTERVAL: 10" in config_str - assert "MOZ_PROFILER_STARTUP_FEATURES: js,stackwalk" in config_str - assert "MOZ_PROFILER_STARTUP_FILTERS: GeckoMain" in config_str - assert controller.profiling_active - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_controller_start_already_active(): - """Test attempting to start profiling when it's already active raises an error""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - - with pytest.raises(GeckoProfilerAlreadyActiveError) as excinfo: - controller.start() - - assert "already active" in str(excinfo.value) - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_controller_stop_not_active(): - """Test attempting to stop the profiler with no active session raises an error""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - - controller = GeckoProfilerController() - controller.profiling_active = False - - output_dir = Path("/tmp/output") - - with pytest.raises(GeckoProfilerNotActiveError) as excinfo: - controller.stop(str(output_dir), 1) - - assert "No active profiling session" in str(excinfo.value) - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -def test_controller_package_id_not_set(): - """Test that trying to resolve a package ID when it is not set raises an erorr""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController._package_id = None - - controller = GeckoProfilerController() - - with pytest.raises(GeckoProfilerError) as excinfo: - controller._resolve_package_id() - - assert "Package id not set" in str(excinfo.value) - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.extract_tgz_and_find_files") -def test_geckoprofiler_archive_profiles(mock_extract, tmp_path): - """Test that archived profiles are extracted from .tgz and packaged into a .zip file.""" - mach_cmd, metadata, env = running_env( - app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=str(tmp_path) - ) - - profiler = GeckoProfiler(env, mach_cmd) - profiler.output_dir = tmp_path - profiler.test_name = "test_gecko" - - work_dir = tmp_path / "work" - work_dir.mkdir() - - # Profiles are now streamed as .json directly from device (not compressed) - profile = work_dir / "profile-0.json" - with open(profile, "w") as f: - f.write('{"meta": {"profile_type": "gecko"}}') - - tgz_file = tmp_path / "test_gecko.tgz" - with tarfile.open(tgz_file, "w:gz") as tar: - tar.add(profile, arcname=profile.name) - - mock_extract.return_value = ([profile], work_dir) - - profiler._archive_profiles() - - archive_file = tmp_path / "profile_test_gecko.zip" - assert archive_file.exists() - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -@mock.patch("tempfile.NamedTemporaryFile") -def test_controller_config_file_naming(mock_temp, mock_run): - """Test that the YAML filename follows the naming rule "{package_id}-geckoview-config.yaml".""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("com.example.app") - mock_temp.return_value.__enter__.return_value.name = "/tmp/config" - - controller = GeckoProfilerController() - controller.start() - - expected_config_name = "com.example.app-geckoview-config.yaml" - assert controller.config_filename == expected_config_name - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -def test_controller_stop_success(mock_run, tmp_path): - """Test that the controller stop successfully stops profiling and saves profile""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - controller.package_id = "org.mozilla.firefox" - - output_dir = tmp_path - index = 1 - profile_path = output_dir / f"profile-{index}.json" - - # Mock subprocess to write valid profile data - profile_json_data = b'{"meta": {"version": 1}, "threads": []}' - mock_run.side_effect = partial(mock_subprocess_run, profile_json_data, 0) - - result = controller.stop(str(output_dir), index) - - assert controller.profiling_active is False - assert result == profile_path - assert profile_path.exists() - assert profile_path.read_bytes() == profile_json_data - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -def test_controller_stop_error(mock_run, tmp_path): - """Test that GeckoProfilerController.stop() handles errors and resets state""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - controller.package_id = "org.mozilla.firefox" - - output_dir = tmp_path - index = 1 - - # Mock subprocess to return error - mock_run.side_effect = partial(mock_subprocess_run, b"", 1) - - # Call stop() - should handle error gracefully - result = controller.stop(str(output_dir), index) - - # Verify profiling_active is reset even on error - assert controller.profiling_active is False - # Verify None is returned on error - assert result is None - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -def test_controller_stop_empty_file(mock_run, tmp_path): - """Test that GeckoProfilerController.stop() handles empty profile data""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - controller.package_id = "org.mozilla.firefox" - - output_dir = tmp_path - index = 1 - - # Mock subprocess to write empty file - mock_run.side_effect = partial(mock_subprocess_run, b"", 0) - - # Call stop() - should detect empty file and return None - result = controller.stop(str(output_dir), index) - - assert controller.profiling_active is False - assert result is None - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -def test_controller_stop_error_response(mock_run, tmp_path): - """Test that GeckoProfilerController.stop() handles ERROR responses from content provider""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - controller.package_id = "org.mozilla.firefox" - - output_dir = tmp_path - index = 1 - - # Mock subprocess to write ERROR response - error_data = b"ERROR=Profiler not running" - mock_run.side_effect = partial(mock_subprocess_run, error_data, 0) - - # ERROR in the output file should return None - result = controller.stop(str(output_dir), index) - - assert controller.profiling_active is False - assert result is None - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -def test_controller_stop_gzipped_profile(mock_run, tmp_path): - """Test that GeckoProfilerController.stop() decompresses gzipped profiles""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - controller = GeckoProfilerController() - controller.profiling_active = True - controller.package_id = "org.mozilla.firefox" - - output_dir = tmp_path - index = 1 - profile_path = output_dir / f"profile-{index}.json" - - profile_json_data = b'{"meta": {"version": 1}, "threads": []}' - compressed_data = gzip.compress(profile_json_data) - - mock_run.side_effect = partial(mock_subprocess_run, compressed_data, 0) - result = controller.stop(str(output_dir), index) - - assert controller.profiling_active is False - assert result == profile_path - # Verify file was successfully decompressed - assert profile_path.read_bytes() == profile_json_data # Should be decompressed - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -@mock.patch("mozperftest.system.geckoprofiler.ADBDevice", new=FakeDevice) -@mock.patch("mozperftest.system.geckoprofiler.subprocess.run") -@mock.patch("tempfile.NamedTemporaryFile") -def test_controller_merges_default_and_custom_options(mock_temp, mock_run): - """Test that when partial custom options are given, they are merged with the defaults one""" - os.environ["MOZPERFTEST_GECKOPROFILE"] = "1" - GeckoProfilerController.set_package_id("org.mozilla.firefox") - - partial_opts = {"interval": 15} - - mock_file = MagicMock() - mock_file.name = "/tmp/test_config" - mock_temp.return_value.__enter__.return_value = mock_file - - controller = GeckoProfilerController() - controller.start(partial_opts) - - written_content = b"" - for call_args in mock_file.write.call_args_list: - written_content += call_args[0][0] - - config_str = written_content.decode() - assert "MOZ_PROFILER_STARTUP_INTERVAL: 15" in config_str - assert ( - f"MOZ_PROFILER_STARTUP_FEATURES: {DEFAULT_GECKOPROFILER_OPTS['features']}" - in config_str - ) - assert ( - f"MOZ_PROFILER_STARTUP_FILTERS: {DEFAULT_GECKOPROFILER_OPTS['filters']}" - in config_str - ) - - os.environ.pop("MOZPERFTEST_GECKOPROFILE", None) - - -if __name__ == "__main__": - mozunit.main() diff --git a/python/mozperftest/mozperftest/tests/test_simpleperf.py b/python/mozperftest/mozperftest/tests/test_simpleperf.py @@ -178,7 +178,7 @@ def test_simpleperf_setup_without_path(mock_exists): # Verify simpleperf path was set correctly. expected_path = mock_ndk / "simpleperf" - assert profiler.get_arg("path") == expected_path + assert profiler.get_arg("path") == str(expected_path) # Verify binary was installed. mock_exists.assert_called_once_with( @@ -441,10 +441,10 @@ def test_local_simpleperf_symbolicate(tmp_path): # Mock files (mock_perf_data_path := output_dir / "mock_perf-0.data").write_text("mock-data") - (output_dir / "profile-0-unsymbolicated.json").write_text( + (mock_work_dir_path / "profile-0-unsymbolicated.json").write_text( "mock-unsymbolicated-profile" ) - (output_dir / "profile-0.json").write_text("mock-symbolicated-profile") + (mock_work_dir_path / "profile-0.json").write_text("mock-symbolicated-profile") # Mock args profiler.set_arg("symbol-path", symbol_dir) @@ -478,7 +478,7 @@ def test_local_simpleperf_symbolicate(tmp_path): profiler.teardown() # Verify the temporary work directory is deleted - mock_rmtree.assert_not_called() + mock_rmtree.assert_called_once_with(mock_work_dir_path) # Expected process calls expected_import = call( @@ -488,7 +488,7 @@ def test_local_simpleperf_symbolicate(tmp_path): str(mock_perf_data_path), "--save-only", "-o", - str(output_dir / "profile-0-unsymbolicated.json"), + str(mock_work_dir_path / "profile-0-unsymbolicated.json"), ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -500,7 +500,7 @@ def test_local_simpleperf_symbolicate(tmp_path): [ "samply", "load", - str(output_dir / "profile-0-unsymbolicated.json"), + str(mock_work_dir_path / "profile-0-unsymbolicated.json"), "--no-open", "--breakpad-symbol-dir", str(symbol_dir), @@ -517,9 +517,9 @@ def test_local_simpleperf_symbolicate(tmp_path): str(node_path), str(symbolicator_dir / "symbolicator-cli.js"), "--input", - str(output_dir / "profile-0-unsymbolicated.json"), + str(mock_work_dir_path / "profile-0-unsymbolicated.json"), "--output", - str(output_dir / "profile-0.json"), + str(mock_work_dir_path / "profile-0.json"), "--server", "http://127.0.0.1:3000", ], @@ -600,9 +600,9 @@ def test_local_simpleperf_symbolicate_timeout(tmp_path): str(node_path), str(symbolicator_dir / "symbolicator-cli.js"), "--input", - str(output_dir / "profile-0-unsymbolicated.json"), + str(mock_work_dir_path / "profile-0-unsymbolicated.json"), "--output", - str(output_dir / "profile-0.json"), + str(mock_work_dir_path / "profile-0.json"), "--server", "http://127.0.0.1:3000", ], @@ -617,7 +617,7 @@ def test_local_simpleperf_symbolicate_timeout(tmp_path): assert expected_symbolicator not in mock_popen.call_args_list # Check for clean exit - mock_rmtree.assert_not_called() + mock_rmtree.assert_called_once() mock_cleanup.assert_called_once() @@ -681,8 +681,6 @@ def test_ci_simpleperf_symbolicate(tmp_path): }, clear=False, ), mock.patch("mozperftest.system.simpleperf.ON_TRY", True), mock.patch( - "mozperftest.utils.ON_TRY", True - ), mock.patch( "tempfile.mkdtemp", return_value=str(mock_work_dir_path) ), mock.patch( "shutil.rmtree" @@ -708,7 +706,7 @@ def test_ci_simpleperf_symbolicate(tmp_path): profiler.teardown() # Verify the temporary work directory is deleted - mock_rmtree.assert_called_once() + mock_rmtree.assert_called_once_with(mock_work_dir_path) # Verify proper .zip extraction mock_symbol_path = ( @@ -830,8 +828,6 @@ def test_ci_simpleperf_symbolicate_timeout(tmp_path): # Test timeout error in CI with mock.patch("mozperftest.system.simpleperf.ON_TRY", True), mock.patch( - "mozperftest.utils.ON_TRY", True - ), mock.patch( "tempfile.mkdtemp", return_value=str(mock_work_dir_path) ), mock.patch.dict( os.environ, diff --git a/python/mozperftest/mozperftest/utils.py b/python/mozperftest/mozperftest/utils.py @@ -15,7 +15,6 @@ import subprocess import sys import tarfile import tempfile -import zipfile from collections import defaultdict from datetime import date, datetime, timedelta from io import StringIO @@ -664,66 +663,3 @@ def archive_folder(folder_to_archive, output_path, archive_name=None): tar.add(folder_to_archive, arcname=archive_name) return full_archive_path - - -def archive_files(files, output_dir, archive_name, prefix=""): - """Archives individual files into a zip file, with optional append mode. - - Args: - files: List of Path objects to archive - output_dir: Path object - directory where the archive should be created - archive_name: Name for the archive - prefix: Optional prefix for archived filenames - - Returns: - Path to the archive if created/updated, None otherwise - """ - output_dir.mkdir(parents=True, exist_ok=True) - archive_path = output_dir / f"{archive_name}.zip" - - mode = "a" if archive_path.exists() else "w" - - with zipfile.ZipFile(archive_path, mode, compression=zipfile.ZIP_DEFLATED) as zf: - for file_path in files: - base_name = file_path.name - - if prefix: - archive_name = f"{prefix}-{base_name}" - else: - archive_name = base_name - - print(f"Adding {archive_name} to archive") - zf.write(file_path, arcname=archive_name) - - return archive_path - - -def extract_tgz_and_find_files(output_dir, tgz_name, patterns): - """Extract TGZ file if on CI and find files matching patterns. - - Args: - output_dir: Path object - directory where files are located or where TGZ should be extracted - tgz_name: Name of the TGZ file (without extension) - patterns: List of patterns for file extensions (e.g., ["*.data", "*.json.gz"]) - - Returns: - Tuple of (files, work_dir) where work_dir is the temp directory to clean up (or None) - """ - work_dir = None - search_dir = output_dir - - if ON_TRY: - tgz_path = output_dir / f"{tgz_name}.tgz" - if tgz_path.exists(): - work_dir = Path(tempfile.mkdtemp()) - with tarfile.open(tgz_path, "r:gz") as tar: - tar.extractall(path=work_dir) - search_dir = work_dir - - found_files = [] - for pattern in patterns: - found_files.extend(search_dir.rglob(pattern)) - - valid_files = [f for f in found_files if f.is_file()] - - return (valid_files, work_dir) diff --git a/taskcluster/gecko_taskgraph/target_tasks.py b/taskcluster/gecko_taskgraph/target_tasks.py @@ -1574,7 +1574,7 @@ def target_tasks_perftest_fenix_startup(full_task_graph, parameters, graph_confi for name, task in full_task_graph.tasks.items(): if task.kind != "perftest": continue - if "fenix" in name and "startup" in name and "profiling" not in name: + if "fenix" in name and "startup" in name and "simpleperf" not in name: yield name diff --git a/taskcluster/gecko_taskgraph/transforms/perftest.py b/taskcluster/gecko_taskgraph/transforms/perftest.py @@ -277,10 +277,10 @@ def create_duplicate_simpleperf_jobs(config, jobs): new_job["dependencies"] = { "android-aarch64-shippable": "build-android-aarch64-shippable/opt" } - new_job["name"] += "-profiling" + new_job["name"] += "-simpleperf" new_job["run"][ "command" - ] += " --simpleperf --simpleperf-path $MOZ_FETCHES_DIR/android-simpleperf --geckoprofiler" + ] += " --simpleperf --simpleperf-path $MOZ_FETCHES_DIR/android-simpleperf" new_job["description"] = str(new_job["description"]).replace( "Run", "Profile" ) diff --git a/tools/tryselect/selectors/perfselector/classification.py b/tools/tryselect/selectors/perfselector/classification.py @@ -637,7 +637,7 @@ class ClassificationProvider: }, "Startup": { "query": { - Suites.PERFTEST.value: ["'startup !-test- !profiling"], + Suites.PERFTEST.value: ["'startup !-test- !simple"], Suites.TALOS.value: ["'sessionrestore | 'other !damp"], }, "suites": [Suites.PERFTEST.value, Suites.TALOS.value],