Skip to content

Instantly share code, notes, and snippets.

@alexeagle
Last active July 14, 2026 18:28
Show Gist options
  • Select an option

  • Save alexeagle/80bef82e244f4711fb3e5d8dafce50f5 to your computer and use it in GitHub Desktop.

Select an option

Save alexeagle/80bef82e244f4711fb3e5d8dafce50f5 to your computer and use it in GitHub Desktop.
Rank a Bazel test target's action inputs by on-disk size, grouped by owning package/repo — find which deps bloat runfiles (slow remote-cache downloads)
#!/usr/bin/env bash
# Rank a Bazel target's output artifacts by size, grouped by owning package/repo,
# so a product engineer sees WHICH deps bloat their test's inputs/runfiles — the
# thing that makes EngFlow remote-cache downloads slow.
#
# Sizes come from the CAS digests in the Build Event Protocol, NOT from disk:
# every BEP file entry has a `bytestream://<host>/blobs/<hash>/<SIZE>` URI, and a
# Bazel Digest is {hash, size_bytes} — the size IS the last path segment. So this
# works under `--remote_download_minimal` (build-without-the-bytes) with zero byte
# downloads and no local stat. (CAS has no "size of hash X" RPC; you read the size
# from the digest wherever Bazel recorded it — here, the BEP.)
#
# Usage: ./bazel-input-weight.sh //pkg:target [extra bazel build flags...]
set -euo pipefail
TARGET="${1:?usage: $0 //pkg:target}"; shift || true
BEP="$(mktemp -t bep.XXXXXX.json)"
# Build (or cache-hit) to emit the BEP. --remote_download_minimal keeps bytes remote;
# we only want the digest metadata, so nothing is fetched.
bazel build "$TARGET" \
--remote_download_minimal \
--build_event_json_file="$BEP" \
"$@" >/dev/null 2>&1 || true # keep going even if some action fails; BEP still has files
# Parse namedSetOfFiles: pull (size, group) from each file's bytestream URI.
# group key = external repo (external/<repo>) else the bazel-out package path.
jq -r '
select(.namedSetOfFiles) | .namedSetOfFiles.files[]?
| .uri as $u
| ($u | capture("/blobs/[0-9a-f]+/(?<sz>[0-9]+)$").sz | tonumber) as $sz
| ((.pathPrefix // []) + [.name] | join("/")) as $p
| (if ($p|test("(^|/)external/")) then ($p | capture("external/(?<r>[^/]+)").r)
elif ($p|test("(^|/)bin/")) then ($p | sub(".*/bin/";"") | split("/")[0:3] | join("/"))
else ($p | split("/")[0:3] | join("/")) end) as $g
| "\($sz)\t\($g)"
' "$BEP" \
| awk -F'\t' '{s[$2]+=$1} END{for(k in s) printf "%d\t%s\n", s[k], k}' \
| sort -rn \
| awk 'BEGIN{print "SIZE\tGROUP"}
{h=$1;u="B"; if(h>1073741824){h/=1073741824;u="G"} else if(h>1048576){h/=1048576;u="M"} else if(h>1024){h/=1024;u="K"}
printf "%.1f%s\t%s\n", h, u, $2}' \
| head -30
rm -f "$BEP"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment