# `Mob.Diag`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/diag.ex#L1)

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.

# `load_failure`

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

# `load_report`

```elixir
@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`

```elixir
@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()
}
```

# `health`

```elixir
@spec health() :: map()
```

Whether the framework's own diagnostics are working, so an empty answer from
one of them can be told apart from a broken one.

    :rpc.call(node, Mob.Diag, :health, [])

Per store (`Mob.Agent.Receipts`, `Mob.Defect.Bus`, `Mob.Invariant`,
`Mob.PostMortem.Registry`, `Mob.RenderStats`):

  * `owner` — the owning pid, or `nil` while down. The tables are then held
    by `Mob.Diag.Heir`, still written to, until the owner the heir restarts
    takes them back. An owner that stays `nil` with its tables at the heir
    failed to restart, and the reason was logged. The store's next write
    after a `mob` upgrade starts one too.
  * `tables` — each table's size and whether the `:owner`, the `:heir`,
    another process (`:other`) holds it, or it is `:missing`
  * `lost` — writes that failed and were dropped. Non-zero means the store's
    answers are incomplete.
  * `resets` — times the tables had to be recreated after being lost (the
    owner and heir both died). Everything recorded before was lost with them.
  * `owner_starts` — owner processes started so far
  * `state_vsn` — current and expected state versions (differ briefly after
    a hot push)
  * `framework_vsn` — the `Mob.Diag.Store` version the store was set up
    under (`nil` for 0.9.5) and the running one. They differ after a `mob`
    upgrade until the store's next write sets it up again.
  * `store` — store-specific counts, e.g. evictions (`:stale` while the
    state is in a shape this code cannot read)

Plus the heir; the subscriber lists of `Mob.Defect.Bus` and
`Mob.Event.Trace`: per topic, how many receive (`topics`) and how many are
waiting for their node to reconnect (`parked`); and `listener` —
`Mob.Listener`'s pid and `undeliverable`, the native events that arrived for
a screen that had died (each discrete one also has an `:undeliverable`
receipt). Counts and pids only, never recorded content. Read-only: it starts
nothing and repairs nothing.

# `loaded_snapshot`

```elixir
@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`

```elixir
@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`

```elixir
@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 `t:load_report/0`. Failures usually mean a stripped lib
contained a transitive dependency of a kept module.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
