#!/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("") # 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()