pkg_size: create a simple CLI tool to dump a system size report

pkg_size dumps the sizes of the packages contained within a built root.
It will also push gauge data (package size) per package and per
invocation (total root size) to the build api metrics append-only queue
via append_metrics_log. This data is to be used for build metrics trend
reporting.

BUG=chromium:1000449
TEST=cros_sdk -- '$HOME/trunk/chromite/run_tests'

Cq-Depend: chromium:1834120
Change-Id: I735057f9bcfe4de367a8df888e76a09836e371d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1801037
Tested-by: Will Bradley <wbbradley@chromium.org>
Commit-Queue: Will Bradley <wbbradley@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/scripts/emit_metric.py b/scripts/emit_metric.py
index 9e58dba..928365c 100644
--- a/scripts/emit_metric.py
+++ b/scripts/emit_metric.py
@@ -14,16 +14,19 @@
 def main(argv):
   """Emit a metric event."""
   parser = commandline.ArgumentParser(description=__doc__)
-  parser.add_argument('op', choices=metrics.VALID_OPS,
+  parser.add_argument('op', choices=sorted(metrics.VALID_OPS),
                       help='Which metric event operator to emit.')
   parser.add_argument('name',
                       help='The name of the metric event as you would like it '
                            'to appear downstream in data stores.')
-  parser.add_argument('key', nargs='?',
-                      help='A unique key for this invocation to ensure that '
-                           'start and stop timers can be matched.')
+  parser.add_argument('arg', nargs='?',
+                      help='An accessory argument dependent upon the "op".')
   opts = parser.parse_args(argv)
 
+  if opts.arg and not metrics.OP_EXPECTS_ARG[opts.op]:
+    # We do not expect to get an |arg| for this |op|.
+    parser.error('Unexpected arg "%s" given for op "%s"' % (opts.arg,
+                                                            opts.op))
+
   timestamp = metrics.current_milli_time()
-  key = opts.key or opts.name
-  metrics.append_metrics_log(timestamp, opts.name, opts.op, key=key)
+  metrics.append_metrics_log(timestamp, opts.name, opts.op, arg=opts.arg)
diff --git a/scripts/pkg_size b/scripts/pkg_size
new file mode 120000
index 0000000..b7045c5
--- /dev/null
+++ b/scripts/pkg_size
@@ -0,0 +1 @@
+wrapper.py
\ No newline at end of file
diff --git a/scripts/pkg_size.py b/scripts/pkg_size.py
new file mode 100644
index 0000000..1a7e845
--- /dev/null
+++ b/scripts/pkg_size.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""The Package Size Reporting CLI entry point."""
+
+from __future__ import print_function
+
+import json
+
+from chromite.lib import commandline
+from chromite.lib import portage_util
+from chromite.utils import metrics
+
+
+def _get_parser():
+  """Create an argument parser for this script."""
+  parser = commandline.ArgumentParser(description=__doc__)
+  parser.add_argument('--root', required=True, type='path',
+                      help='Specify the rootfs to investigate.')
+  parser.add_argument('--image-type',
+                      help='Specify the type of image being investigated. '
+                           'e.g. [base, dev, test]')
+  parser.add_argument('--partition-name',
+                      help='Specify the partition name. '
+                           'e.g. [rootfs, stateful]')
+  parser.add_argument('packages', nargs='*',
+                      help='Names of packages to investigate. Must be '
+                           'specified as category/package-version.')
+  return parser
+
+
+def generate_package_size_report(db, root, image_type, partition_name,
+                                 installed_packages):
+  """Collect package sizes and generate a report."""
+  results = {}
+  total_size = 0
+  package_sizes = portage_util.GeneratePackageSizes(db, root,
+                                                    installed_packages)
+  timestamp = metrics.current_milli_time()
+  for package_cpv, size in package_sizes:
+    results[package_cpv] = size
+    metrics.append_metrics_log(timestamp,
+                               'package_size.%s.%s.%s' % (image_type,
+                                                          partition_name,
+                                                          package_cpv),
+                               metrics.OP_GAUGE,
+                               arg=size)
+    total_size += size
+
+  metrics.append_metrics_log(timestamp,
+                             'total_size.%s.%s' % (image_type, partition_name),
+                             metrics.OP_GAUGE,
+                             arg=total_size)
+  return {'root': root, 'package_sizes': results, 'total_size': total_size}
+
+
+def main(argv):
+  """Find and report approximate size info for a particular built package."""
+  commandline.RunInsideChroot()
+
+  parser = _get_parser()
+  opts = parser.parse_args(argv)
+  opts.Freeze()
+
+  db = portage_util.PortageDB(root=opts.root)
+
+  if opts.packages:
+    installed_packages = portage_util.GenerateInstalledPackages(db, opts.root,
+                                                                opts.packages)
+  else:
+    installed_packages = db.InstalledPackages()
+
+  results = generate_package_size_report(db, opts.root, opts.image_type,
+                                         opts.partition_name,
+                                         installed_packages)
+  print(json.dumps(results))