# 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 os import shutil import signal import subprocess import time import zipfile from pathlib import Path from urllib.parse import unquote
from mozbuild.nodeutil import find_node_executable
from mozperftest.layers import Layer from mozperftest.utils import (
ON_TRY,
archive_files,
extract_tgz_and_find_files,
get_adb_device_or_emu,
)
"""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. """
DEFAULT_SIMPLEPERF_OPTS = "-g --duration 30 -f 1000 --trace-offcpu -e cpu-clock -a"
class SimpleperfError(Exception): """Base class for Simpleperf-related exceptions."""
pass
class SimpleperfAlreadyRunningError(SimpleperfError): """Raised when attempting to start simpleperf while it's already running."""
pass
class SimpleperfNotRunningError(SimpleperfError): """Raised when attempting to stop simpleperf when it's not running."""
pass
class SimpleperfExecutionError(SimpleperfError): """Raised when simpleperf fails to execute properly."""
pass
class SimpleperfSystemError(SimpleperfError): """Raised when the system is not compatible with Android NDK installation."""
pass
class SimpleperfBinaryNotFoundError(SimpleperfError): """Raised when the simpleperf binary cannot be found at the expected path."""
pass
class SimpleperfSymbolicationTimeoutError(SimpleperfError): """Raised when starting the samply server required for symbolicating
simpleperf profiles execeeds the specified time time limit."""
pass
class SimpleperfSymbolicationError(SimpleperfError): """Raised when symbolication paths are missing or invalid."""
pass
class SimpleperfController: def __init__(self):
self.device = get_adb_device_or_emu()
self.profiler_process = None
def start(self, simpleperf_opts): """Starts the simpleperf profiler asynchronously if the layer is enabled.
This method expects that the /data/local/tmp/simpleperf binary has
already been installed during the setup phase of the layer.
The simpleperf options can be provided as an argument. Ifnone are
provided, we default to system-wide profiling which will require
root access. """ if simpleperf_opts isNone:
simpleperf_opts = DEFAULT_SIMPLEPERF_OPTS
assert SimpleperfProfiler.is_enabled() if self.profiler_process: raise SimpleperfAlreadyRunningError("simpleperf already running")
cmd = f"/data/local/tmp/simpleperf record {simpleperf_opts} -o /data/local/tmp/perf.data"
# Make sure the arm64 binary exists in the NDK path.
binary_path = Path(
self.get_arg("path"), "bin", "android", "arm64", "simpleperf"
) ifnot os.path.exists(binary_path): raise SimpleperfBinaryNotFoundError(
f"Cannot find simpleperf binary at {binary_path}"
)
def _cleanup(self): """Cleanup step, called during setup and teardown.
Remove any leftover profiles and simpleperf binaries on the device, and also undefine the $MOZPERFTEST_SIMPLEPERF environment variable. """
self.device.shell("rm -f /data/local/tmp/perf.data /data/local/tmp/simpleperf")
os.environ.pop("MOZPERFTEST_SIMPLEPERF", None)
def setup(self): """Setup the simpleperf layer
First verify that the simpleperf NDK and ARM64 binary exists.
Next, install the ARM64 simpleperf binary in /data/local/tmp on the device. Finally, define $MOZPERFTEST_SIMPLEPERF to indicate layer is active. """
self.setup_simpleperf_path()
self._cleanup()
self.device.push(
Path(self.get_arg("path"), "bin", "android", "arm64", "simpleperf"), "/data/local/tmp",
)
self.device.shell("chmod a+x /data/local/tmp/simpleperf")
os.environ["MOZPERFTEST_SIMPLEPERF"] = "1"
def _validate_symbolication_paths(
self, symbol_dir_arg, profiler_node_tools_dir_arg
): """Check if the breakpad directory path and the profiler-node-tools paths for symbolication are valid.
:param symbol_dir_arg str: Path to the Breakpad symbol directory
:param profiler_node_tools_dir_arg str: Path to the profiler-node-tools directory
:return tuple[pathlib.Path, pathlib.Path]: Returns a tuple containing validated (breakpad_symbol_dir, profiler_node_tools_dir).
:raises SimpleperfSymbolicationError: If validation fails """
ifnot symbol_dir_arg: raise SimpleperfSymbolicationError( "Breakpad Symbol Directory not provided."
)
breakpad_symbol_dir = Path(symbol_dir_arg) ifnot breakpad_symbol_dir.exists(): raise SimpleperfSymbolicationError(
f"Breakpad Symbol Directory not found at {breakpad_symbol_dir}."
)
ifnot profiler_node_tools_dir_arg: raise SimpleperfSymbolicationError( "Profiler-node-tools directory not provided."
)
profiler_node_tools_dir = Path(profiler_node_tools_dir_arg) ifnot profiler_node_tools_dir.exists(): raise SimpleperfSymbolicationError(
f"Profiler-node-tools directory not found at {profiler_node_tools_dir}."
)
# Extracting crashreporter symbols
zip_path = f"{self.breakpad_symbol_dir}.zip" with zipfile.ZipFile(zip_path, "r") as zipf:
zipf.extractall(self.breakpad_symbol_dir) else:
self.samply_path = "samply"# Assumed to be available via PATH
self.node_path = Path(find_node_executable()[0]).resolve()
self.breakpad_symbol_dir, self.profiler_node_tools_dir = (
self._validate_symbolication_paths(
self.get_arg("symbol-path", None),
self.get_arg("profiler-node-tools-path", None),
)
)
def _convert_perf_to_json(self, perf_data, work_dir): """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 """
unsymbolicated_profiles = []
# Convert perf.data to unsymbolicated .json profiles for file_path in perf_data:
filename = file_path.stem
number = filename.split("-")[-1]
# Run samply import as a blocking command to ensure perf.data # is processed to profile.json before proceeding with subprocess.Popen(
[
str(self.samply_path), "import",
str(file_path), "--save-only", "-o",
str(output_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
) as samply_process: # Stream and forward to self.info() for line in samply_process.stdout:
self.info(f"samply {line.strip()}")
unsymbolicated_profiles.append(output_path)
return unsymbolicated_profiles
def _symbolicate_profiles(self, unsymbolicated_profiles, work_dir): """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.
:return list[pathlib.Path]: Returns list of paths to symbolicated profiles in processed profile format. """
symbolicated_profiles = []
for file_path in unsymbolicated_profiles: # Load unsymbolicated profile with samply
samply_process = subprocess.Popen(
[
str(self.samply_path), "load",
str(file_path), "--no-open", "--breakpad-symbol-dir",
str(self.breakpad_symbol_dir), "--breakpad-symbol-server",
BREAKPAD_SYMBOL_SERVER,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
# Tail output for timeout seconds to obtain symbol server url
server_url = ""
start = time.time() with samply_process.stdout: for line in iter(samply_process.stdout.readline, b""): if line.startswith("http"):
url = unquote(line)
server_url = str(url.split("symbolServer=", 1)[-1]) break
if (time.time() - start) > SYMBOL_SERVER_TIMEOUT: raise SimpleperfSymbolicationTimeoutError(
f"Timed out after {SYMBOL_SERVER_TIMEOUT} seconds while waiting for samply server to start"
)
# Symbolicate profiles with a blocking symbolicator-cli call
input_profile_path = file_path
filename = file_path.stem.replace("-unsymbolicated", "")
output_profile_path = file_path.parent / f"{filename}.json" with subprocess.Popen(
[
str(self.node_path),
str(Path(self.profiler_node_tools_dir, "profiler-edit.js")), "-i",
str(input_profile_path), "-o",
str(output_profile_path), "--symbolicate-with-server",
server_url,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
) as profiler_edit_process: # Stream and forward to self.info() for line in profiler_edit_process.stdout:
self.info(f"profiler-edit {line.strip()}")
symbolicated_profiles.append(output_profile_path)
# Terminate samply server
samply_process.send_signal(signal.SIGINT)
samply_process.wait()
return symbolicated_profiles
def _archive_profiles(self, symbolicated_profiles, base_dir): """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.
:param base_dir pathlib.Path: Base directory to preserve relative paths in archive. """
def _symbolicate(self): """Convert perf data to symbolicated profiles.
This method works for tests run locally andin CI. When run
locally, it assumes that samply is already
installed on the system. Additionally, local paths to
the directories containing Breakpad symbols and a build of
profiler-node-tools 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("Archiving symbolicated profile.json files")
self._archive_profiles(symbolicated_profiles, search_dir) except SimpleperfSymbolicationError as e: # If flags are not provided / invalid, skip this symbolication step completely
self.warning(
f"Failed to prepare symbolication environment. Skipping profile symbolication: {e}"
)
except SimpleperfSymbolicationTimeoutError as e:
self.warning(
f"Timed out after while waiting for samply server. Skipping profile symbolication: {e}"
)
finally: if work_dir:
shutil.rmtree(work_dir) # Ensure cleanup
def run(self, metadata): """Run the simpleperf layer.
The run step of the simpleperf layer is a no-op since the expectation is that
the start/stop controls are manually called through the ProfilerMediator. """
self.test_name = metadata.script["name"]
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.