Runtime diagnostics that run inside a Mob app's BEAM. Designed to be invoked via Erlang RPC from a developer's machine to inspect the actual state of a deployed app.
Pairs with mob_dev's tooling — mix mob.verify_strip calls into
verify_loaded_modules/0. Kept in the mob library (not mob_dev)
so the functions are present in every shipped app, not just at build
time on the developer's machine.
Don't expand the API surface here without thinking — anything added is permanently shipped to every Mob app and a permanent target for remote-execution if dist credentials leak.
Summary
Functions
Snapshot of what's currently loaded in the running BEAM, plus what's shipped-but-never-loaded (the empirical strip candidates).
Captures unique MFAs called during a tracing window from a running app.
Force-load every .beam file under the running app's OTP tree and
report any that fail. Used by mix mob.verify_strip to validate
that an aggressive strip didn't remove a module something else
needed.
Types
@type load_report() :: %{ total: non_neg_integer(), loaded: non_neg_integer(), failed: [load_failure()], elapsed_us: non_neg_integer(), otp_root: String.t() | nil }
@type loaded_snapshot() :: %{ loaded: [module()], loaded_count: non_neg_integer(), shipped_count: non_neg_integer(), unloaded_in_bundle: [module()], otp_root: String.t() | nil, captured_at: DateTime.t() }
Functions
@spec loaded_snapshot() :: loaded_snapshot()
Snapshot of what's currently loaded in the running BEAM, plus what's shipped-but-never-loaded (the empirical strip candidates).
In interactive mode (Mob's default), a module is loaded only when something calls into it. So the loaded set after a representative user session is "what the app actually needs." Anything in the bundle but not in the loaded set is a strong strip candidate.
Better than tracing for our purposes: zero overhead, no rate-limit worries, no risk of mailbox-overflowing a busy app.
Workflow:
- Deploy the app
- User exercises every flow they care about
- RPC
Mob.Diag.loaded_snapshot/0from a Mix task - Cross-reference
:unloaded_in_bundlewith the static audit: shipped + statically-reachable + never-loaded = high-confidence strip candidates.
Caveats: a flow that wasn't exercised won't show up. Run after a thorough session, not after just opening the app.
@spec mfa_trace(non_neg_integer()) :: %{ mfas: [{module(), atom(), arity()}], modules: [module()], mfa_count: non_neg_integer(), module_count: non_neg_integer(), duration_ms: non_neg_integer(), captured_at: DateTime.t() }
Captures unique MFAs called during a tracing window from a running app.
Wraps :erlang.trace_pattern/3 + :erlang.trace/3 for duration_ms,
then collects the unique {mod, fun, arity} set into ETS for retrieval.
Useful for empirical reachability beyond what loaded_snapshot/0
shows — loaded_snapshot/0 answers "which modules are loaded";
mfa_trace/1 answers "which functions actually got called during
this window." The MFA grain matters for Pass 4 (OpenSSL feature
surgery) where the question is "does the app call crypto:rsa_* at
all" not just "is the :crypto module loaded."
Returns:
%{
mfas: [{:crypto, :crypto_one_time_aead, 6}, ...],
modules: [:crypto, :ssl, ...],
mfa_count: 1247,
module_count: 89,
duration_ms: 30_000,
captured_at: ~U[...]
}Limits:
:erlang.trace/3is process-global. One trace at a time — overlapping calls clobber each other.- Holds an ETS table during the window. ~100k events / 60s on an active app, dedup keeps the unique set small.
- Tracing has a measurable runtime cost (~1.5–2× slowdown). Don't leave a trace running indefinitely.
@spec verify_loaded_modules() :: load_report()
Force-load every .beam file under the running app's OTP tree and
report any that fail. Used by mix mob.verify_strip to validate
that an aggressive strip didn't remove a module something else
needed.
Walks all entries in :code.get_path/0, finds the OTP root from
the first matching .../otp/lib/... path, and enumerates .beam
files under it.
Returns load_report/0. Failures usually mean a stripped lib
contained a transitive dependency of a kept module.