Packages
mob_dev
0.6.2
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/mob_dev/otp_trace.ex
defmodule MobDev.OtpTrace do
@moduledoc """
Runtime call-tracing utility. Wraps `:erlang.trace_pattern/3` and
`:erlang.trace/3` to capture the set of `{module, function, arity}`
actually called during a function's execution.
Used by the characterization harness (`priv/trace/harness.exs`) to
empirically map what Elixir's runtime calls under the hood — the
baseline that complements `MobDev.OtpAudit`'s static analysis.
## Why this exists
Static reachability misses dynamic dispatch (`apply/3` with a
computed atom, `Code.ensure_loaded/1`, NIF lazy load via
`:erlang.load_nif/2`). Tracing catches everything that *actually
ran* during the trace window — ground truth, no inference.
Combined with static analysis: `static ∪ trace` gives a high-confidence
reachable set; `static ∩ trace` is the "definitely called and
statically reachable" core.
## Usage
result = MobDev.OtpTrace.capture(fn ->
# any Elixir code — exercise features you want to measure
Enum.map(1..10, &(&1 * 2))
end)
result.mfas # MapSet of {module, function, arity}
result.modules # MapSet of modules called
result.elapsed_us # How long the wrapped fn took (incl. trace overhead)
Trace overhead is real (~10x slowdown for tight loops) — only enable
for measurement runs, never in production.
"""
@type mfa_set :: MapSet.t({module(), atom(), arity()})
@type module_set :: MapSet.t(module())
@type result :: %{
mfas: mfa_set(),
modules: module_set(),
elapsed_us: non_neg_integer()
}
@doc """
Run `fun` with full call tracing enabled on the calling process and
any processes it spawns during execution. Returns a `result/0` map.
## Options
* `:exclude_modules` — modules whose calls should NOT be recorded.
Defaults to `[__MODULE__, MobDev.OtpTrace.Collector, Agent, Task]`
so we don't pollute the trace with our own machinery.
"""
@spec capture((-> any()), keyword()) :: result()
def capture(fun, opts \\ []) when is_function(fun, 0) do
exclude =
opts
|> Keyword.get(:exclude_modules, [__MODULE__, __MODULE__.Collector, Agent, Task])
|> MapSet.new()
{:ok, collector} = MobDev.OtpTrace.Collector.start_link(exclude)
# Trace ALL local function calls across ALL modules.
# `:local` includes calls within the module too; `:global` would
# only catch external calls. We want both for full coverage.
:erlang.trace_pattern({:_, :_, :_}, true, [:local])
# Trace this process + anything it spawns. Collector receives
# {:trace, pid, :call, {m, f, a}} messages.
:erlang.trace(self(), true, [:call, :set_on_spawn, {:tracer, collector}])
started = System.monotonic_time(:microsecond)
try do
fun.()
after
:erlang.trace(self(), false, [:all])
:erlang.trace_pattern({:_, :_, :_}, false, [:local])
end
elapsed = System.monotonic_time(:microsecond) - started
# Give the collector a beat to process pending trace messages.
Process.sleep(50)
mfas = MobDev.OtpTrace.Collector.snapshot(collector)
MobDev.OtpTrace.Collector.stop(collector)
%{
mfas: mfas,
modules: MapSet.new(mfas, fn {m, _, _} -> m end),
elapsed_us: elapsed
}
end
end