mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	This patch adds a job called `run-macrobenchmark-firebase-fenix` that runs a macrobenchmark on a physical device on Firebase Test lab to benchmark the impact of the baseline profile on app startup. Differential Revision: https://phabricator.services.mozilla.com/D229019
		
			
				
	
	
		
			325 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
# 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/.
 | 
						|
 | 
						|
"""
 | 
						|
This script is designed to automate the process of fetching artifacts (either baseline profile or crash logs)
 | 
						|
from Google Cloud Storage (GCS) for devices in Firebase TestLab.
 | 
						|
It is intended to be run as part of a Taskcluster job following a scheduled test task, or as part of
 | 
						|
a Taskcluster that runs baseline profile generation on Firebase TestLab.
 | 
						|
The script requires the presence of a `matrix_ids.json` artifact in the results directory
 | 
						|
and the availability of the `gsutil` command in the environment.
 | 
						|
 | 
						|
The script performs the following operations:
 | 
						|
- Loads the `matrix_ids.json` artifact to identify the GCS paths for the artifacts.
 | 
						|
- In the case of crash logs, identifies failed devices based on the outcomes specified in the `matrix_ids.json` artifact.
 | 
						|
- Fetches the specified artifact type (baseline profiles or crash logs) from the specified GCS paths.
 | 
						|
- Copies the fetched artifacts to the current worker artifact results directory.
 | 
						|
 | 
						|
The script is configured to log its operations and errors, providing visibility into its execution process.
 | 
						|
It uses the `gsutil` command-line tool to interact with GCS, ensuring compatibility with the GCS environment.
 | 
						|
 | 
						|
Usage:
 | 
						|
    python3 copy-artifacts-from-ftl.py <artifact_type>
 | 
						|
 | 
						|
    artifact_type: "baseline_profile" or "crash_log"
 | 
						|
 | 
						|
Requirements:
 | 
						|
    - The `matrix_ids.json` artifact must be present in the results directory.
 | 
						|
    - The `gsutil` command must be available in the environment.
 | 
						|
    - The script should be run after a scheduled test task in a Taskcluster job or as part of a
 | 
						|
        scheduled baseline profile task in a Taskcluster job
 | 
						|
 | 
						|
Output:
 | 
						|
    - Artifacts are copied to the current worker artifact results directory.
 | 
						|
"""
 | 
						|
 | 
						|
import json
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import re
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
from enum import Enum
 | 
						|
 | 
						|
 | 
						|
def setup_logging():
 | 
						|
    """Configure logging for the script."""
 | 
						|
    log_format = "%(message)s"
 | 
						|
    logging.basicConfig(level=logging.INFO, format=log_format)
 | 
						|
 | 
						|
 | 
						|
class Worker(Enum):
 | 
						|
    """
 | 
						|
    Worker paths
 | 
						|
    """
 | 
						|
 | 
						|
    RESULTS_DIR = "/builds/worker/artifacts/results"
 | 
						|
    BASELINE_PROFILE_DEST = "/builds/worker/artifacts/build/baseline-prof.txt"
 | 
						|
    MACROBENCHMARK_DEST = "/builds/worker/artifacts/build/macrobenchmark.json"
 | 
						|
    ARTIFACTS_DIR = "/builds/worker/artifacts"
 | 
						|
 | 
						|
 | 
						|
class ArtifactType(Enum):
 | 
						|
    """
 | 
						|
    Artifact types for fetching matrix IDs, crash logs and baseline profile.
 | 
						|
    """
 | 
						|
 | 
						|
    BASELINE_PROFILE = (
 | 
						|
        "artifacts/sdcard/Android/media/org.mozilla.fenix.benchmark/*-baseline-prof.txt"
 | 
						|
    )
 | 
						|
    CRASH_LOG = "data_app_crash*.txt"
 | 
						|
    MACROBENCHMARK = (
 | 
						|
        "artifacts/sdcard/Android/media/org.mozilla.fenix.benchmark/*benchmarkData.json"
 | 
						|
    )
 | 
						|
    MATRIX_IDS = "matrix_ids.json"
 | 
						|
 | 
						|
 | 
						|
def load_matrix_ids_artifact(matrix_file_path):
 | 
						|
    """Load the matrix IDs artifact from the specified file path.
 | 
						|
 | 
						|
    Args:
 | 
						|
        matrix_file_path (str): The file path to the matrix IDs artifact.
 | 
						|
    Returns:
 | 
						|
        dict: The contents of the matrix IDs artifact.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        with open(matrix_file_path) as f:
 | 
						|
            return json.load(f)
 | 
						|
    except FileNotFoundError:
 | 
						|
        exit_with_error(f"Could not find matrix file: {matrix_file_path}")
 | 
						|
    except json.JSONDecodeError:
 | 
						|
        exit_with_error(f"Error decoding matrix file: {matrix_file_path}")
 | 
						|
 | 
						|
 | 
						|
def get_gcs_path(matrix_artifact_file):
 | 
						|
    """
 | 
						|
    Extract the root GCS path from the matrix artifact file.
 | 
						|
 | 
						|
    Args:
 | 
						|
        matrix_artifact_file (dict): The matrix artifact file contents.
 | 
						|
    Returns:
 | 
						|
        str: The root GCS path extracted from the matrix artifact file.
 | 
						|
    """
 | 
						|
    for matrix in matrix_artifact_file.values():
 | 
						|
        gcs_path = matrix.get("gcsPath")
 | 
						|
        if gcs_path:
 | 
						|
            return gcs_path
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def check_gsutil_availability():
 | 
						|
    """
 | 
						|
    Check the availability of the `gsutil` command in the environment.
 | 
						|
    Exit the script if `gsutil` is not available.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        subprocess.run(
 | 
						|
            ["gsutil", "--version"], capture_output=True, text=True, check=True
 | 
						|
        )
 | 
						|
    except Exception as e:
 | 
						|
        exit_with_error(f"Error executing gsutil: {e}")
 | 
						|
 | 
						|
 | 
						|
def fetch_artifacts(root_gcs_path, device, artifact_pattern):
 | 
						|
    """
 | 
						|
    Fetch artifacts from the specified GCS path pattern for the given device.
 | 
						|
 | 
						|
    Args:
 | 
						|
        root_gcs_path (str): The root GCS path for the artifacts.
 | 
						|
        device (str): The device name for which to fetch artifacts.
 | 
						|
        artifact_pattern (str): The pattern to match the artifacts.
 | 
						|
    Returns:
 | 
						|
        list: A list of artifacts matching the specified pattern.
 | 
						|
    """
 | 
						|
    gcs_path_pattern = f"gs://{root_gcs_path.rstrip('/')}/{device}/{artifact_pattern}"
 | 
						|
 | 
						|
    try:
 | 
						|
        result = subprocess.check_output(["gsutil", "ls", gcs_path_pattern], text=True)
 | 
						|
        return result.splitlines()
 | 
						|
    except subprocess.CalledProcessError as e:
 | 
						|
        if "AccessDeniedException" in e.output:
 | 
						|
            logging.error(f"Permission denied for GCS path: {gcs_path_pattern}")
 | 
						|
        elif "network error" in e.output.lower():
 | 
						|
            logging.error(f"Network error accessing GCS path: {gcs_path_pattern}")
 | 
						|
        else:
 | 
						|
            logging.error(f"Failed to list files: {e.output}")
 | 
						|
        return []
 | 
						|
    except Exception as e:
 | 
						|
        logging.error(f"Error executing gsutil: {e}")
 | 
						|
        return []
 | 
						|
 | 
						|
 | 
						|
def fetch_device_names(matrix_artifact_file, only_failed=False):
 | 
						|
    """
 | 
						|
    Fetch the names of devices that were used based on the outcomes specified in the matrix artifact file.
 | 
						|
 | 
						|
    Args:
 | 
						|
        matrix_artifact_file (dict): The matrix artifact file contents.
 | 
						|
        only_failed (bool): If True, only return devices with failed outcomes.
 | 
						|
    Returns:
 | 
						|
        list: A list of device names.
 | 
						|
    """
 | 
						|
    devices = []
 | 
						|
    for matrix in matrix_artifact_file.values():
 | 
						|
        axes = matrix.get("axes", [])
 | 
						|
        for axis in axes:
 | 
						|
            if not only_failed or axis.get("outcome") == "failure":
 | 
						|
                device = axis.get("device")
 | 
						|
                if device:
 | 
						|
                    devices.append(device)
 | 
						|
    return devices
 | 
						|
 | 
						|
 | 
						|
def gsutil_cp(artifact, dest):
 | 
						|
    """
 | 
						|
    Copy the specified artifact to the destination path using `gsutil`.
 | 
						|
 | 
						|
    Args:
 | 
						|
        artifact (str): The path to the artifact to copy.
 | 
						|
        dest (str): The destination path to copy the artifact to.
 | 
						|
    Returns:
 | 
						|
        None
 | 
						|
    """
 | 
						|
    logging.info(f"Copying {artifact} to {dest}")
 | 
						|
    try:
 | 
						|
        result = subprocess.run(
 | 
						|
            ["gsutil", "cp", artifact, dest], capture_output=True, text=True
 | 
						|
        )
 | 
						|
        if result.returncode != 0:
 | 
						|
            if "AccessDeniedException" in result.stderr:
 | 
						|
                logging.error(f"Permission denied for GCS path: {artifact}")
 | 
						|
            elif "network error" in result.stderr.lower():
 | 
						|
                logging.error(f"Network error accessing GCS path: {artifact}")
 | 
						|
            else:
 | 
						|
                logging.error(f"Failed to list files: {result.stderr}")
 | 
						|
    except Exception as e:
 | 
						|
        logging.error(f"Error executing gsutil: {e}")
 | 
						|
 | 
						|
 | 
						|
def parse_crash_log(log_path):
 | 
						|
    """Parse the crash log and log any crash stacks in a specific format."""
 | 
						|
    crashes_reported = 0
 | 
						|
    if os.path.isfile(log_path):
 | 
						|
        with open(log_path) as f:
 | 
						|
            contents = f.read()
 | 
						|
            proc = "unknown"
 | 
						|
            match = re.search(r"Process: (.*)\n", contents, re.MULTILINE)
 | 
						|
            if match and len(match.groups()) == 1:
 | 
						|
                proc = match.group(1)
 | 
						|
            match = re.search(
 | 
						|
                r"\n([\w\.]+[:\s\w\.,!?#^\'\"]+)\s*(at\s.*\n)", contents, re.MULTILINE
 | 
						|
            )
 | 
						|
            if match and len(match.groups()) == 2:
 | 
						|
                top_frame = match.group(1).rstrip() + " " + match.group(2)
 | 
						|
                remainder = contents[match.span()[1] :]
 | 
						|
                logging.error(f"PROCESS-CRASH | {proc} | {top_frame}{remainder}")
 | 
						|
                crashes_reported = 1
 | 
						|
    return crashes_reported
 | 
						|
 | 
						|
 | 
						|
def process_artifacts(artifact_type):
 | 
						|
    """
 | 
						|
    Process the artifacts based on the specified artifact type.
 | 
						|
 | 
						|
    Args:
 | 
						|
        artifact_type (ArtifactType): The type of artifact to process.
 | 
						|
    """
 | 
						|
 | 
						|
    matrix_ids_artifact = load_matrix_ids_artifact(
 | 
						|
        Worker.RESULTS_DIR.value + "/" + ArtifactType.MATRIX_IDS.value
 | 
						|
    )
 | 
						|
    only_get_devices_with_failure = artifact_type == ArtifactType.CRASH_LOG
 | 
						|
    device_names = fetch_device_names(
 | 
						|
        matrix_ids_artifact, only_get_devices_with_failure
 | 
						|
    )
 | 
						|
 | 
						|
    if not device_names:
 | 
						|
        if artifact_type == ArtifactType.CRASH_LOG:
 | 
						|
            logging.info(
 | 
						|
                "No devices with failure outcomes found - skipping crash log collection."
 | 
						|
            )
 | 
						|
            return
 | 
						|
        else:
 | 
						|
            exit_with_error("Could not find any device in matrix file.")
 | 
						|
 | 
						|
    root_gcs_path = get_gcs_path(matrix_ids_artifact)
 | 
						|
    if not root_gcs_path:
 | 
						|
        exit_with_error("Could not find root GCS path in matrix file.")
 | 
						|
 | 
						|
    if artifact_type == ArtifactType.BASELINE_PROFILE:
 | 
						|
        return process_baseline_profile_artifact(root_gcs_path, device_names)
 | 
						|
    elif artifact_type == ArtifactType.MACROBENCHMARK:
 | 
						|
        return process_macrobenchmark_artifact(root_gcs_path, device_names)
 | 
						|
    else:
 | 
						|
        return process_crash_artifacts(root_gcs_path, device_names)
 | 
						|
 | 
						|
 | 
						|
def process_baseline_profile_artifact(root_gcs_path, device_names):
 | 
						|
    device = device_names[0]
 | 
						|
    artifact = fetch_artifacts(
 | 
						|
        root_gcs_path, device, ArtifactType.BASELINE_PROFILE.value
 | 
						|
    )[0]
 | 
						|
    if not artifact:
 | 
						|
        exit_with_error(f"No artifacts found for device: {device}")
 | 
						|
 | 
						|
    gsutil_cp(artifact, Worker.BASELINE_PROFILE_DEST.value)
 | 
						|
 | 
						|
 | 
						|
def process_macrobenchmark_artifact(root_gcs_path, device_names):
 | 
						|
    device = device_names[0]
 | 
						|
    artifact = fetch_artifacts(
 | 
						|
        root_gcs_path, device, ArtifactType.MACROBENCHMARK.value
 | 
						|
    )[0]
 | 
						|
    if not artifact:
 | 
						|
        exit_with_error(f"No artifacts found for device: {device}")
 | 
						|
 | 
						|
    gsutil_cp(artifact, Worker.MACROBENCHMARK_DEST.value)
 | 
						|
 | 
						|
 | 
						|
def process_crash_artifacts(root_gcs_path, failed_device_names):
 | 
						|
    crashes_reported = 0
 | 
						|
    for device in failed_device_names:
 | 
						|
        artifacts = fetch_artifacts(root_gcs_path, device, ArtifactType.CRASH_LOG.value)
 | 
						|
        if not artifacts:
 | 
						|
            logging.info(f"No artifacts found for device: {device}")
 | 
						|
            continue
 | 
						|
 | 
						|
        for artifact in artifacts:
 | 
						|
            gsutil_cp(artifact, Worker.RESULTS_DIR.value)
 | 
						|
            crashes_reported += parse_crash_log(
 | 
						|
                os.path.join(Worker.RESULTS_DIR.value, os.path.basename(artifact))
 | 
						|
            )
 | 
						|
 | 
						|
    return crashes_reported
 | 
						|
 | 
						|
 | 
						|
def exit_with_error(message):
 | 
						|
    logging.error(message)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    setup_logging()
 | 
						|
    check_gsutil_availability()
 | 
						|
 | 
						|
    if len(sys.argv) < 2:
 | 
						|
        logging.error("Usage: python script_name.py <artifact_type>")
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
    artifact_type_arg = sys.argv[1]
 | 
						|
    if artifact_type_arg == "baseline_profile":
 | 
						|
        process_artifacts(ArtifactType.BASELINE_PROFILE)
 | 
						|
    elif artifact_type_arg == "macrobenchmark":
 | 
						|
        process_artifacts(ArtifactType.MACROBENCHMARK)
 | 
						|
    elif artifact_type_arg == "crash_log":
 | 
						|
        process_artifacts(ArtifactType.CRASH_LOG)
 | 
						|
    else:
 | 
						|
        logging.error("Invalid artifact type. Use 'baseline_profile' or 'crash_log'.")
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    sys.exit(main())
 |