blob: d997312310678fac8689953c6264cef8738c4205 [file] [edit]
#!/usr/bin/env python3
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A script to calculate and breakdown the installed packages size."""
import argparse
import functools
import heapq
import logging
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
from typing import Dict, List, Literal, NoReturn, Optional, Tuple
# Use Dict[] directly instead of TypedDict to make type inference works better
# for .items().
PackageSize = Dict[
Literal["rootfs", "stateful"],
# List of (file path, size in bytes) pairs sorted decreasingly by size.
List[Tuple[str, int]],
]
# TODO(shik): Extract common python functions into a utility module.
@functools.lru_cache(1)
def get_workspace_root() -> Path:
"""Gets the root of tflite workspace."""
root = Path(__file__).resolve().parent.parent
assert root.name == "tflite" and (root / "WORKSPACE.bazel").exists()
logging.debug("root = %s", root)
return root
def shell_join(cmd: List[str]) -> str:
return " ".join(shlex.quote(c) for c in cmd)
def run(args: List[str]) -> int:
logging.debug("$ %s", shell_join(args))
return subprocess.check_call(args, cwd=get_workspace_root())
def check_output(args: List[str]) -> str:
logging.debug("$ %s", shell_join(args))
return subprocess.check_output(args, text=True, cwd=get_workspace_root())
@functools.lru_cache(1)
def is_in_cros_sdk() -> bool:
return Path("/etc/cros_chroot_version").exists()
def human_readble_size(num_bytes: int) -> str:
units = [(2**30, "G"), (2**20, "M"), (2**10, "K")]
scale, unit = next((u for u in units if num_bytes >= u[0]), (1, "B"))
fmt = "%d" if unit == "B" else "%.1f"
return fmt % (num_bytes / scale) + unit
def get_package_size(board: str, package: str) -> PackageSize:
sysroot = Path(f"/build/{board}")
if not sysroot.is_dir():
raise ValueError(
f"{sysroot} not found, "
"please check board name and ensure packages are built"
)
cmd = [
f"equery-{board}",
"files",
"--filter=obj",
package,
]
files = check_output(cmd).splitlines()
package_size: PackageSize = {
"rootfs": [],
"stateful": [],
}
for file in files:
# TODO(shik): Ignore files listed in install_mask.py instead of
# hard-coding common masks here. Reference:
# https://www.chromium.org/chromium-os/developer-library/guides/portage/ebuild-faq/#how-do-i-find-out-the-on-disk-package-size
if re.search(r"^/usr/(include|lib/debug)/|\.(a|c|cc|h|hpp)$", file):
continue
path = sysroot / file.lstrip("/")
size = path.stat().st_size
key = "stateful" if file.startswith("/usr/local/") else "rootfs"
package_size[key].append((file, size))
return package_size
def print_package_size(package_size: PackageSize, top: int) -> None:
for key, sizes in package_size.items():
total = sum(size for _, size in sizes)
print(f" {key}: {human_readble_size(total)}")
largest_files = heapq.nlargest(top, sizes, key=lambda x: x[1])
for file, size in largest_files:
print(" %7s %s" % (human_readble_size(size), file))
def setup_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=__doc__,
)
parser.add_argument(
"--board",
required=True,
help="board name",
)
parser.add_argument(
"--top",
type=int,
default=10,
help="how many largest files to print for each package",
)
parser.add_argument(
"--debug",
action="store_true",
help="enable debug logging",
)
parser.add_argument(
"packages",
metavar="package",
nargs="*",
default=["tensorflow"],
help="package names",
)
return parser
def run_self_in_cros_sdk(argv: List[str]) -> NoReturn:
logging.debug("Rerun self in cros_sdk")
tflite = Path("/mnt/host/source/src/platform/tflite")
script = tflite / Path(__file__).relative_to(get_workspace_root())
cmd = [
"cros_sdk",
"--no-update",
"--",
script,
] + (argv or [])
os.chdir(get_workspace_root())
os.execvp(cmd[0], cmd)
def main(argv: Optional[List[str]] = None) -> Optional[int]:
parser = setup_argument_parser()
args = parser.parse_args(argv)
log_level = logging.DEBUG if args.debug else logging.INFO
log_format = "%(asctime)s - %(levelname)s - %(funcName)s: %(message)s"
logging.basicConfig(level=log_level, format=log_format)
logging.debug("args = %s", args)
if not is_in_cros_sdk():
run_self_in_cros_sdk(argv or [])
for i, pkg in enumerate(args.packages):
package_size = get_package_size(args.board, pkg)
if i > 0:
print()
print(f"{pkg}:")
print_package_size(package_size, args.top)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))