blob: 1b53b30462ea66f2104c31623ef656b3d1215c58 [file] [log] [blame]
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''Generates a budgets configuration file for size checker tool.
This tool converts the old size budget file to the new format as part of RFC-0144 migration plan.
Usage example:
python3 build/assembly/scripts/convert_size_limits.py \
--size_limits out/default/size_checker.json \
--product_config out/default/obj/build/images/fuchsia/fuchsia_product_config.json \
--output out/default/size_budgets.json
The budget configuration is a JSON file used by `ffx assembly size-check` that contains two types
of budgets:
resource_budgets: applies to all the files matched by path across all package sets. It is
used to allocate space for common resources, widely used. Blobs corresponding to the matched
files are no longer charged to any packages. It has the following fields:
name: string, human readable name for this budget.
budget_bytes: integer, number of bytes the matched files have to fit in.
creep_budget_bytes: integer, maximum number of bytes added by a given code change.
paths: list of strings, file name to be matched in the package manifest.
package_set_budgets: size limit that applies to one or more packages. It has the following fields:
name, budget_bytes, creep_budget_bytes: ditto.
packages: list of string, path to package manifests that belongs to this set.
merge: boolean, when true the packages blobs are deduplicate by hash before accounting.
This affects the shared blobs accounting, and is intended to approximate merged packages
such as base_package_manifest.json.
'''
import argparse
import json
import sys
import collections
import os
_UPDATE_PACKAGE_NAME = 'Update Package'
class InvalidInputError(Exception):
pass
def convert_budget_format(component, all_manifests):
"""Converts a component budget to the new budget format.
Args:
component: dictionary, former size checker configuration entry.
all_manifests: [string], list of path to packages manifests.
Returns:
dictionary, new configuration with a name, a maximum size and the list of
packages manifest to fit in the budget.
"""
prefixes = tuple(os.path.join("obj", src) for src in component["src"])
# Finds all package manifest files located bellow the directories
# listed by the component `src` field.
packages = sorted(m for m in all_manifests if m.startswith(prefixes))
return dict(
name=component["component"],
budget_bytes=component["limit"],
creep_budget_bytes=component["creep_limit"],
merge=False,
packages=packages)
def convert_non_blobfs_budget(component):
"""Converts a budget that are not part of blobfs.
Args:
component: dictionary, former non_blobfs size checker configuration entry.
Returns:
dictionary, new configuration with a name, a maximum size and the list of
packages manifest to fit in the budget.
"""
return dict(
name=component["component"],
budget_bytes=component["limit"],
creep_budget_bytes=component["creep_limit"],
merge=False,
packages=[component["blobs_json_path"]])
def make_non_blobfs_budgets(size_limits):
return [
convert_non_blobfs_budget(non_blobfs)
for non_blobfs in size_limits.get("non_blobfs_components", [])
]
def count_packages(budgets, all_manifests):
"""Returns packages that are missing, or present in multiple budgets."""
package_count = collections.Counter(
package for budget in budgets for package in budget["packages"])
more_than_once = [
package for package, count in package_count.most_common() if count > 1
]
zero = [package for package in all_manifests if package_count[package] == 0]
return more_than_once, zero
def make_package_set_budgets(size_limits, product_config):
# Convert each budget to the new format and packages from base and cache.
# Package from system belongs the system budget.
all_manifests = product_config.get("base", []) + product_config.get(
"cache", [])
components = size_limits.get("components", [])
packages_budgets = list(
convert_budget_format(pkg, all_manifests) for pkg in components)
# Verify packages are in exactly one budget.
more_than_once, zero = count_packages(packages_budgets, all_manifests)
# Create a budget for the packages not matched by any other budget.
if "core_limit" in size_limits:
packages_budgets.append(
dict(
name="Core system+services",
budget_bytes=size_limits["core_limit"],
creep_budget_bytes=size_limits["core_creep_limit"],
merge=False,
packages=sorted(zero)))
if more_than_once:
print("ERROR: Package(s) matched by more than one size budget:")
for package in more_than_once:
print(f" - {package}")
raise InvalidInputError() # Exit with an error code.
# Assign all system packages from the product configuration to the
# system package set.
system_budget = next(
(
budget for budget in packages_budgets
if budget["name"] == "/system (drivers and early boot)"), None)
if system_budget:
# De-duplicate blobs by hash accors the packages of this budget in order to
# approximate the package merging that happens with base_package_manifest.
system_budget["merge"] = True
system_budget["packages"] = sorted(product_config.get("system", []))
return sorted(packages_budgets, key=lambda budget: budget["name"])
def make_resources_budgets(size_limits):
budgets = []
if "distributed_shlibs" in size_limits:
budgets.append(
dict(
name="Distributed shared libraries",
paths=sorted(size_limits["distributed_shlibs"]),
budget_bytes=size_limits["distributed_shlibs_limit"],
creep_budget_bytes=size_limits[
"distributed_shlibs_creep_limit"],
))
if "icu_data" in size_limits:
budgets.append(
dict(
name="ICU Data",
paths=sorted(size_limits["icu_data"]),
budget_bytes=size_limits["icu_data_limit"],
creep_budget_bytes=size_limits["icu_data_creep_limit"],
))
return budgets
def main():
parser = argparse.ArgumentParser(
description=
'Converts the former size_checker.go budget file to the new format as part of RFC-0144'
)
parser.add_argument(
'--size-limits', type=argparse.FileType('r'), required=True)
parser.add_argument(
'--image-assembly-config', type=argparse.FileType('r'), required=True)
parser.add_argument('--output', type=argparse.FileType('w'), required=True)
parser.add_argument(
'--non-blobfs-output', type=argparse.FileType('w'), required=True)
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('--blobfs-capacity', type=int, required=True)
args = parser.parse_args()
# Read all input configurations.
size_limits = json.load(args.size_limits)
image_assembly_config = json.load(args.image_assembly_config)
# Format the resulting configuration file.
try:
# Convert all the budget formats.
non_blobfs_budgets = make_non_blobfs_budgets(size_limits)
resource_budgets = make_resources_budgets(size_limits)
package_set_budgets = make_package_set_budgets(
size_limits, image_assembly_config)
output_contents = dict(
resource_budgets=resource_budgets,
package_set_budgets=package_set_budgets)
# Determine the budget for slot A.
if non_blobfs_budgets or resource_budgets or package_set_budgets:
# Find the update package budget.
update_budget = None
for budget in non_blobfs_budgets:
if budget['name'] == _UPDATE_PACKAGE_NAME:
update_budget = budget['budget_bytes']
if update_budget:
budget_for_slot_a = int(
(args.blobfs_capacity - update_budget) / 2)
output_contents['total_budget_bytes'] = budget_for_slot_a
else:
print('Failed to find the update package budget')
return 1
# Ensure the outputfile is closed early.
with args.output as output:
json.dump(output_contents, output, indent=2)
# Non blobfs budgets are written to a separate configuration so that
# they can be evaluated separately.
with args.non_blobfs_output as output:
json.dump(
dict(package_set_budgets=non_blobfs_budgets), output, indent=2)
return 0
except InvalidInputError:
return 1
if __name__ == '__main__':
sys.exit(main())