1#!/usr/bin/env python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""A tool to print human-readable metrics information regarding the last build. 17 18By default, the consumed file will be $OUT_DIR/soong_build_metrics.pb. You may 19pass in a different file instead using the metrics_file flag. 20""" 21 22import argparse 23import json 24import os 25import subprocess 26import sys 27 28 29class Event(object): 30 """Contains nested event data. 31 32 Fields: 33 name: The short name of this event e.g. the 'b' in an event called a.b. 34 children: Nested events 35 start_time_relative_ns: Time since the epoch that the event started 36 duration_ns: Duration of this event, including time spent in children. 37 """ 38 39 def __init__(self, name): 40 self.name = name 41 self.children = list() 42 self.start_time_relative_ns = 0 43 self.duration_ns = 0 44 45 def get_child(self, name): 46 "Get a child called 'name' or return None" 47 for child in self.children: 48 if child.name == name: 49 return child 50 return None 51 52 def get_or_add_child(self, name): 53 "Get a child called 'name', or if it isn't there, add it and return it." 54 child = self.get_child(name) 55 if not child: 56 child = Event(name) 57 self.children.append(child) 58 return child 59 60 61def _get_proto_output_file(): 62 """Returns the location of the proto file used for analyzing out/soong_build_metrics.pb. 63 64 This corresponds to soong/ui/metrics/metrics_proto/metrics.proto. 65 """ 66 return os.getenv("ANDROID_BUILD_TOP" 67 ) + "/build/soong/ui/metrics/metrics_proto/metrics.proto" 68 69 70def _get_default_output_file(): 71 """Returns the filepath for the build output.""" 72 out_dir = os.getenv("OUT_DIR") 73 if not out_dir: 74 out_dir = "out" 75 build_top = os.getenv("ANDROID_BUILD_TOP") 76 if not build_top: 77 raise Exception( 78 "$ANDROID_BUILD_TOP not found in environment. Have you run lunch?") 79 return os.path.join(build_top, out_dir, "soong_build_metrics.pb") 80 81 82def _make_nested_events(root_event, event): 83 """Splits the event into its '.' separated name parts, and adds Event objects for it to the 84 85 synthetic root_event event. 86 """ 87 node = root_event 88 for sub_event in event["description"].split("."): 89 node = node.get_or_add_child(sub_event) 90 node.start_time_relative_ns = event["start_time_relative_ns"] 91 node.duration_ns = event["real_time"] 92 93 94def _write_events(out, events, parent=None): 95 """Writes the list of events. 96 97 Args: 98 out: The stream to write to 99 events: The list of events to write 100 parent: Prefix parent's name 101 """ 102 for event in events: 103 _write_event(out, event, parent) 104 105 106def _write_event(out, event, parent=None): 107 "Writes an event. See _write_events for args." 108 full_event_name = parent + "." + event.name if parent else event.name 109 out.write( 110 "%(start)9s %(duration)9s %(name)s\n" % { 111 "start": _format_ns(event.start_time_relative_ns), 112 "duration": _format_ns(event.duration_ns), 113 "name": full_event_name, 114 }) 115 _write_events(out, event.children, full_event_name) 116 117 118def _format_ns(duration_ns): 119 "Pretty print duration in nanoseconds" 120 return "%.02fs" % (duration_ns / 1_000_000_000) 121 122 123def _save_file(data, file): 124 f = open(file, "wb") 125 f.write(data) 126 f.close() 127 128 129def main(): 130 # Parse args 131 parser = argparse.ArgumentParser(description="") 132 parser.add_argument( 133 "metrics_file", 134 nargs="?", 135 default=_get_default_output_file(), 136 help="The soong_metrics file created as part of the last build. " + 137 "Defaults to out/soong_build_metrics.pb") 138 parser.add_argument( 139 "--save-proto-output-file", 140 nargs="?", 141 default="", 142 help="(Optional) The file to save the output of the printproto command to." 143 ) 144 args = parser.parse_args() 145 146 # Check the metrics file 147 metrics_file = args.metrics_file 148 if not os.path.exists(metrics_file): 149 raise Exception("File " + metrics_file + " not found. Did you run a build?") 150 151 # Check the proto definition file 152 proto_file = _get_proto_output_file() 153 if not os.path.exists(proto_file): 154 raise Exception( 155 "$ANDROID_BUILD_TOP not found in environment. Have you run lunch?") 156 157 # Load the metrics file from the out dir 158 cmd = r"""printproto --proto2 --raw_protocol_buffer --json \ 159 --json_accuracy_loss_reaction=ignore \ 160 --message=soong_build_metrics.SoongBuildMetrics --multiline \ 161 --proto=""" + proto_file + " " + metrics_file 162 json_out = subprocess.check_output(cmd, shell=True) 163 164 if args.save_proto_output_file != "": 165 _save_file(json_out, args.save_proto_output_file) 166 167 build_output = json.loads(json_out) 168 169 # Bail if there are no events 170 raw_events = build_output.get("events") 171 if not raw_events: 172 print("No events to display") 173 return 174 175 # Update the start times to be based on the first event 176 first_time_ns = min([event["start_time"] for event in raw_events]) 177 for event in raw_events: 178 event["start_time_relative_ns"] = event["start_time"] - first_time_ns 179 180 # Sort by start time so the nesting also is sorted by time 181 raw_events.sort(key=lambda x: x["start_time_relative_ns"]) 182 183 # We don't show this event, so that there doesn't have to be a single top level event 184 fake_root_event = Event("<root>") 185 186 # Convert the flat event list into the tree 187 for event in raw_events: 188 _make_nested_events(fake_root_event, event) 189 190 # Output the results 191 print(" start duration") 192 193 _write_events(sys.stdout, fake_root_event.children) 194 195 196if __name__ == "__main__": 197 main() 198