Packages
mob_dev
0.3.35
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.audit_otp.ex
defmodule Mix.Tasks.Mob.AuditOtp do
@shortdoc "Reports which OTP libs your bundled app actually uses"
@moduledoc """
Walks an OTP runtime tree and tells you which libraries are dead weight.
Reads each `.beam` file's `imports` chunk to build a call graph, seeds
reachability from your app's modules + the OTP runtime essentials
(kernel/stdlib/elixir/logger/sasl), and reports:
* Libraries with zero reachable modules → safe to strip entirely
* Duplicate library versions → keep only the newest
* Foreign apps from another project's release tree → cache cruft
* Per-lib breakdown of reachable vs total module count + KB
## Usage
mix mob.audit_otp # audit the most recent release tree
mix mob.audit_otp --root path/to/otp # audit a specific tree
mix mob.audit_otp --json # machine-readable output
## Where the audit reads from
Looks for an OTP root in this order:
1. `--root` arg if given
2. `_build/mob_release/<App>.app/otp` (latest release build)
3. The iOS device cache at `~/.mob/cache/otp-ios-device-*`
## What this is NOT (yet)
Read-only. Does not modify the bundle. The companion `mix mob.release
--slim` will use the same audit to drive auto-stripping; this task is
the dry-run that lets you see what would happen.
"""
use Mix.Task
alias MobDev.OtpAudit
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args, strict: [root: :string, json: :boolean, app: :string])
root = resolve_root(opts[:root])
app_name = opts[:app] || infer_app_name()
Mix.shell().info("Auditing OTP tree: #{root}")
if app_name, do: Mix.shell().info("App entry point: #{app_name}\n")
report = OtpAudit.audit(root, app_name: app_name && String.to_atom(to_string(app_name)))
if opts[:json] do
report
|> Jason.encode!(pretty: true)
|> IO.puts()
else
print_report(report)
end
end
defp resolve_root(nil) do
candidates =
Path.wildcard("_build/mob_release/*.app/otp") ++
Path.wildcard(Path.expand("~/.mob/cache/otp-ios-device-*"))
case Enum.find(candidates, &File.dir?/1) do
nil ->
Mix.raise("""
No OTP tree found.
Run `mix mob.release --ios` first, or pass `--root path/to/otp`.
Searched:
#{Enum.map_join(candidates, "\n", &" #{&1}")}
""")
path ->
path
end
end
defp resolve_root(path), do: path
defp infer_app_name do
case Mix.Project.get() do
nil -> nil
_ -> Mix.Project.config()[:app]
end
end
defp print_report(r) do
h1 = IO.ANSI.bright()
dim = IO.ANSI.faint()
yellow = IO.ANSI.yellow()
red = IO.ANSI.red()
reset = IO.ANSI.reset()
Mix.shell().info("#{h1}=== Per-library breakdown ==={reset}")
Mix.shell().info("(libs sorted by total size; ✗ = nothing reachable, ◐ = partly used)")
Mix.shell().info("")
for lib <- r.libs do
icon =
cond do
lib.modules_reachable == 0 -> "#{red}✗#{reset}"
lib.modules_reachable < lib.modules_total -> "#{yellow}◐#{reset}"
true -> "✓"
end
version = if lib.version, do: "-#{lib.version}", else: ""
Mix.shell().info(
" #{icon} #{String.pad_trailing("#{lib.name}#{version}", 30)}" <>
" #{format_kb(lib.kb_total)}" <>
" #{lib.modules_reachable}/#{lib.modules_total} modules"
)
end
Mix.shell().info("")
Mix.shell().info("#{h1}=== Strippable (no reachable modules) ==={reset}")
if r.strippable_libs == [] do
Mix.shell().info(" #{dim}none — every shipped lib has at least one used module#{reset}")
else
for name <- r.strippable_libs do
lib = Enum.find(r.libs, &(&1.name == name))
Mix.shell().info(" #{red}#{name}#{reset} #{format_kb(lib.kb_total)}")
end
end
if map_size(r.duplicates) > 0 do
Mix.shell().info("\n#{h1}=== Duplicate library versions ==={reset}")
Mix.shell().info("(only the highest version is reachable; older ones are cache cruft)")
for {name, dupe_paths} <- r.duplicates, dupe_path <- dupe_paths do
Mix.shell().info(" #{yellow}#{name}#{reset} obsolete: #{Path.basename(dupe_path)}")
end
end
if r.foreign_apps != [] do
Mix.shell().info("\n#{h1}=== Foreign apps in lib/ ==={reset}")
Mix.shell().info("(other projects' code in your release — clean your OTP cache)")
for path <- r.foreign_apps do
Mix.shell().info(" #{red}#{Path.basename(path)}#{reset}")
end
end
duplicate_kb = duplicate_kb(r)
foreign_kb = foreign_kb(r)
estimated_savings = r.strippable_kb + duplicate_kb + foreign_kb
Mix.shell().info("\n#{h1}=== Summary ==={reset}")
Mix.shell().info(" Total shipped: #{format_kb(r.total_kb)}")
Mix.shell().info(" Reachable: #{format_kb(r.reachable_kb)}")
Mix.shell().info(" Strippable libs: #{format_kb(r.strippable_kb)}")
Mix.shell().info(" Duplicate versions: #{format_kb(duplicate_kb)}")
Mix.shell().info(" Foreign apps: #{format_kb(foreign_kb)}")
Mix.shell().info(" #{h1}Total potential savings: #{format_kb(estimated_savings)}#{reset}\n")
end
defp duplicate_kb(r) do
r.duplicates
|> Enum.flat_map(fn {_name, paths} -> paths end)
|> Enum.map(&dir_size_kb/1)
|> Enum.sum()
end
defp foreign_kb(r) do
r.foreign_apps |> Enum.map(&dir_size_kb/1) |> Enum.sum()
end
defp dir_size_kb(path) do
case System.cmd("du", ["-sk", path], stderr_to_stdout: true) do
{out, 0} -> out |> String.split() |> List.first() |> String.to_integer()
_ -> 0
end
end
defp format_kb(kb) when kb >= 1024, do: "#{Float.round(kb / 1024, 1)} MB"
defp format_kb(kb), do: "#{kb} KB"
end