|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# mcpp#874 re-measure: the areas of the issue's tables, read from a workspace's |
| 3 | +# target/ and the global build cache after `mcpp test --workspace`. It prints |
| 4 | +# and never asserts. |
| 5 | +# |
| 6 | +# 874-measure.py WORKSPACE SINCE_EPOCH [LABEL] |
| 7 | +# 874-measure.py --snapshot WORKSPACE > lines.json |
| 8 | +# |
| 9 | +# With NINJA_BASE=lines.json, the edges of a run are the lines its .ninja_log |
| 10 | +# files gained since the snapshot (an edge whose output restat kept counts too). |
| 11 | +import collections, json, os, subprocess, sys, time |
| 12 | + |
| 13 | +def ninja_logs(target): |
| 14 | + return [os.path.join(r, ".ninja_log") for r, _, fs in os.walk(target) if ".ninja_log" in fs] |
| 15 | + |
| 16 | +if sys.argv[1] == "--snapshot": |
| 17 | + t = os.path.join(sys.argv[2], "target") |
| 18 | + print(json.dumps({os.path.relpath(l, t): sum(1 for _ in open(l, errors="replace")) for l in ninja_logs(t)})) |
| 19 | + sys.exit(0) |
| 20 | + |
| 21 | +ws, since = sys.argv[1], float(sys.argv[2]) |
| 22 | +base = json.load(open(os.environ["NINJA_BASE"])) if os.environ.get("NINJA_BASE") else None |
| 23 | +label = sys.argv[3] if len(sys.argv) > 3 else "run" |
| 24 | +target = os.path.join(ws, "target") |
| 25 | +home = os.environ.get("MCPP_HOME") or os.path.expanduser("~/.mcpp") |
| 26 | +out = [] |
| 27 | +def say(s=""): out.append(s); print(s) |
| 28 | + |
| 29 | +def du(p): |
| 30 | + total = 0 |
| 31 | + seen = set() |
| 32 | + for root, _, files in os.walk(p): |
| 33 | + for f in files: |
| 34 | + try: st = os.lstat(os.path.join(root, f)) |
| 35 | + except OSError: continue |
| 36 | + if (st.st_dev, st.st_ino) in seen: continue |
| 37 | + seen.add((st.st_dev, st.st_ino)); total += st.st_blocks * 512 |
| 38 | + return total |
| 39 | +def gb(n): return f"{n / 2**30:.2f} GiB" if n >= 2**30 else f"{n / 2**20:.0f} MiB" |
| 40 | + |
| 41 | +def sections(p): |
| 42 | + """(name, size, flags) of each section; flags hold C when compressed.""" |
| 43 | + rows = [] |
| 44 | + for l in subprocess.run(["readelf", "-S", "-W", p], capture_output=True, text=True).stdout.splitlines(): |
| 45 | + if "]" not in l or l.lstrip().startswith("[Nr]"): continue |
| 46 | + t = l.split("]", 1)[1].split() |
| 47 | + if len(t) >= 9: rows.append((t[0], int(t[4], 16), t[6] if len(t) >= 10 else "")) |
| 48 | + return rows |
| 49 | + |
| 50 | +def is_elf(p): |
| 51 | + try: |
| 52 | + with open(p, "rb") as f: return f.read(4) == b"\x7fELF" |
| 53 | + except OSError: return False |
| 54 | + |
| 55 | +say(f"## {label}") |
| 56 | +say() |
| 57 | +say("### 1. Areas of target/ (allocated bytes, hard links counted once)") |
| 58 | +say() |
| 59 | +say("| Where | Size |") |
| 60 | +say("|---|---:|") |
| 61 | +fps = [] |
| 62 | +for triple in sorted(os.listdir(target)) if os.path.isdir(target) else []: |
| 63 | + tdir = os.path.join(target, triple) |
| 64 | + if not os.path.isdir(tdir) or triple.startswith("."): continue |
| 65 | + for fp in sorted(os.listdir(tdir)): |
| 66 | + d = os.path.join(tdir, fp) |
| 67 | + if not os.path.isdir(d): continue |
| 68 | + fps.append(d) |
| 69 | + for area in ("obj", "gcm.cache", "pcm.cache", "bin", "lib"): |
| 70 | + if os.path.isdir(os.path.join(d, area)): |
| 71 | + say(f"| `{triple}/{fp[:8]}…/{area}/` | {gb(du(os.path.join(d, area)))} |") |
| 72 | +for area in ("host", "out"): |
| 73 | + p = os.path.join(target, ".build-mcpp", area) |
| 74 | + if os.path.isdir(p): say(f"| `target/.build-mcpp/{area}/` | {gb(du(p))} |") |
| 75 | +say(f"| **`target/` in all** | **{gb(du(target))}** |") |
| 76 | +say() |
| 77 | + |
| 78 | +say("### 2. bin/") |
| 79 | +say() |
| 80 | +for d in fps: |
| 81 | + b = os.path.join(d, "bin") |
| 82 | + if not os.path.isdir(b): continue |
| 83 | + tests, archives, shared, inodes = [], [], [], {} |
| 84 | + for root, _, files in os.walk(b): |
| 85 | + for f in files: |
| 86 | + p = os.path.join(root, f) |
| 87 | + try: st = os.lstat(p) |
| 88 | + except OSError: continue |
| 89 | + if f.endswith(".a"): archives.append((p, st)) |
| 90 | + elif ".so" in f and not f.endswith(".json"): shared.append((p, st)) |
| 91 | + elif os.access(p, os.X_OK) and is_elf(p) and ("test" in f or "/tests/" in p): tests.append((p, st)) |
| 92 | + say(f"`{os.path.relpath(b, ws)}`") |
| 93 | + say() |
| 94 | + if tests: |
| 95 | + sizes = sorted(st.st_size for _, st in tests) |
| 96 | + compressed = 0 |
| 97 | + sample = max(tests, key=lambda t: t[1].st_size)[0] |
| 98 | + for p, _ in tests[:400]: |
| 99 | + if any(n == ".debug_info" and "C" in fl or n.startswith(".zdebug") for n, _, fl in sections(p)): |
| 100 | + compressed += 1 |
| 101 | + say(f"- test programs: {len(tests)}, {gb(sum(sizes))}; median {gb(sizes[len(sizes)//2])}, largest {gb(sizes[-1])} (`{os.path.basename(sample)}`); compressed debug sections in {compressed} of {min(len(tests), 400)} read") |
| 102 | + dbg = [(n, z, fl) for n, z, fl in sections(sample) if n.startswith((".debug_", ".zdebug"))] |
| 103 | + say(f" - `{os.path.basename(sample)}`: debug sections {gb(sum(z for _, z, _ in dbg))} of {gb(os.path.getsize(sample))}" + (", compressed" if any("C" in fl for _, _, fl in dbg) else "")) |
| 104 | + if archives: |
| 105 | + members, distinct, thin = 0, set(), 0 |
| 106 | + for p, st in archives: |
| 107 | + with open(p, "rb") as f: thin += f.read(8) == b"!<thin>\n" |
| 108 | + names = subprocess.run(["ar", "t", p], capture_output=True, text=True).stdout.split() |
| 109 | + members += len(names); distinct.update(names) |
| 110 | + big = sorted(archives, key=lambda a: -a[1].st_size)[:4] |
| 111 | + say(f"- member archives: {len(archives)}, {gb(sum(st.st_blocks * 512 for _, st in archives))} allocated; {members} members for {len(distinct)} distinct names; thin: {thin}") |
| 112 | + say(" - largest: " + ", ".join(f"`{os.path.basename(p)}` {gb(st.st_size)}" for p, st in big)) |
| 113 | + if shared: |
| 114 | + uniq = {(st.st_dev, st.st_ino): st for _, st in shared} |
| 115 | + dirs = {os.path.dirname(p) for p, _ in shared} |
| 116 | + say(f"- staged shared objects: {len(shared)} files in {len(dirs)} directories, {len(uniq)} distinct inodes, {gb(sum(st.st_size for st in uniq.values()))} distinct, {gb(sum(st.st_size for _, st in shared))} as named") |
| 117 | + say() |
| 118 | + |
| 119 | +say("### 3. Build programs") |
| 120 | +say() |
| 121 | +host = os.path.join(target, ".build-mcpp", "host") |
| 122 | +if os.path.isdir(host): |
| 123 | + nested = [r for r, ds, _ in os.walk(host) if r.endswith(os.path.join("target", ".build-mcpp", "host"))] |
| 124 | + progs = [] |
| 125 | + for root, _, files in os.walk(host): |
| 126 | + for f in files: |
| 127 | + if f in ("build-program", "build.mcpp.bin") or f.startswith("build-program"): |
| 128 | + p = os.path.join(root, f) |
| 129 | + if is_elf(p): progs.append((os.path.relpath(p, host), os.path.getsize(p))) |
| 130 | + say(f"- groups under `target/.build-mcpp/host/`: {len([x for x in os.listdir(host) if os.path.isdir(os.path.join(host, x))])}") |
| 131 | + say(f"- nested `target/.build-mcpp/host` inside a group: {len(nested)}" + ("".join(f"\n - `{os.path.relpath(n, target)}`" for n in nested[:5]))) |
| 132 | + say(f"- build-program executables: {len(progs)}, {gb(sum(s for _, s in progs))}") |
| 133 | +outd = os.path.join(target, ".build-mcpp", "out") |
| 134 | +if os.path.isdir(outd): |
| 135 | + say(f"- generated-source trees under `target/.build-mcpp/out/`: {len(os.listdir(outd))}") |
| 136 | +say() |
| 137 | + |
| 138 | +say("### 4. Edges of this run (.ninja_log)") |
| 139 | +say() |
| 140 | +count, ms, hashes = collections.Counter(), collections.Counter(), collections.defaultdict(set) |
| 141 | +logs = ninja_logs(target) |
| 142 | +for log in logs: |
| 143 | + lines = open(log, errors="replace").read().split("\n") |
| 144 | + skip = base.get(os.path.relpath(log, target), 0) if base is not None else 1 |
| 145 | + for line in lines[max(skip, 1):]: |
| 146 | + if line.count("\t") != 4: continue |
| 147 | + start, end, mtime, o, h = line.split("\t") |
| 148 | + t = int(mtime) / 1e9 if int(mtime) > 1e12 else int(mtime) |
| 149 | + if base is not None or t >= since: |
| 150 | + key = os.path.join(os.path.dirname(log), o) |
| 151 | + count[key] += 1; ms[key] += int(end) - int(start); hashes[key].add(h) |
| 152 | +again = [o for o, c in count.items() if c > 1] |
| 153 | +say(f"- logs: {len(logs)}; edges ran: {sum(count.values())} for {len(count)} distinct outputs") |
| 154 | +say(f"- outputs built more than once: {len(again)}; repeated minutes: {round(sum(ms[o] * (count[o] - 1) / count[o] for o in again) / 60000, 1)} of {round(sum(ms.values()) / 60000, 1)}") |
| 155 | +for o in sorted(again, key=lambda o: -count[o])[:8]: |
| 156 | + say(f" - `{os.path.relpath(o, target)}`: {count[o]} runs, {len(hashes[o])} command hashes") |
| 157 | +say() |
| 158 | + |
| 159 | +say("### 5. Host tools in the global cache") |
| 160 | +say() |
| 161 | +tool = os.path.join(home, "build-cache", "v1", "tool") |
| 162 | +if os.path.isdir(tool): |
| 163 | + keys = collections.defaultdict(list) |
| 164 | + for root, ds, files in os.walk(tool): |
| 165 | + if "entry.json" in files: |
| 166 | + rel = os.path.relpath(root, tool).split(os.sep) |
| 167 | + keys[os.sep.join(rel[:-1])].append(root) |
| 168 | + path_keys = [k for k in keys if "+path." in k] |
| 169 | + say(f"- `build-cache/v1/tool/`: {gb(du(tool))}; {len(keys)} keys, {sum(len(v) for v in keys.values())} variants; path-package keys: {len(path_keys)}") |
| 170 | + for k, v in sorted(keys.items(), key=lambda kv: -len(kv[1]))[:8]: |
| 171 | + say(f" - `{k}`: {len(v)} variant(s)") |
| 172 | +std = os.path.join(home, "build-cache", "v1", "std") |
| 173 | +if os.path.isdir(std): say(f"- `build-cache/v1/std/`: {len(os.listdir(std))} builds, {gb(du(std))}") |
| 174 | +say() |
| 175 | + |
| 176 | +with open(os.environ.get("GITHUB_STEP_SUMMARY", os.devnull), "a") as f: f.write("\n".join(out) + "\n\n") |
0 commit comments