My own corner of monopam
README.md

dissect #

Attribute a compiled binary's size to the OCaml modules and libraries that fill it.

Installation #

Install with opam:

$ opam install dissect

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install dissect

Overview #

An OCaml executable of 16 MiB gives no hint of which libraries made it that size, and the usual answer, nm piped through a script, needs binutils and knowledge of how OCaml names its symbols. dissect reads a compiled OCaml binary (Mach-O or ELF, detected from its leading bytes), attributes every byte to the OCaml module and library that produced it, and prints a sorted weight-map of proportional bars in the terminal. The binary formats are parsed natively by mach-o and elf (both built on the wire codec library); the rendering uses console.

Usage #

dissect path/to/binary        # group by library, top 20 (the default)
dissect --by-module ./a.out   # group by individual module
dissect -n 40 ./a.out         # show more rows (0 shows all)
! 5.4 MiB (34%) is not code: symbol & string tables, constants, unwind data
  and headers. Most is removed by `strip`.

╭────────────────────┬───────────┬─────┬───...╮
│ Module             │      Size │   % │      │
├────────────────────┼───────────┼─────┼───...┤
│ Stdlib             │ 713.7 KiB │  6% │ ███  │
│ Parser             │ 700.7 KiB │  6% │ ███  │
│ Typecore           │ 455.0 KiB │  4% │ ██   │
│ (... 190 more)     │   4.9 MiB │ 46% │ ████ │
╰────────────────────┴───────────┴─────┴───...╯

10.5 MiB of code across 210 module(s), 65% of the 15.9 MiB file

The bytes no symbol covers are summarised in the warning rather than given rows in the table: the symbol and string tables (Mach-O __LINKEDIT, which strip removes), the constant and exception sections, and the headers. The percentages in the table are therefore shares of the attributed code rather than of the whole file.

How symbols are bucketed #

Each linker symbol is classified by name. Dune wraps a library by default: each module M of library Lib is compiled as Lib__M, and one generated module Lib re-exports them, so two libraries can each have a module called Parser without their symbols colliding. A wrapped library's symbol therefore has the form caml<Lib>__<Module>$<function>, and it is grouped under <Lib>. A symbol with no __ is bucketed under its own module name, since the symbol does not encode the library. Anything that is not a module symbol starting with caml goes to the C runtime bucket:

# Dissect.Demangle.classify "_camlStdlib__Arg$go_42"
- : Dissect.Demangle.t =
Dissect.Demangle.Ocaml {Dissect.Demangle.library = "Stdlib"; modul = "Arg"}
# Dissect.Demangle.classify "_caml_alloc"
- : Dissect.Demangle.t = Dissect.Demangle.C_runtime

Mach-O symbol records carry no size, so a symbol's size is taken as the distance to the next symbol in its section, and the last symbol of a section runs to the section's end (its address plus its size). The symbols of a section therefore sum to the bytes from its first symbol to its end. ELF records carry an explicit size, and dissect uses it.

Licence #

ISC. See LICENSE.md.