Reachability analysis for the bundled OTP runtime tree of a Mob app.
Walks every .beam file under an OTP root, extracts the imports chunk
to learn who calls whom, and computes the transitive closure starting
from the app's entry-point modules. Anything not reachable is a strip
candidate — modules, whole libs, or duplicate library versions.
Used by mix mob.audit_otp (read-only report) and the planned
mix mob.release --slim flag (auto-strip based on the report).
Entry points
Reachability seeding is deliberately generous:
- App's
start/2callback module (read from each.appfile'smodkey) - All exported functions of
kernelandstdlib(BEAM startup needs them) elixir,logger,eex(runtime support that gets called via macros)
Anything reachable from those is kept; the rest is candidate-for-strip.
Output
Returns a map with:
:libs— every lib found, with reachable/total module counts and KB:duplicates— libs that appear multiple times (only newest is kept):foreign_apps— non-OTP, non-app code in the lib dir (other projects):strippable_libs— libs with zero reachable modules:total_kb/:reachable_kb/:strippable_kb— size summary
All sizes are post-strip — i.e. what mix mob.release actually ships,
not raw OTP source.
Summary
Functions
Run the audit. otp_root is the directory containing lib/ and erts-*/
(typically the runtime tree extracted into the app bundle, or the cache
the release packaging copies from).
Reads one or more JSON trace files written by
mix mob.trace_otp --json and unions their modules atoms into a
single MapSet suitable for :trace_input.
Types
@type lib_name() :: String.t()
@type lib_report() :: %{ name: lib_name(), version: String.t() | nil, path: String.t(), modules_total: non_neg_integer(), modules_reachable: non_neg_integer(), modules_traced: non_neg_integer() | nil, kb_total: non_neg_integer(), kb_reachable: non_neg_integer(), unreachable_modules: [module_atom()], untraced_modules: [module_atom()] | nil, is_app_under_test?: boolean() }
@type module_atom() :: atom()
@type report() :: %{ otp_root: String.t(), app_name: String.t() | nil, libs: [lib_report()], duplicates: %{required(lib_name()) => [String.t()]}, foreign_apps: [String.t()], foreign_app_names: [lib_name()], strippable_libs: [lib_name()], trace_strippable_libs: [lib_name()] | nil, total_kb: non_neg_integer(), reachable_kb: non_neg_integer(), strippable_kb: non_neg_integer() }
Functions
Run the audit. otp_root is the directory containing lib/ and erts-*/
(typically the runtime tree extracted into the app bundle, or the cache
the release packaging copies from).
Options
:app_name— the application's atom name (e.g.:air_cart_max). Used to seed reachability from the app's modules. If omitted, every lib with nomodcallback is treated as a potential entry point (broader, finds less to strip).:project_deps— list of atoms naming the project's deps (the transitive closure, as Mix sees them). When given, the foreign-app classifier uses it as the authoritative set of "non-OTP libs that are supposed to be in this bundle." Anything in the bundle whose name isn't OTP-shipped, Elixir-shipped, the app under test, or in:project_depsis classified as foreign and quarantined intoreport.foreign_apps. Themix mob.audit_otptask auto-detects this from the currentMix.Project.When omitted, the classifier falls back to a narrow name-pattern heuristic (
test_,toy_,mob_test,scratch_) — sufficient for tests, less accurate in real bundles.:trace_input— aMapSet(or list) ofmodule()atoms that were observed at runtime during a trace window. Comes fromMobDev.OtpTrace.capture/1(local synthetic harness) orMob.Diag.mfa_trace/1(remote device trace). When given, the report grows two fields per lib (modules_traced,untraced_modules) and one top-level field (trace_strippable_libs) listing libs whose modules were ALL absent from the trace — i.e., empirically never called.Static reachability misses dynamic dispatch (
apply/3,:erlang.load_nif, runtime config). Trace data catches everything that actually ran. The intersectionstrippable_libs ∩ trace_strippable_libsis the high-confidence strip set;trace_strippable_libs \ strippable_libsis the "static graph reaches it but nothing actually called it" set that lets you strip partly-used libs like megaco / snmp / diameter.
Reads one or more JSON trace files written by
mix mob.trace_otp --json and unions their modules atoms into a
single MapSet suitable for :trace_input.
Returns:
nilwhen given an empty list (the audit will take its no-trace branch).nilwhen every read failed — better than handing back an empty set, which would let the trace-augmented expansion strip every partly-used lib in the bundle. A warning is emitted viaon_read_error.(path, reason)for each failure so callers can route them through their own logging.MapSet.t/0ofmodule()atoms otherwise.
Multi-trace union is the right shape for "this lib is never called" claims: a 60-second window only exercises one slice of the app. Unioning boot + UI + auth + idle captures lets users say "across ALL captured sessions, this lib was never touched" — a much stronger signal than any single trace.
Example
iex> MobDev.OtpAudit.union_trace_jsons(["priv/boot.json", "priv/ui.json"])
#MapSet<[:kernel, :Elixir.Enum, :Elixir.Map, ...]>