Packages
mob_dev
0.3.37
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.trace_otp.ex
defmodule Mix.Tasks.Mob.TraceOtp do
@shortdoc "Run the Elixir characterization harness under trace, dump touched MFAs"
@moduledoc """
Runs `MobDev.OtpTrace.Harness` under full call tracing and reports
the runtime modules + MFAs actually exercised.
This is the empirical complement to `mix mob.audit_otp` (which uses
static reachability). Combined, they give a high-confidence answer
to "what does any Elixir runtime actually need."
## Usage
mix mob.trace_otp # Run all phases, summary report
mix mob.trace_otp --phase otp # Just one phase
mix mob.trace_otp --json out.json # Machine-readable dump
# Trace a real running app on a connected device for 30s.
# First run `mix mob.connect` in another terminal.
mix mob.trace_otp --remote pigeon_ios_defd4bdc@127.0.0.1 --duration 30000
Local mode (default) runs a synthetic Elixir/OTP characterization
harness inside the host BEAM. Remote mode wraps `:erlang.trace_pattern`
on a connected device node and captures the MFAs hit during a real
user session — drive the app interactively while the trace runs.
## Phases
language — pattern match, comprehensions, structs, protocols
collections — Enum, List, Map, MapSet, Stream, Range
strings — String, Binary, charlist, codepoints
processes — spawn, send/receive, monitor, link, Task
otp — GenServer, Supervisor, Application, Logger
data — ETS, persistent_term, Date/Time, System
errors — raise/rescue, throw/catch, exit, exception structs
all — every phase (default)
## Output
Plain mode prints a summary + sorted module list. `--json` writes a
JSON file with the full MFA set, suitable for cross-referencing
against the static audit (`mix mob.audit_otp`) to find modules
shipped-but-never-called.
"""
use Mix.Task
alias MobDev.OtpTrace
alias MobDev.OtpTrace.Harness
@phases ~w(language collections strings processes otp data errors all)a
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
strict: [phase: :string, json: :string, remote: :string, duration: :integer]
)
Mix.Task.run("loadpaths")
Mix.Task.run("compile")
if opts[:remote] do
run_remote(opts)
else
run_local(opts)
end
end
# ── Local synthetic-harness trace ────────────────────────────────────────────
defp run_local(opts) do
phase = parse_phase(opts[:phase] || "all")
Code.ensure_all_loaded([
Harness,
Harness.HarnessStruct,
Harness.HarnessProto,
Harness.HarnessGS,
Harness.HarnessApp
])
Mix.shell().info("Running Elixir characterization harness under trace…")
Mix.shell().info(" Phase: #{phase}\n")
result = OtpTrace.capture(fn -> apply(Harness, phase, []) end)
if opts[:json] do
write_json(result, opts[:json])
Mix.shell().info("Wrote #{opts[:json]}")
else
print_summary(result, phase)
end
end
# ── Remote (real-app) trace ──────────────────────────────────────────────────
defp run_remote(opts) do
node_str = opts[:remote]
duration = opts[:duration] || 30_000
node = String.to_atom(node_str)
ensure_distribution_started!()
Mix.shell().info("Tracing #{node_str} for #{duration / 1000}s…")
Mix.shell().info(" Drive the app on the device while the window is open.\n")
case :rpc.call(node, Mob.Diag, :mfa_trace, [duration]) do
{:badrpc, reason} ->
Mix.shell().error("RPC failed: #{inspect(reason)}")
Mix.shell().error("""
Is the device connected? Try `mix mob.connect` in another
terminal first, then re-run this command.
""")
exit({:shutdown, 1})
result ->
if opts[:json] do
write_remote_json(result, opts[:json])
Mix.shell().info("Wrote #{opts[:json]}")
else
print_remote_summary(result, node)
end
end
end
defp print_remote_summary(result, node) do
h1 = IO.ANSI.bright()
dim = IO.ANSI.faint()
reset = IO.ANSI.reset()
Mix.shell().info("#{h1}=== Remote trace summary ==={reset}")
Mix.shell().info(" Node: #{node}")
Mix.shell().info(" Duration: #{result.duration_ms / 1000}s")
Mix.shell().info(" Modules touched: #{result.module_count}")
Mix.shell().info(" Unique MFAs: #{result.mfa_count}")
Mix.shell().info("\n#{h1}=== Modules touched (sorted) ==={reset}\n")
{elixir, erlang} =
result.modules
|> Enum.split_with(&String.starts_with?(to_string(&1), "Elixir."))
Mix.shell().info(" #{dim}Elixir modules (#{length(elixir)}):#{reset}")
for m <- elixir, do: Mix.shell().info(" #{inspect(m)}")
Mix.shell().info("\n #{dim}Erlang modules (#{length(erlang)}):#{reset}")
for m <- erlang, do: Mix.shell().info(" #{inspect(m)}")
Mix.shell().info("")
end
# mix doesn't start distribution by default. Without it, :rpc.call
# returns {:badrpc, :nodedown} even though the target node is alive
# and registered in EPMD. Self-name into a unique node so multiple
# invocations don't collide.
defp ensure_distribution_started! do
if Node.alive?() do
:ok
else
name = :"mob_trace_otp_#{System.unique_integer([:positive])}@127.0.0.1"
case Node.start(name, :longnames) do
{:ok, _} ->
:ok
{:error, reason} ->
Mix.raise("Failed to start distribution: #{inspect(reason)}")
end
end
Node.set_cookie(:mob_secret)
end
defp write_remote_json(result, path) do
payload = %{
mfas:
result.mfas
|> Enum.map(fn {m, f, a} -> [to_string(m), to_string(f), a] end),
modules: Enum.map(result.modules, &to_string/1),
mfa_count: result.mfa_count,
module_count: result.module_count,
duration_ms: result.duration_ms,
captured_at: DateTime.to_iso8601(result.captured_at)
}
File.write!(path, Jason.encode!(payload, pretty: true))
end
defp parse_phase(name) do
atom = String.to_existing_atom(name)
if atom in @phases do
atom
else
Mix.raise("Unknown phase: #{name}. Valid: #{Enum.join(@phases, ", ")}")
end
rescue
ArgumentError ->
Mix.raise("Unknown phase: #{name}. Valid: #{Enum.join(@phases, ", ")}")
end
defp print_summary(result, phase) do
h1 = IO.ANSI.bright()
dim = IO.ANSI.faint()
reset = IO.ANSI.reset()
Mix.shell().info("#{h1}=== Trace summary ==={reset}")
Mix.shell().info(" Phase exercised: #{phase}")
Mix.shell().info(" Modules touched: #{MapSet.size(result.modules)}")
Mix.shell().info(" Unique MFAs: #{MapSet.size(result.mfas)}")
Mix.shell().info(" Wall time: #{format_us(result.elapsed_us)}")
Mix.shell().info("\n#{h1}=== Modules touched (sorted) ==={reset}")
{elixir, erlang} =
result.modules
|> Enum.sort()
|> Enum.split_with(fn m -> String.starts_with?(to_string(m), "Elixir.") end)
Mix.shell().info("\n #{dim}Elixir modules (#{length(elixir)}):#{reset}")
for m <- elixir do
Mix.shell().info(" #{inspect(m)}")
end
Mix.shell().info("\n #{dim}Erlang modules (#{length(erlang)}):#{reset}")
for m <- erlang do
Mix.shell().info(" #{inspect(m)}")
end
Mix.shell().info("")
end
defp write_json(result, path) do
payload = %{
modules: result.modules |> Enum.sort() |> Enum.map(&to_string/1),
mfas:
result.mfas
|> Enum.sort()
|> Enum.map(fn {m, f, a} -> [to_string(m), to_string(f), a] end),
elapsed_us: result.elapsed_us,
module_count: MapSet.size(result.modules),
mfa_count: MapSet.size(result.mfas)
}
File.write!(path, Jason.encode!(payload, pretty: true))
end
defp format_us(us) when us < 1_000, do: "#{us}μs"
defp format_us(us) when us < 1_000_000, do: "#{Float.round(us / 1_000, 1)}ms"
defp format_us(us), do: "#{Float.round(us / 1_000_000, 2)}s"
end