pw_bloat: GN template to collect binary size report data
This adds a `pw_size_report_aggregation` template to collect JSON data
from several size reports into a single size report file.
Bug: 266717927
Change-Id: I0081fe7f338ddce1283fa7e51a725c50547ae787
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/127042
Reviewed-by: Chris Kenyon <chriskenyon@google.com>
Pigweed-Auto-Submit: Alexei Frolov <frolv@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index b156b09..52a3a3f 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# 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
@@ -171,6 +171,51 @@
}
}
+# Aggregates JSON size report data from several pw_size_report targets into a
+# single output file.
+#
+# Args:
+# deps: List of pw_size_report targets whose data to collect.
+# output: Path to the output JSON file.
+#
+# Example:
+# pw_size_report_aggregation("image_sizes") {
+# deps = [
+# ":app_image_size_report",
+# ":bootloader_image_size_report",
+# ]
+# output = "$root_gen_dir/artifacts/image_sizes.json"
+# }
+#
+template("pw_size_report_aggregation") {
+ assert(defined(invoker.deps) && invoker.deps != [],
+ "pw_size_report_aggregation requires size report dependencies")
+ assert(defined(invoker.output),
+ "pw_size_report_aggregation requires an output file path")
+
+ _input_json_files = []
+
+ foreach(_dep, invoker.deps) {
+ _gen_dir = get_label_info(_dep, "target_gen_dir")
+ _dep_name = get_label_info(_dep, "name")
+ _input_json_files +=
+ [ rebase_path("$_gen_dir/${_dep_name}.binary_sizes.json",
+ root_build_dir) ]
+ }
+
+ pw_python_action(target_name) {
+ script = "$dir_pw_bloat/py/pw_bloat/binary_size_aggregator.py"
+ python_deps = [ "$dir_pw_bloat/py" ]
+ args = [
+ "--output",
+ rebase_path(invoker.output, root_build_dir),
+ ] + _input_json_files
+ outputs = [ invoker.output ]
+ deps = invoker.deps
+ forward_variables_from(invoker, [ "visibility" ])
+ }
+}
+
# Creates a target which runs a size report diff on a set of executables.
#
# Args:
@@ -190,7 +235,7 @@
# Overrides global source_filter argument.
# data_sources: Optional List of datasources from bloaty config file
# Overrides global data_sources argument.
-
+#
#
# Example:
# pw_size_diff("foo_bloat") {
diff --git a/pw_bloat/docs.rst b/pw_bloat/docs.rst
index 88267c9..85441e8 100644
--- a/pw_bloat/docs.rst
+++ b/pw_bloat/docs.rst
@@ -231,6 +231,35 @@
(``pigweed/pw_bloat/bloat.gni``), set the ``pw_bloat_SHOW_SIZE_REPORTS``
build arg to ``true``.
+Collecting size report data
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Each ``pw_size_report`` target outputs a JSON file containing the sizes of all
+top-level labels in the binary. (By default, this represents "segments", i.e.
+ELF program headers.) If a build produces multiple images, it may be useful to
+collect all of their sizes into a single file to provide a snapshot of sizes at
+some point in time --- for example, to display per-commit size deltas through
+CI.
+
+The ``pw_size_report_aggregation`` template is provided to collect multiple size
+reports' data into a single JSON file.
+
+**Arguments**
+
+* ``deps``: List of ``pw_size_report`` targets whose data to collect.
+* ``output``: Path to the output JSON file.
+
+.. code::
+
+ import("$dir_pw_bloat/bloat.gni")
+
+ pw_size_report_aggregation("image_sizes") {
+ deps = [
+ ":app_image_size_report",
+ ":bootloader_image_size_report",
+ ]
+ output = "$root_gen_dir/artifacts/image_sizes.json"
+ }
+
Documentation integration
=========================
Bloat reports are easy to add to documentation files. All ``pw_size_diff``
diff --git a/pw_bloat/py/BUILD.gn b/pw_bloat/py/BUILD.gn
index 67a9598..2f75579 100644
--- a/pw_bloat/py/BUILD.gn
+++ b/pw_bloat/py/BUILD.gn
@@ -25,6 +25,7 @@
sources = [
"pw_bloat/__init__.py",
"pw_bloat/__main__.py",
+ "pw_bloat/binary_size_aggregator.py",
"pw_bloat/bloat.py",
"pw_bloat/bloaty_config.py",
"pw_bloat/label.py",
diff --git a/pw_bloat/py/pw_bloat/binary_size_aggregator.py b/pw_bloat/py/pw_bloat/binary_size_aggregator.py
new file mode 100644
index 0000000..d590c3c
--- /dev/null
+++ b/pw_bloat/py/pw_bloat/binary_size_aggregator.py
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""
+Collects binary size JSON outputs from bloat targets into a single file.
+"""
+
+import argparse
+import json
+import logging
+from pathlib import Path
+import sys
+
+from typing import Dict, List
+
+import pw_cli.log
+
+_LOG = logging.getLogger(__package__)
+
+
+def _parse_args() -> argparse.Namespace:
+ """Parses the script's arguments."""
+
+ parser = argparse.ArgumentParser(__doc__)
+ parser.add_argument(
+ '--output',
+ type=Path,
+ required=True,
+ help='Output JSON file',
+ )
+ parser.add_argument(
+ 'inputs',
+ type=Path,
+ nargs='+',
+ help='Input JSON files',
+ )
+
+ return parser.parse_args()
+
+
+def main(inputs: List[Path], output: Path) -> int:
+ all_data: Dict[str, int] = {}
+
+ for file in inputs:
+ try:
+ all_data |= json.loads(file.read_text())
+ except FileNotFoundError:
+ target_name = file.name.split('.')[0]
+ _LOG.error('')
+ _LOG.error('JSON input file %s does not exist', file)
+ _LOG.error('')
+ _LOG.error(
+ 'Check that the build target "%s" is a pw_size_report template',
+ target_name,
+ )
+ _LOG.error('')
+ return 1
+
+ output.write_text(json.dumps(all_data, sort_keys=True, indent=2))
+ return 0
+
+
+if __name__ == '__main__':
+ pw_cli.log.install()
+ sys.exit(main(**vars(_parse_args())))