• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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