198 lines
5.9 KiB
Python
198 lines
5.9 KiB
Python
|
|
#!/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()
|