Search documentation of mob

# Mob.Diag (mob v0.6.18)

Copy Markdown View Source

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

### Types

load_failure()
load_report()
loaded_snapshot()

### Functions

loaded_snapshot()

Snapshot of what's currently loaded in the running BEAM, plus what's shipped-but-never-loaded (the empirical strip candidates).

mfa_trace(duration_ms \\ 30000)

Captures unique MFAs called during a tracing window from a running app.

verify_loaded_modules()

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

# load_failure()

    @type load_failure() :: %{module: module(), reason: term()}

# load_report()

    @type load_report() :: %{
      total: non_neg_integer(),
      loaded: non_neg_integer(),
      failed: [load_failure()],
      elapsed_us: non_neg_integer(),
      otp_root: String.t() | nil
    }

# loaded_snapshot()

    @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

# loaded_snapshot()

    @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:

1. Deploy the app
2. User exercises every flow they care about
3. RPC `Mob.Diag.loaded_snapshot/0` from a Mix task
4. Cross-reference `:unloaded_in_bundle` with 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.

# mfa_trace(duration_ms \\ 30000)

    @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/3` is 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.

# verify_loaded_modules()

    @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.