blob: e5d8199eb1633aa67a32e91d1ec826a73869b44e [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A tool to print human-readable metrics information regarding the last build.
By default, the consumed file will be $OUT_DIR/soong_build_metrics.pb. You may
pass in a different file instead using the metrics_file flag.
"""
import argparse
import json
import os
import subprocess
import sys
class Event(object):
"""Contains nested event data.
Fields:
name: The short name of this event e.g. the 'b' in an event called a.b.
children: Nested events
start_time_relative_ns: Time since the epoch that the event started
duration_ns: Duration of this event, including time spent in children.
"""
def __init__(self, name):
self.name = name
self.children = list()
self.start_time_relative_ns = 0
self.duration_ns = 0
def get_child(self, name):
"Get a child called 'name' or return None"
for child in self.children:
if child.name == name:
return child
return None
def get_or_add_child(self, name):
"Get a child called 'name', or if it isn't there, add it and return it."
child = self.get_child(name)
if not child:
child = Event(name)
self.children.append(child)
return child
def _get_proto_output_file():
"""Returns the location of the proto file used for analyzing out/soong_build_metrics.pb.
This corresponds to soong/ui/metrics/metrics_proto/metrics.proto.
"""
return os.getenv("ANDROID_BUILD_TOP"
) + "/build/soong/ui/metrics/metrics_proto/metrics.proto"
def _get_default_output_file():
"""Returns the filepath for the build output."""
out_dir = os.getenv("OUT_DIR")
if not out_dir:
out_dir = "out"
build_top = os.getenv("ANDROID_BUILD_TOP")
if not build_top:
raise Exception(
"$ANDROID_BUILD_TOP not found in environment. Have you run lunch?")
return os.path.join(build_top, out_dir, "soong_build_metrics.pb")
def _make_nested_events(root_event, event):
"""Splits the event into its '.' separated name parts, and adds Event objects for it to the
synthetic root_event event.
"""
node = root_event
for sub_event in event["description"].split("."):
node = node.get_or_add_child(sub_event)
node.start_time_relative_ns = event["start_time_relative_ns"]
node.duration_ns = event["real_time"]
def _write_events(out, events, parent=None):
"""Writes the list of events.
Args:
out: The stream to write to
events: The list of events to write
parent: Prefix parent's name
"""
for event in events:
_write_event(out, event, parent)
def _write_event(out, event, parent=None):
"Writes an event. See _write_events for args."
full_event_name = parent + "." + event.name if parent else event.name
out.write(
"%(start)9s %(duration)9s %(name)s\n" % {
"start": _format_ns(event.start_time_relative_ns),
"duration": _format_ns(event.duration_ns),
"name": full_event_name,
})
_write_events(out, event.children, full_event_name)
def _format_ns(duration_ns):
"Pretty print duration in nanoseconds"
return "%.02fs" % (duration_ns / 1_000_000_000)
def _save_file(data, file):
f = open(file, "wb")
f.write(data)
f.close()
def main():
# Parse args
parser = argparse.ArgumentParser(description="")
parser.add_argument(
"metrics_file",
nargs="?",
default=_get_default_output_file(),
help="The soong_metrics file created as part of the last build. " +
"Defaults to out/soong_build_metrics.pb")
parser.add_argument(
"--save-proto-output-file",
nargs="?",
default="",
help="(Optional) The file to save the output of the printproto command to."
)
args = parser.parse_args()
# Check the metrics file
metrics_file = args.metrics_file
if not os.path.exists(metrics_file):
raise Exception("File " + metrics_file + " not found. Did you run a build?")
# Check the proto definition file
proto_file = _get_proto_output_file()
if not os.path.exists(proto_file):
raise Exception(
"$ANDROID_BUILD_TOP not found in environment. Have you run lunch?")
# Load the metrics file from the out dir
cmd = r"""printproto --proto2 --raw_protocol_buffer --json \
--json_accuracy_loss_reaction=ignore \
--message=soong_build_metrics.SoongBuildMetrics --multiline \
--proto=""" + proto_file + " " + metrics_file
json_out = subprocess.check_output(cmd, shell=True)
if args.save_proto_output_file != "":
_save_file(json_out, args.save_proto_output_file)
build_output = json.loads(json_out)
# Bail if there are no events
raw_events = build_output.get("events")
if not raw_events:
print("No events to display")
return
# Update the start times to be based on the first event
first_time_ns = min([event["start_time"] for event in raw_events])
for event in raw_events:
event["start_time_relative_ns"] = event["start_time"] - first_time_ns
# Sort by start time so the nesting also is sorted by time
raw_events.sort(key=lambda x: x["start_time_relative_ns"])
# We don't show this event, so that there doesn't have to be a single top level event
fake_root_event = Event("<root>")
# Convert the flat event list into the tree
for event in raw_events:
_make_nested_events(fake_root_event, event)
# Output the results
print(" start duration")
_write_events(sys.stdout, fake_root_event.children)
if __name__ == "__main__":
main()