Packages
mob_dev
0.6.4
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.snapshot_loaded.ex
defmodule Mix.Tasks.Mob.SnapshotLoaded do
@shortdoc "Snapshot what's loaded on a connected device — empirical strip candidates"
@moduledoc """
Asks a connected device's BEAM what modules it has loaded right now.
In interactive mode (Mob's default), a module is loaded only when
something calls into it — so the loaded set after a real user session
is the empirical "actually used" set.
Anything **shipped but never loaded** is a strong strip candidate.
Combine with `mix mob.audit_otp` (static reachability) for the
highest-confidence strip set:
shipped ∩ statically-reachable ∩ NEVER-loaded = safe to strip
## Workflow
mix mob.deploy # deploy the app
# Use the app — every flow you care about
mix mob.connect --no-iex # set up the dist tunnel
mix mob.snapshot_loaded # snapshot
mix mob.snapshot_loaded --json out.json # machine-readable
## What the report shows
* Total .beam files shipped
* Modules currently loaded
* Modules in the bundle that have never been loaded
* Per-OTP-lib breakdown (which libs have any module loaded vs not)
## What this is NOT
Not a trace — it doesn't tell you *which* function was called or how
often. Just whether the module was touched. For per-function or
per-call-frequency data, use `mix mob.trace_otp` (host-side harness)
or extend `Mob.Diag` with `:recon_trace`-based on-device tracing.
"""
use Mix.Task
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, strict: [node: :string, json: :string])
Mix.Task.run("loadpaths")
Mix.Task.run("compile")
node = resolve_node(opts[:node])
case :rpc.call(node, Mob.Diag, :loaded_snapshot, []) do
{:badrpc, reason} ->
Mix.shell().error("RPC failed: #{inspect(reason)}")
Mix.shell().error("""
Is the device's mob library new enough to have Mob.Diag.loaded_snapshot/0?
Push the latest BEAMs: mix mob.push
""")
exit({:shutdown, 1})
snapshot ->
if opts[:json] do
File.write!(opts[:json], encode_json(snapshot))
Mix.shell().info("Wrote snapshot to #{opts[:json]}")
else
print_summary(snapshot)
end
end
end
defp resolve_node(nil) do
case Node.list() do
[] ->
Mix.raise("""
No connected nodes found. Run `mix mob.connect --no-iex` first
in another terminal so the device's BEAM is reachable.
Or pass --node <name@host> explicitly.
""")
[single] ->
single
multiple ->
Mix.raise("""
Multiple nodes connected — specify with --node:
#{Enum.map_join(multiple, "\n", &" #{&1}")}
""")
end
end
defp resolve_node(node_str), do: String.to_atom(node_str)
defp encode_json(snapshot) do
%{
otp_root: snapshot.otp_root,
captured_at: DateTime.to_iso8601(snapshot.captured_at),
loaded_count: snapshot.loaded_count,
shipped_count: snapshot.shipped_count,
loaded: Enum.map(snapshot.loaded, &to_string/1),
unloaded_in_bundle: Enum.map(snapshot.unloaded_in_bundle, &to_string/1)
}
|> Jason.encode!(pretty: true)
end
defp print_summary(s) do
h1 = IO.ANSI.bright()
yellow = IO.ANSI.yellow()
green = IO.ANSI.green()
reset = IO.ANSI.reset()
Mix.shell().info("#{h1}=== Loaded-modules snapshot ==={reset}")
Mix.shell().info(" OTP root: #{s.otp_root || "(not detected)"}")
Mix.shell().info(" Captured at: #{DateTime.to_iso8601(s.captured_at)}")
Mix.shell().info(" Shipped: #{s.shipped_count} .beam files")
Mix.shell().info(" Loaded: #{s.loaded_count} modules")
Mix.shell().info(
" Strip candidates (shipped, never loaded): #{length(s.unloaded_in_bundle)}\n"
)
by_lib = group_by_lib(s.unloaded_in_bundle)
if by_lib == %{} do
Mix.shell().info(
" #{green}Every shipped module has been loaded — no strip candidates from this snapshot.#{reset}"
)
else
Mix.shell().info("#{h1}Strip candidates by lib (top 20):#{reset}")
by_lib
|> Enum.sort_by(fn {_lib, mods} -> -length(mods) end)
|> Enum.take(20)
|> Enum.each(fn {lib, mods} ->
Mix.shell().info(
" #{yellow}#{String.pad_trailing(to_string(lib), 25)}#{reset} " <>
"#{length(mods)} unused module(s)"
)
end)
end
Mix.shell().info("""
Cross-reference with `mix mob.audit_otp` for the highest-confidence
strip set: shipped + statically-reachable + NEVER loaded.
""")
end
# Heuristic lib grouping from module name patterns. Not perfect, but
# good enough for a summary view.
defp group_by_lib(modules) do
Enum.group_by(modules, fn mod ->
mod
|> to_string()
|> guess_lib()
end)
end
defp guess_lib(name) do
cond do
String.starts_with?(name, "Elixir.") ->
# "Elixir.Logger.Foo" → "elixir/logger"
rest = String.replace_prefix(name, "Elixir.", "")
rest |> String.split(".") |> List.first() |> Macro.underscore()
true ->
# "logger_h_common" → "logger" by stripping after first underscore
case String.split(name, "_", parts: 2) do
[head | _] -> head
_ -> name
end
end
end
end