Files
thor/AUDITING_BINARY_SIZE.md

3.9 KiB

Visualizing & Managing thor Binary Size

How to inspect what makes up the compiled thor binary, and how to keep that inspection useful once we bundle layout/asset files into the binary.

Quick inspection (tools already available)

The binary is a not-stripped ELF, so it ships symbols we can break down.

# Section totals (text / data / bss)
size ./thor

# Top symbols by size
nm --print-size --size-sort --radix=d ./thor \
  | awk '{sz=$2+0; sub(/^[0-9a-fA-F]+ +[0-9]+ +[A-Za-z] +/,""); printf "%8d  %s\n", sz, $0}' \
  | sort -rn | head -25

# Aggregate symbol sizes by Odin module namespace (foo::bar -> "foo")
nm --print-size --size-sort --radix=d ./thor \
  | awk '{sz=$2+0; rest=$0; sub(/^[0-9a-fA-F]+ +[0-9]+ +[A-Za-z] +/,"",rest);
          ns=rest; if (rest ~ /::/){sub(/::.*/,"",ns)}
          else if (rest ~ /^ts_/){ns="treesitter (ts_*)"} else {ns="<other/runtime>"};
          s[ns]+=sz} END{for (k in s) printf "%9d  %s\n", s[k], k}' \
  | sort -rn | head -25

As of this writing the top contributors were: tree-sitter tables (~118 KB), main (~96 KB), runtime/other (~93 KB), mustache (~67 KB), then fmt / encoding_json from the stdlib.

Better visualization: bloaty

bloaty (Google's binary-size profiler) is the right tool. Not installed, but runnable via Nix:

bloaty ./thor                      # by section
bloaty ./thor -d symbols -n 30     # by symbol, hierarchical
bloaty ./thor -d sections,symbols  # section then symbols within each
# via nix (no install):
nix run nixpkgs#bloaty -- ./thor -d symbols -n 30

For a stripped-size estimate:

readelf -S ./thor | grep -E 'debug|symtab|strtab'   # strippable overhead
strip -s ./thor -o thor.stripped && size thor.stripped

Odin also supports smaller builds: odin build . -o:size -no-bounds-check.

IMPORTANT: keeping bloaty useful once we bundle layouts into the binary

Today the VFS (vfs.odin) reads layout/asset files lazily from disk at runtime (VFS_Entry.fs_path + os.read_entire_file_from_path); VFS_Entry.data is never populated at build time. So embedded content is not in ./thor yet.

When we switch to embedding via Odin #load(...), the file bytes land in the binary's .rodata section. Whether bloaty can show per-file sizes depends on how we emit the embed:

  • Section total — always visible. .rodata grows by ~the total bundled size. bloaty ./thor -d sections (or size) shows the aggregate, and we can diff before/after. This always works.

  • Per-file breakdown — only if each file is a NAMED symbol.

    • A generated map[string]VFS_Entry literal full of inline #load(...) expressions makes each blob an anonymous .rodata constant. Bloaty lumps them together — no per-file breakdown.

    • Assigning each file to its own package-level variable makes each a named global symbol, so bloaty ./thor -d symbols (and the nm recipe above) show each file individually:

      layout_base_html   := #load("defaults/layouts/base.html")
      layout_single_html := #load("defaults/layouts/single.html")
      // ...then reference these in the VFS map instead of inlining #load.
      

Decision to bake into the bundling codegen

When we build the codegen that replaces the runtime mount_dir disk walk, have it emit one named package-level variable per embedded file (not an anonymous map literal). That single choice is what makes bloaty / nm per-file analysis possible — cheap to do up front, painful to retrofit.

Additional options that help regardless of symbolization:

  1. Build with Odin -debug (DWARF) and use bloaty ./thor -d compileunits to attribute embedded bytes to the generated source file.
  2. Since the generator already knows every file and its size, emit a size manifest at bundle time (path -> bytes, sorted / treemap). This is the most reliable per-file view and can't drift from reality; bloaty then just confirms the .rodata total matches.