blob: e395490d055399ce184f93ae8b8c3c098b46b5d1 [file] [log] [blame] [edit]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Implementation of the `report` subcommand."""
import logging
import os
import re
import subprocess
from typing import Optional
# pylint: disable=import-error
from cros_camera_tracing import utils
class TraceProcessorEnv:
"""Runtime environment for Perfetto trace processor."""
# Local directory hosting the camera metrics definitions.
METRICS_DIR = "metrics"
# Archive of protofiles under METRICS_DIR
PROTO_ARCHIVE = "protos.tar.gz"
# Path to the built-in trace processors.
TRACE_PROCESSOR_IN_SDK = "/usr/bin/trace_processor_shell"
TRACE_PROCESSOR_ON_DUT = "/usr/local/bin/trace_processor_shell"
def __init__(self, trace_processor_path: Optional[str]):
self.trace_processor_path = None
if trace_processor_path:
self.trace_processor_path = trace_processor_path
elif utils.is_inside_chroot():
self.trace_processor_path = TraceProcessorEnv.TRACE_PROCESSOR_IN_SDK
elif utils.is_on_dut():
self.trace_processor_path = TraceProcessorEnv.TRACE_PROCESSOR_ON_DUT
if self.trace_processor_path is None or not os.path.exists(
self.trace_processor_path
):
raise RuntimeError("Unable to locate trace processor")
self.metrics_dir = os.path.realpath(
os.path.join(
os.path.dirname(__file__), "..", TraceProcessorEnv.METRICS_DIR
)
)
if utils.is_on_dut():
self.proto_archive = os.path.join(
self.metrics_dir, TraceProcessorEnv.PROTO_ARCHIVE
)
if os.path.exists(self.proto_archive):
subprocess.run(
[
"tar",
"-xzf",
self.proto_archive,
"--directory",
self.metrics_dir,
],
check=True,
)
else:
raise RuntimeError("Unable to find proto files")
def run(self, args):
"""Runs the trace processor."""
cmd = [
self.trace_processor_path,
"--metric-extension",
f"{self.metrics_dir}@/",
"--dev",
"--run-metrics",
args.metrics,
"--metrics-output",
args.metrics_output,
args.input_file,
]
if args.interactive:
cmd += ["--interactive"]
output = subprocess.run(
cmd,
check=True,
encoding="utf-8",
stderr=None if args.interactive else subprocess.DEVNULL,
stdout=None if args.interactive else subprocess.PIPE,
)
if not args.interactive:
logging.info("Computed metrics:\n\n%s", output.stdout)
if args.output_file:
with open(args.output_file, "w", encoding="utf-8") as f:
f.write(output.stdout)
def list_metrics(self):
"""Lists the camera metrics defined in the camera metrics dir."""
# Metric has proto definition like:
# extend TraceMetrics {
# optional <Type> <Name> = <Id>;
# }
metric_def_re = re.compile(
r"extend\s+TraceMetrics\s*{"
r"\s*optional\s+(?P<type>\w+)\s+(?P<name>\w+)\s*=\s*(?P<id>\d+);"
r"\s*}"
)
all_metrics = {}
protos_dir = os.path.join(self.metrics_dir, "protos")
for proto_file in os.listdir(protos_dir):
with open(
os.path.join(protos_dir, proto_file), encoding="utf-8"
) as f:
m = metric_def_re.search(f.read())
if m is None:
continue
mid = int(m.group("id"))
all_metrics[mid] = m.group("name")
logging.info(
"List of camera metrics:\n%s",
"\n".join(
[
f"{k:5d}: {all_metrics[k]}"
for k in sorted(all_metrics.keys())
]
),
)
def set_up_subcommand_parser(subparsers):
"""Sets up subcommand parser for the `report` subcommand."""
report_parser = subparsers.add_parser(
"report",
description=(
"Report parsed info from a recorded trace, such as metrics. Works "
"only inside the CrOS SDK chroot for the time being due to "
"dependency on the Tast trace processor binary"
),
help="Report parsed info from recorded trace",
)
report_parser.add_argument(
"-i",
"--input_file",
type=str,
default="/tmp/perfetto-trace",
help="The input recorded trace file to parse (default=%(default)s)",
)
report_parser.add_argument(
"--trace_processor_path",
type=str,
default=None,
help=(
"Path to the Perfetto trace_processor binary; if not specified, "
"will use the built-in trace processor inside the SDK or on the "
"DUT (default=%(default)s)"
),
)
report_parser.add_argument(
"--metrics",
type=str,
default=None,
help="The list of comma separated metrics to compute",
)
report_parser.add_argument(
"--list_metrics",
action="store_true",
default=False,
help="List the available metrics that can be computed",
)
report_parser.add_argument(
"--interactive",
action="store_true",
default=False,
help=(
"Enters the interactive mode of the trace processor; used mainly "
"for development and debugging metrics SQL definitions"
),
)
report_parser.add_argument(
"--metrics_output",
type=str,
default="text",
choices=["binary", "text", "json"],
help="The metrics output format of the trace processor",
)
report_parser.add_argument(
"--output_file",
type=str,
default="",
help="Path the computed metrics will be written to",
)
def execute_subcommand(args):
"""Executes the `report` subcommand."""
env = TraceProcessorEnv(args.trace_processor_path)
if args.list_metrics:
env.list_metrics()
if args.metrics:
logging.info("Using trace processor: %s", env.trace_processor_path)
env.run(args)