llm
Download
204 lines 8.5 KiBPythonModified 2026-08-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
#!/usr/bin/env python3
"""Render size-breakdown.png: where a compiled Tomo binary's bytes come from.

Builds a small program that exercises bignum `Int` and exact-real `Num`
arithmetic (so GMP and the number engine are genuinely linked), re-links it
with `--print-map`, and attributes every byte of the file to the archive it
came from. The chart this draws (sorted bars, plus a strip showing the same
components as shares of the whole) is the answer to "why is a Tomo binary
the size it is": mostly GMP, Unicode tables, and the runtime, with the
program itself a sliver.

Usage: python3 size_breakdown.py   (writes size-breakdown.svg and .png)
"""
import glob
import os
import re
import subprocess
import sys
import collections

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

HERE = os.path.dirname(os.path.abspath(__file__))
BUILD = os.path.join(HERE, ".build", "breakdown")
LOCAL_TOMO = os.path.join(HERE, "..", "local-tomo")

SURFACE, INK, MUTED = "#fcfcfb", "#202124", "#5f6368"
# One hue per component, assigned by descending size. More slots than a strict
# categorical palette would allow, on purpose: nothing disparate is lumped to
# fit a palette, and every slice is direct-labeled so identity never rides on
# color alone.
HUES = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300",
        "#4a3aa7", "#e34948", "#8a6d3b", "#0e9aa7", "#b45fbf", "#616a72",
        "#94b447"]

SAMPLE = """\
func main()
    big := Int(2)^128 + Int(1)
    exact := Num(1)/3 + Num.PI
    say("$big  $exact  $(exact.digits(12))")
"""


def build_with_map():
    """Build the sample, then re-run its link with --print-map captured."""
    os.makedirs(BUILD, exist_ok=True)
    src = os.path.join(BUILD, "breakdown.tm")
    binpath = os.path.join(BUILD, "breakdown")
    with open(src, "w") as f:
        f.write(SAMPLE)
    out = subprocess.run([LOCAL_TOMO, "build", "-O3", "-f", "--verbose",
                          "-o", binpath, src],
                         capture_output=True, text=True)
    # The verbose log prints the exact link command (in ANSI color, which has
    # to be stripped); zig cc has no -Wl,-Map, but LLD's --print-map writes
    # the map to stdout.
    log = re.sub(r"\x1b\[[0-9;]*m", "", out.stdout + out.stderr)
    link = None
    for line in log.splitlines():
        m = re.search(r"(\S*zig cc .*-o " + re.escape(binpath) + r")\b", line)
        if m:
            link = m.group(1)
    if link is None or not os.path.exists(binpath):
        sys.exit("could not build the sample or find its link command "
                 "(is ../local-tomo runnable?)")
    if link.startswith("zig cc"):
        # The log says just "zig cc"; resolve it to the bundled toolchain:
        candidates = sorted(
            p for p in
            [os.path.join(d, "zig", "zig") for d in
             glob.glob(os.path.join(HERE, "..", "build", "*", "tomo",
                                    "libexec", "tomo@*"))]
            if os.access(p, os.X_OK))
        if not candidates:
            sys.exit("no bundled zig found under ../build")
        link = candidates[-1] + link[len("zig"):]
    mapped = link.replace(f"-o {binpath}",
                          f"-Wl,--print-map -o {binpath}.mapped")
    r = subprocess.run(mapped, shell=True, capture_output=True, text=True)
    if r.returncode != 0:
        sys.exit(f"re-link with --print-map failed:\n{r.stderr[:500]}")
    return binpath, r.stdout


def attribute(map_text, binpath):
    """Sum each input archive's contribution to the file, from the map."""
    row = re.compile(r"^\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+(\d+)"
                     r" (\s*)(\S.*)$")

    def bucket(path, out_section):
        b = os.path.basename(path)
        if b.startswith("<internal>"):
            # The linker generates these itself: the symbol table (what
            # `strip` removes) vs the merged string/constant pools programs
            # actually read at runtime.
            if out_section in (".symtab", ".strtab", ".shstrtab"):
                return "symbol tables"
            return "linker data\n(merged strings/consts)"
        # libtomo.a is built from exactly src/stdlib/*.c (Makefile's
        # STDLIB_OBJS), so this really is the standard library.
        if b == "libtomo.a": return "Tomo standard library"
        if b == "libgmp.a": return "GMP (bignum Int)"
        if b == "libunistring.a": return "libunistring (Unicode)"
        if b == "libgc.a": return "Boehm GC"
        if b == "libbacktrace.a": return "libbacktrace (stacktraces)"
        if b in ("libc.a", "libzigc.a", "crt1.o"): return "musl libc"
        if b == "libcompiler_rt.a": return "compiler-rt"
        if b == "libunwind.a": return "libunwind"
        # The program's own two objects, split by what the bytes are for:
        if out_section.startswith(".tomo."):
            return "embedded licenses\n+ source (.tomo.source)"
        if out_section.startswith(".debug"):
            return "program debug info"
        return "program code"

    grouped = collections.Counter()
    cur_out = None
    for line in map_text.splitlines():
        m = row.match(line)
        if not m:
            continue
        size, indent, what = int(m.group(3), 16), len(m.group(5)), m.group(6)
        if indent == 0:  # output-section header
            cur_out = what.split()[0]
            continue
        if indent != 8:  # symbol row: would double-count its input section
            continue
        # .bss and friends occupy no file bytes, so they don't belong in a
        # chart of the *file*:
        if cur_out and cur_out.startswith((".bss", ".tbss", ".relro")):
            continue
        grouped[bucket(what.split(":(")[0].split("(")[0], cur_out)] += size
    return grouped, os.path.getsize(binpath)


def draw(grouped, file_size):
    parts = sorted(grouped.items(), key=lambda t: -t[1])
    total = sum(v for _, v in parts)
    flat = [(n.replace("\n", " "), v) for n, v in parts]

    fig, (ax, ax2) = plt.subplots(2, 1, figsize=(9.8, 7.0),
                                  height_ratios=[13, 1.1])
    fig.patch.set_facecolor(SURFACE)
    for a in (ax, ax2):
        a.set_facecolor(SURFACE)

    # Sorted bars carry the magnitudes: with this many components the reader's
    # question is "what's big, what's small, by how much", which arc lengths in
    # a pie answer poorly. Names are plain axis labels, so nothing can collide.
    names = [n for n, _ in flat]
    vals = [v for _, v in flat]
    y = range(len(flat))
    ax.barh(y, vals, color=HUES[:len(flat)], height=0.68)
    ax.invert_yaxis()
    ax.set_yticks(list(y))
    ax.set_yticklabels(names, fontsize=10, color=INK)
    for yi, v in zip(y, vals):
        ax.text(v + total * 0.008, yi,
                f"{v/1024:.0f} KB · {100*v/total:.1f}%",
                va="center", fontsize=9, color=MUTED)
    ax.set_xlim(0, max(vals) * 1.30)
    for sp in ("top", "right", "left", "bottom"):
        ax.spines[sp].set_visible(False)
    ax.tick_params(length=0)
    ax.set_xticks([])

    # The strip keeps the part-to-whole reading a pie gives: the same
    # components tiling one binary from 0 to its full size, in the same hues
    # as the bars above, so it needs no labels of its own.
    left = 0
    for (name, v), hue in zip(flat, HUES):
        ax2.barh([0], [v], left=left, color=hue, height=1.0,
                 edgecolor=SURFACE, linewidth=1.5)
        left += v
    ax2.set(xlim=(0, total), ylim=(-0.75, 0.75))
    ax2.axis("off")
    ax2.text(0, -1.35, "0", fontsize=8.5, color=MUTED, ha="left")
    ax2.text(total, -1.35, f"{total/1024:.0f} KB", fontsize=8.5, color=MUTED,
             ha="right")

    fig.suptitle("Where a Tomo binary's bytes go", x=0.02, y=0.975, ha="left",
                 fontsize=15, fontweight="bold", color=INK)
    fig.text(0.02, 0.925,
             f"an {total/1024:.0f} KB static binary doing bignum and "
             "exact-real arithmetic, built with -O3 · attributed from the "
             "linker map · musl throughout",
             ha="left", fontsize=9, color=MUTED)
    fig.tight_layout(rect=[0, 0.01, 1, 0.91])
    for ext, dpi in (("svg", None), ("png", 150)):
        out = os.path.join(HERE, f"size-breakdown.{ext}")
        fig.savefig(out, dpi=dpi, facecolor=SURFACE)
        print(f"wrote {out}")

    for name, val in flat:
        print(f"  {val/1024:7.1f} KB  {100*val/total:5.1f}%  {name}")
    print(f"  {total/1024:7.1f} KB attributed of a {file_size/1024:.1f} KB file")


if __name__ == "__main__":
    binpath, map_text = build_with_map()
    grouped, file_size = attribute(map_text, binpath)
    draw(grouped, file_size)