#!/usr/bin/env python
#
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import subprocess
import argparse
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(
os.path.join(SCRIPT_DIR, os.pardir, os.pardir, os.pardir, os.pardir,
'third_party', 'perfetto', 'python'))
sys.path.append(
os.path.join(SCRIPT_DIR, os.pardir, os.pardir, os.pardir, 'mac', 'power',
'protos', 'third_party', 'pprof'))
PERFETTO_DIR = os.path.normpath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
os.pardir, 'third_party', 'perfetto'))
TRACECONV_PATH = os.path.join(PERFETTO_DIR, 'tools', 'traceconv')
ARTIFACTS_DIR = os.path.normpath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, 'artifacts'))
_DESCRIPTION = ("""
Symbolizes and deobfuscates the traces generated by a run of the benchmark for
the contrib power stories. This script will automatically find all the runs and
only generate results if needed.
""")
_USAGE = """
process_results --out-dir=<out/Release>
"""
_POWER_STORIES = ['power_scroll_top']
def _CreateArgumentParser():
parser = argparse.ArgumentParser(description=_DESCRIPTION, usage=_USAGE)
parser.add_argument('--out-dir', action='store', dest='out_dir')
parser.add_argument('-v',
'--verbose',
action='count',
dest='verbosity',
default=0,
help='Increase verbosity level (repeat as needed)')
return parser
def _GetTraceEventProto(story_results_path):
for f in os.scandir(os.path.join(story_results_path, 'trace', 'traceEvents')):
if not f.is_file():
continue
if f.name.endswith('.pb') and f.name.count('.') == 1:
return f.path
return None
def _GetProcessNameMapping(trace_path):
# We can not have it at the top level as perfetto is not checked out in some
# bots and this file is loaded in some tests to look for benchmarks
from perfetto.trace_processor import TraceProcessor # pylint: disable=import-error,import-outside-toplevel
mappings = {}
logging.info("Getting prcess mappings from: %s ",
trace_path[len(ARTIFACTS_DIR) + 1:])
tp = TraceProcessor(file_path=trace_path)
res = tp.query('SELECT pid, name FROM process')
for r in res:
mappings[r.pid] = (r.name if r.name is not None else 'null')
return mappings
def _AddProcessNameToProfile(input_path, output_path, process_name):
# We can not have it at the top level as perfetto is not checked out in some
# bots and this file is loaded in some tests to look for benchmarks
import profile_pb2 # pylint: disable=import-error,import-outside-toplevel
profile = profile_pb2.Profile()
with open(input_path, "rb") as f:
profile.ParseFromString(f.read())
process_name_id = len(profile.string_table)
profile.string_table.append(process_name)
new_function = profile_pb2.Function()
new_function.id = max([a.id for a in profile.function]) + 1
new_function.name = process_name_id
profile.function.append(new_function)
new_location = profile_pb2.Location()
new_location.id = max([a.id for a in profile.location]) + 1
new_line = new_location.line.add()
new_line.function_id = new_function.id
profile.location.append(new_location)
for sample in profile.sample:
sample.location_id.append(new_location.id)
with open(output_path, "wb") as f:
f.write(profile.SerializeToString())
def _ProcessProfile(input_path, output_path, trace_path):
logging.info('Processing profile: %s', input_path[len(ARTIFACTS_DIR) + 1:])
pid_map = _GetProcessNameMapping(trace_path)
if not os.path.exists(output_path):
os.mkdir(output_path)
for p in os.scandir(input_path):
processed_profile_path = os.path.join(output_path, p.name)
parts = p.name.split('.')
if (len(parts) != 5 or not parts[3].isnumeric() or len(parts[3]) == 0):
logging.warning('Unexpected profile file: %s', p.name)
continue
pid = int(parts[3])
_AddProcessNameToProfile(p.path, processed_profile_path,
pid_map.get(pid, '(unknown)'))
def _RunTraceConv(command, build_out_path):
traceconv_env = os.environ.copy()
traceconv_env['PERFETTO_SYMBOLIZER_MODE'] = 'index'
traceconv_env['PERFETTO_BINARY_PATH'] = os.path.join(build_out_path,
'lib.unstripped')
traceconv_env['PERFETTO_PROGUARD_MAP'] = 'org.chromium.*=' + os.path.join(
build_out_path, 'apks', 'ChromePublic.apk.mapping')
return subprocess.run([TRACECONV_PATH] + command,
env=traceconv_env,
check=True,
capture_output=True)
def _Symbolize(input_path, output_path, build_out_path):
logging.info('Symbolizing: %s', input_path[len(ARTIFACTS_DIR) + 1:])
cmd = ['symbolize', input_path, output_path]
return _RunTraceConv(cmd, build_out_path)
def _Deobfuscate(input_path, output_path, build_out_path):
logging.info('Deobfuscating: %s', input_path[len(ARTIFACTS_DIR) + 1:])
cmd = ['deobfuscate', input_path, output_path]
return _RunTraceConv(cmd, build_out_path)
def _Profile(input_path, output_path, build_out_path):
logging.info('Profiling: %s', input_path[len(ARTIFACTS_DIR) + 1:])
cmd = ['profile', "--perf", input_path]
run = _RunTraceConv(cmd, build_out_path)
if len(run.stdout) == 0:
os.mkdir(output_path)
return
profile_out_dir = run.stdout.split()[-1]
os.rename(profile_out_dir, output_path)
def _Cat(input_paths, out_path):
logging.info('Concatenating: %s', ", ".join(input_paths))
with open(out_path, "wb") as out_f:
for input_path in input_paths:
with open(input_path, "rb") as in_f:
out_f.write(in_f.read())
def _ProcessStory(story_results_path, build_out_path):
if not _HasTraceEvents(story_results_path):
logging.info('Skipping results with no traceEvents: %s',
story_results_path[len(ARTIFACTS_DIR) + 1:])
return
logging.info('Processing results: %s',
story_results_path[len(ARTIFACTS_DIR) + 1:])
traceconv_env = os.environ.copy()
traceconv_env['PERFETTO_SYMBOLIZER_MODE'] = 'index'
traceconv_env['PERFETTO_BINARY_PATH'] = os.path.join(build_out_path,
'lib.unstripped')
traceconv_env['PERFETTO_PROGUARD_MAP'] = 'org.chromium.*=' + os.path.join(
build_out_path, 'apks', 'ChromePublic.apk.mapping')
trace_file = _GetTraceEventProto(story_results_path)
if trace_file is None:
raise Exception('Did not find trace file in %s' % story_results_path)
base_path = os.path.splitext(trace_file)[0]
symbols_path = base_path + '.symbols.pb'
mappings_path = base_path + '.mappings.pb'
combined_path = base_path + '.combined.pb'
profile_path = os.path.join(os.path.dirname(combined_path), 'profile')
processed_profile_path = os.path.join(os.path.dirname(combined_path),
'processed_profile')
if not os.path.isfile(symbols_path):
_Symbolize(trace_file, symbols_path, build_out_path)
if not os.path.isfile(mappings_path):
_Deobfuscate(trace_file, mappings_path, build_out_path)
if not os.path.isfile(combined_path):
_Cat([trace_file, symbols_path, mappings_path], combined_path)
if not os.path.isdir(profile_path):
_Profile(combined_path, profile_path, build_out_path)
if not os.path.isdir(processed_profile_path):
_ProcessProfile(profile_path, processed_profile_path, trace_file)
def _IsPowerStory(name):
return name.startswith("contrib_power_mobile")
def _IterateRunPaths():
for r in os.scandir(ARTIFACTS_DIR):
if not r.is_dir() or not r.name.startswith('run'):
continue
yield r.path
def _IterateStoryPaths(run_path):
for s in os.scandir(run_path):
if s.is_dir() and _IsPowerStory(s.name):
yield s.path
def _HasTraceEvents(story_results_path):
return os.path.isdir(os.path.join(story_results_path, 'trace', 'traceEvents'))
def _ProcessRun(run_path):
combined_profile_path = os.path.join(run_path, 'combined_profile.pb.gz')
if os.path.exists(combined_profile_path):
return
logging.info('Generating combined profile: %s',
combined_profile_path[len(ARTIFACTS_DIR) + 1:])
profiles = []
for s in _IterateStoryPaths(run_path):
processed_profile_path = os.path.join(s, 'trace', 'traceEvents',
'processed_profile')
if not os.path.isdir(processed_profile_path):
continue
for p in os.scandir(processed_profile_path):
profiles.append(p.path)
if len(profiles) == 0:
return
subprocess.run(['pprof', '-proto', '-output', combined_profile_path] +
profiles,
check=True,
capture_output=True)
def main():
parser = _CreateArgumentParser()
args = parser.parse_args()
if args.verbosity >= 2:
logging.getLogger().setLevel(logging.DEBUG)
elif args.verbosity == 1:
logging.getLogger().setLevel(logging.INFO)
else:
logging.getLogger().setLevel(logging.WARNING)
if not args.out_dir:
raise Exception('--out_dir missing')
build_out_path = os.path.abspath(os.path.expanduser(args.out_dir))
if not os.path.isdir(build_out_path):
raise Exception('Unable to find out_dir %s' % build_out_path)
for run_path in _IterateRunPaths():
for s in _IterateStoryPaths(run_path):
_ProcessStory(s, build_out_path)
_ProcessRun(run_path)
if __name__ == '__main__':
sys.exit(main())