Repository navigation
Releases: ayarodionov/otp_debug_mcp
Release list
v0.13.0
Tuning and products: a check of the VM's setup against its container
(vm_config, in diagnose), a gen_server's callers' waits without
telemetry (call_latency), socket options and traffic (socket_tuning),
and RabbitMQ (rabbitmq), each with a demo and a walkthrough; and the
walkthroughs as a regression suite of blind sessions
(scripts/blind_eval.py).
socket_tuning: every TCP and UDP socket on the node, its options and
statistics read twice a window apart, grouped by what owns them, with
the settings that cost latency or throughput: small writes with
Nagle on (warned when a message is written in pieces, by the writes
per read),{active, true}sockets whose owner's mailbox backs up,
senders blocked pasthigh_watermark(andsend_timeoutinfinity),
bulk reads through a small buffer, idle connections with keepalive
off, UDP receive buffers small for their traffic,delay_send. A
sockets demo (examples/sockets, in Docker, as Nagle's delay shows on
Linux loopback, not macOS) and walkthrough 35.vm_config, a check thatdiagnoseruns first: the VM's setup
against what it runs on. Schedulers online against the container's
CPU quota (cgroup v1 or v2) and its CPU affinity, with how often the
kernel throttled it (cpu.stat, since it started and over a second);
busy-waiting schedulers in a container; memory against the
container's limit; processes, ports, atoms, ETS tables and open files
(ulimit -n) against their limits; debug logging, no JIT, a changed
fullsweep_after; the emulator flags given (+S,+sbwt...). The
OTP 26 target in CI now runs with--cpus=1and+S 4:4, and checks
that it's seen. A container demo (examples/container) and
walkthrough 32.call_latency: how long one gen_server makes its callers wait, and
why, with no telemetry: calls by request, each split into the time it
waited in the mailbox and the time the server took (p50, p95, max),
casts and other messages by kind, the share of the window it was
busy, who waited longest, calls still unanswered. It traces that
process alone, in its own trace session on OTP 27+ (the legacy one
before, refused when something else traces there), with callbacks
traced by arity, so the server's state is never copied. An inventory
demo (examples/erlang/run.sh inventory) and walkthrough 33.rabbitmq: RabbitMQ, from a node of its cluster, through its own
APIs, sampled a window apart: version, cluster members running and
down, partitions; resource alarms (memory, disk), with memory and disk
against their limits; queues by messages, ready and unacknowledged,
consumers, memory and growth; channels holding the most unacknowledged
messages, with their prefetch; connections by state, and those in flow
control or blocked through the window. A RabbitMQ demo
(examples/rabbitmq, RabbitMQ in Docker and AMQP clients), scenario
26, and walkthrough 34. Tests against a RabbitMQ node in Docker, in
CI's new RabbitMQ target job.scripts/blind_eval.py: the walkthroughs as a regression suite. Each
case intest/blind/cases.jsonstarts a demo, gives a headless
claude -psession only the symptom and the otp-debug tools, and
checks that its conclusion names the cause; a report and the
transcripts are kept.
v0.12.0
Six tools: a sampling profiler (hot_spots) and a benchmark
(benchmark) for tuning code on a live node; two Erlang products,
EMQX (emqx) and ejabberd (ejabberd), each against its Docker image,
in CI too; Ranch listeners of any protocol (ranch); and Phoenix
applications, LiveView included (phoenix). Each has a demo, a
scenario and a walkthrough.
phoenix: a Phoenix application, sampled a window apart with its
telemetry between: endpoints (where they listen, their sockets,
code_reloader and the like); routers, and routes an earlier route
makes unreachable; LiveView processes by view (one per open page):
memory, how many grew and how fast, mailboxes, the busy ones, and for
the biggest, which assigns hold the memory; channels by module;
PubSub topics by subscribers; LiveView callbacks, channel messages
and requests over the window, slowest first. Thetelemetrytool
now groups LiveView and channel events by view, channel and event. A
LiveView demo (examples/elixir/live_shop,run.sh live: an orders
dashboard whose assigns keep every order), scenarios 1 and 2, and
walkthrough 31.ranch: Ranch listeners, whatever protocol they run, sampled a window
apart: limits and connections per connection supervisor, acceptors
waiting for room; connection processes grouped by where they are and
the first frame of the protocol's code; connections that did nothing
through the window (no reductions, no bytes), stuck in their
handshake, or with bytes waiting for a slow reader; connections
removed from the count; and connection processes that crashed, from
Ranch's log lines, by reason. A Ranch demo (examples/elixir,run.sh ranch: a point-of-sale server whose tills behind a NAT leave dead
connections open), scenario 15, and walkthrough 30.ejabberd: ejabberd, the XMPP server, through its own APIs and tables,
sampled a window apart: version, hosts, database, cluster nodes running
and down; users online by host; client connections (c2s processes)
backed up, by memory as well as mailbox (ejabberd's p1_server moves the
mailbox onto the heap), with their growth, whether their socket is
still open and what they're doing; users with the most offline
messages against theirmax_user_offline_messages(past it, messages
bounce); the message archive (mod_mam), on Mnesia its dets table's
size and how long one look at it takes; MUC rooms and ejabberd's own
processes with long mailboxes; s2s connections. An ejabberd demo
(examples/ejabberd, ejabberd in Docker and XMPP clients), scenario
26, and walkthrough 29. Tests against an ejabberd node in Docker, in
CI's new ejabberd target job.emqx: EMQX, the MQTT broker, through its own APIs and tables: version,
cluster, overload protection, its active alarms, listeners and how full
they are, clients and sessions; its counters over a window (messages
received and delivered, dropped by reason, sessions taken over or
discarded, authentication failures); clients that can't keep up (in
flight unacknowledged, queues full, drops, from its channel table);
client ids reconnecting; rules and actions failing or disconnected. An
EMQX demo (examples/emqx, EMQX in Docker and misbehaving MQTT
clients), scenario 26, and walkthrough 28. Tests against an EMQX node
in Docker, in CI's new EMQX target job.hot_spots: where a node's CPU goes, by function, without tracing: the
busiest processes' stacks sampled every 10 ms (and a scan for busy
short-lived ones), ranked by own time, time with callees and the first
frame of your code, with the hottest stacks, the busiest processes and
the share of time collecting garbage.benchmark: a function called on the node with the same arguments,
repeatedly, by fresh processes: time per call (p50 to max), reductions
per call, calls a second; several functions compared, each checked to
return what the first does, and several spawn options (a larger
min_heap_size) tried. A tuning demo (examples/erlang/run.sh tuning, with a tuned version to load and compare), scenario 3, and
walkthrough 27.
v0.11.0
Four tools for what an application runs on: Postgrex connection pools
(postgrex), Mnesia (mnesia), telemetry handlers
(telemetry_handlers) and shackle client pools (shackle), each with a
demo, a scenario and a walkthrough.
postgrex: the node's Postgrex connection pools (Ecto's Postgres repos
too), from DBConnection's own tables: each pool's connections up or
reconnecting, idle or checked out, who holds each checked-out one,
where in their code and whether in a transaction while doing something
else, and the callers waiting for a connection, how long and from
where. Withserver: true,pg_stat_activityand lock waits, through
a short-lived connection of the pool's own options, matched to the
pool's connections and callers. A Postgrex demo
(examples/elixir/run.sh postgrex, with its own PostgreSQL), scenario
2, and walkthrough 26. Tests run a throwaway PostgreSQL when its
binaries are there.mnesia: Mnesia on the node, through its ownsystem_infoand
table_info: its db nodes and which run, its tables (storage, size,
memory, copies, where to read them, growth), the transactions running
and their commits, aborts and restarts a second, and its own
processes' mailboxes. Through the window it samples the lock queue,
for the records transactions wait for a lock on and where in the code
their holders are, and subscribes to Mnesia's system events (overload,
inconsistent_database,mnesia_down). A Mnesia demo
(examples/erlang/run.sh mnesia), scenarios 4 and 9, and
walkthrough 25.telemetry_handlers: the telemetry handlers attached on the node and
what they cost. Each handler's events and function, and whether that
function exists; over a window, from sampling the processes' stacks,
how many processes are inside each handler at a time (which handler is
read from telemetry's own frame, as a handler may tail-call away) and
about how long it takes per event; handlers that raised and were
detached, from telemetry's[telemetry, handler, failure]event; one
function attached twice to an event; handlers attached per process or
request, piling up. A telemetry demo (examples/elixir/run.sh telemetry), scenario 2, and walkthrough 24.shackle: the node's shackle client pools (and the clients built on
shackle: marina, anchor, buoy): each pool's client, backend, size,
strategy, backlog size and retries, each connection up or reconnecting
with its requests in flight and its mailbox, and the callers waiting
for a reply grouped by where they called from, all read from shackle's
own counters without touching the connections. Over a window, shackle's
telemetry by client: replies and their times, timeouts, calls refused
with{error, no_server}and why (connections full or down), late
replies. Findings include one connection backed up while the others
idle: head-of-line blocking. A shackle demo
(examples/elixir/run.sh shackle), scenario 4, and walkthrough 23.diagnose'sstale_codeno longer reports modules compiled at run
time and loaded under a bare file name (foil's, under shackle;
mochiglobal's) as removed from disk: they never had a file.
v0.10.0
A tool for Yaws web servers (yaws), with a demo, a scenario and a
walkthrough.
yaws: the node's Yaws web servers: each listening server's virtual
servers and appmods, its connections againstmax_connections(past
which Yaws closes new connections at once), what its acceptor processes
are doing (accepting, pooled, waiting for a keep-alive request, or
handling one, which runsout/1), the requests in flight grouped by
where they wait and those still running at the end of the window; and
its cookie sessions: how many, how fast they grow, how many were never
used again after the request that started them, and expired ones kept
until the hourly sweep. A Yaws demo (examples/elixir/run.sh yaws),
scenarios 1 and 15, and walkthrough 22. Yaws is a test-only dependency,
fetched from GitHub; its build needs PAM headers.
v0.9.0
A tool for Cowboy web servers (cowboy), and a look at the riak_dt
CRDTs a riak_core node keeps (riak_core with crdts: true), each with
a demo, a scenario and a walkthrough.
cowboy: the node's Cowboy web servers, through Ranch: each
listener's connection limit (Ranch caps each connection supervisor, so
the real limit ismax_connections×num_conns_sups), acceptors
blocked waiting for room, which leaves new clients unaccepted with
nothing logged, idle and WebSocket connections, the requests in flight
grouped by where they wait and those still running at the end of the
window, timeouts and routes. Over the window, Ranch's accept and close
rates and, with cowboy_telemetry, requests by status, the slowest
routes, handler crashes and early errors. A Cowboy demo
(examples/elixir/run.sh cowboy), scenario 15 and walkthrough 21.riak_corewithcrdts: true: the riak_dt sets (ORSWOTs) and maps the
connected node's vnodes hold, in their state or the ETS tables they
own, as terms or riak_dt binaries: the largest, by key, with their
actors, elements or fields and deferred removes. Findings: more actors
than the ring has partitions (the actor is a client, a request or a
process rather than the vnode, so the clock grows with every update),
a value over 1 MB, deferred removes. Each vnode is asked for its state
(sys:get_state), so withOTP_DEBUG_MCP_CONFIRM=changesit asks
first; a busy vnode is skipped after 2 s. Without it,riak_coresays
when riak_dt is loaded.- A wishlist demo (
examples/elixir/run.sh wishlist): riak_dt sets whose
actor is each request's process. Scenario 16 and walkthrough 20.
v0.8.1
Approving an investigation step by step.
OTP_DEBUG_MCP_CONFIRM: to approve the investigation step by step.
allasks the user before every tool call,changesbefore each one
that does something on the node beyond reading it (connecting,
diagnose, tracing, profiling, attaching a handler,remote_eval...).
The dialog says what the step does, on which node, with which
arguments; declined, nothing runs and the agent is told to ask the user
how to go on. The answer counts only for that call, a client's "always
allow" doesn't skip it, and a client that can't ask gets a refusal.
v0.8.0
Tools for two kinds of system, a Phoenix or Bandit web server (bandit)
and a riak_core cluster (riak_core), a look inside ETS tables
(ets_table), and four more checks in diagnose: where the logs go,
where scheduler time goes, code that isn't what's on disk, and traffic
between nodes. Each comes with a demo, a scenario and a walkthrough.
riak_core: a riak_core cluster (Riak KV, riak_core_lite, and systems
built on them) from one member, through riak_core's own API: the
members, their status and whether this node reaches them, the claimant,
partition ownership and the ownership changes awaiting handoff, this
node's vnodes (primaries, and fallbacks held for unreachable owners),
the longest vnode mailboxes on every member it reaches, of the vnodes
and their proxies, handoffs running, the services up on each member, and
riak_core's own ringready and transfers. Findings, worst first: a member
or the claimant unreachable, a vnode past or near its overload
threshold, a vnode module registered under an application that isn't
running (riak_core then never hands its vnodes off), handoffs off while
changes wait.bandit: Phoenix and Bandit web servers, through Thousand Island
under them. For each server: where it listens, its connection limit
(acceptors × connections per acceptor) and how full it is, in total
and per acceptor; acceptors waiting for room for a new connection,
while new clients wait unaccepted; its timeouts; and its connections,
idle keep-alives, reading, sending or handling a request, the busy ones
grouped by the function they wait in and the first frame of the
application's code. Then, over a window, from its telemetry: requests
per second by status, the slowest routes, requests that raised, socket
errors by reason, and connections refused at the limit, which Thousand
Island doesn't log. Findings, worst first: refused connections are
critical.telemetrygroups Bandit's requests by method, path and status
(GET /products/:id -> 200), failed ones by their error, and Thousand
Island's events by their error; Bandit's exception events now show
their exception.ets_table: what's in one ETS table. Its type, protection, owner and
heir, row count and memory, its first rows, and a scan of up to 10,000
rows (100,000 at most) read in chunks: the average row, the largest
rows, and for a bag the keys with the most rows. A large row comes back
as its key and size only, and a private table, which only its owner can
read, as its settings only.logscheck, indiagnose: where the node's logs go (its primary
level, its handlers and what they write to, the files found with their
size and last write). With no file, it says that errors from before
anyone listened are gone and to start arecent_errorswatch early; a
primary level that hides errors is a warning.stale_codecheck, indiagnose: modules with old code still loaded
after a hot load, and the processes still running it (killed by the
module's next load), and loaded modules whose.beamon the code path
is different (a deploy that was never loaded, or code loaded by hand),
or whose file is gone. A module merely off the code path, as Mix leaves
Hex's, isn't flagged, nor is recon when connect injected it.distributioncheck, indiagnose: the node's connections to other
nodes over 1 s, with their type and state, bytes sent and received per
second, and bytes waiting to be sent against the VM's busy limit; a
connection near the limit, or stuck pending, is a warning. This tool's
own connection is marked.scheduler_timecheck, indiagnose: where the time of each
kind of VM thread goes over 2 s, from microstate accounting: Erlang
code, garbage collection, ports, I/O polling, and busy-waiting (in
other).gc_share_of_workcounts the dirty CPU schedulers too, where
large heaps are collected, andgc_schedulersis the schedulers' worth
of time garbage collection took: 30% of the work and half a scheduler
or more is a warning, whatever the machine's size. Accounting is turned on only for the window,
and left alone if someone else has it on. It runs alongside
long_schedules' 2 s window, sodiagnosetakes no longer.
v0.7.0
Found by running the "Plausible but wrong" examples of the books
Learning Erlang from Claude and Learning Elixir from Claude on live nodes
(examples/books, docs/plausible-but-wrong.md): deadlocks that a
timeout already ended, the exceptions of one process wherever they're
raised, and a fix for call_chains on the commonest deadlock.
- Fixed:
call_chains, and with itdiagnose, failed with "Internal
error" while processes waited ingen_server:call/2(the default
timeout) orTask.await, so a deadlock of two gen_servers calling each
other the plain way, the commonest kind, broke the check that finds it.
It now reads the request and timeout ofgen_server:call/2and of
gen_statem:call/3withinfinitytoo. recent_errorsreportsdeadlocks: processes that crashed within 2
seconds of each other, each while calling the next in a cycle, with each
one's request. That's a deadlock a call's timeout broke, which
call_chainssees only while it lasts.trace_callstakes aprocess: only the calls that process makes.
Withexceptions: truethe module can be left out, to see every
exported function and BIF that raises in that process, even when it
catches the exception (5 s by default, at most 10, since every call on
the node is a little slower meanwhile). Withexceptions: true, each
failing call also says where it was called from, with the line on OTP
27+.telemetrywithlist: truesays when no handler is attached to any
event on the node: what's left when telemetry has detached a handler
that raised.
v0.6.0
Debugging native code and ports, errors and crashes that already
happened (recent_errors, crash_dump), deadlocks and chains of calls
(call_chains), Elixir libraries' telemetry (telemetry), and the calls
that fail (trace_calls with exceptions: true).
Deadlocks, telemetry and failing calls
call_chains(indiagnose): who waits on whom. It finds the processes
waiting in a call (gen_server,GenServer,gen_statem), in
Task.await, or instart_linkfor an init, and the process each
waits on. Sampling twice, a second apart, it keeps the calls still
pending on the same monitor and reports deadlocks (cycles), each
process with its request, its timeout and how many others wait on it;
chains of calls, ending at the process that holds them up; and the
processes most waited on. Critical for a deadlock.telemetry: what the node's:telemetryevents say over a window.
Phoenix requests by route, Ecto queries by table and query, Oban jobs
by worker, HTTP client requests by host, each with count, p50/p95/max
of each duration (Ecto'squeue_timetoo) and the slowest example; the
errors of exception events; the events that didn't happen. Without
events it listens to those the node's handlers listen to and the known
events of the libraries running there (list: truelists them). A
handler that keeps only small metadata sends events to a collector on
the node, capped, detached afterwards; nothing is loaded.trace_callswithexceptions: true: only the calls that raise, each
with its arguments and the exception, even when the caller catches it.
In its own trace session on OTP 27+; before, in the legacy session,
refused while others trace.examples/erlang/run.sh locks: two gen_servers that deadlock, with
shoppers stuck behind them; CI smoke-tests the release against it. The
orders demo also has a coupon service that fails silently. Scenarios 2,
4 and 14 cover the new tools, and walkthroughs 11, 12 and 13 are
recorded sessions with them.
Errors and crash dumps
recent_errors: the errors the node logs, grouped by who failed and
why. A crash group is one process and reason, however often, with its
stack, the arguments of afunction_clause, the reports it caused and
how many processes it was; a log group is one message from one place,
with where it was logged. It listens for a window, or with
watch_minutesin the background, read by later calls and ended with
stop: true. It also groups the errors in the start and the end of the
node's log files (its logger handlers' files, and a release's
erlang.log.N), which hold what happened before. Nothing is loaded on
the node: a logger handler whose filter is evaluated code sends the
events to a process there, and is removed when it ends, even if that
process is killed.crash_dump: why a node died, from itserl_crash.dump, read in one
pass, so dumps of gigabytes work. What the slogan means (out of memory
and in which allocator, a permanent application that stopped, a full
atom table, a failed boot, a signal), findings on what stands out, the
process that was running, the biggest processes and longest mailboxes
with their stacks and what proc_lib processes run, the first messages
of the longest mailbox decoded, and memory, ETS, ports and atoms.
process:shows one process in full. Dumps of OTP 26 to 29 are read;
no connection is needed.supervisor_restartspoints torecent_errorsfor children that crash
less often than its sample catches.examples/erlang/run.sh orders: an orders service whose errors only its
logs show;examples/erlang/run.sh crash: a node that runs out of
memory within a minute and leaves a crash dump. CI smoke-tests the
release against both.docs/scenarios.mdhas a scenario for each (7
and 8; the ones after them are renumbered), anddocs/walkthroughs.md
a recorded session for each (9 and 10).
Ports
ports(indiagnose) tells what the ports are: by type (tcp_inet,
external programs by command, drivers), the processes holding the most,
the ports with data waiting to be written, withowner_blockedwhen the
owner is stuck sending to a slow peer, and the external programs with
their OS pids, OTP's own helpers apart. It also counts sockets of the
socketmodule, which aren't ports.- Snapshot diffs report
port_delta_by_typeandport_delta_by_owner:
which kind of port grew, and which processes opened them, named. socket_traffic(indiagnose): the inet sockets moving the most bytes
over a second, with peer and local address and owner.port_info: one port in detail. For a socket, its addresses, statistics
(send_pend) and options (active,packet, buffers, watermarks); for
an external program, its OS pid; its owner, links, I/O and queue.trace_port: a port's traffic (what its owner sends it, what it sends
back, truncated), or withopened: truethe ports opened and closed in
a window, summed up by who opened them. Capped liketrace_calls; in its
own trace session on OTP 27+, refused before while others trace.long_schedulesnames the driver and owner of a port in its events.examples/erlang/run.sh ports: a shop that leaks sockets and external
programs and fills a slow peer's socket. CI smoke-tests the release
against it.docs/scenarios.mdhas a scenario for ports (now 12), and
docs/walkthroughs.mdan eighth recorded
session against the demo.
Native code
long_schedules(indiagnose): over 2 seconds, the processes that
held a scheduler for more than 10 ms without yielding, which is native
code (a NIF or BIF), with each process's name, initial call and the
longest and total time; plus garbage collections over 50 ms and
processes suspended on busy ports. On OTP 28+ it runs in its own trace
session (trace:system/3), so the node'ssystem_monitoris left
alone; before OTP 28 it usessystem_monitor, and skips the check if
another process holds it.schedulersalso measures the dirty CPU and I/O schedulers, which dirty
NIFs run on, and their run queues, and warns when the dirty CPU
schedulers are saturated with work waiting.native_memory(indiagnose): the memory the OS charges the node
against what the VM's allocators hold. The difference is memory NIFs and
drivers allocate themselves, whicherlang:memory()never shows.
Snapshot diffs report its growth,native_memory_delta_mb, and
os_memory_delta_mb. The OS's figure counts memory compressed or
swapped out: macOS's physical footprint,VmRSSplusVmSwapon Linux.
Resident memory alone can miss a leak under memory pressure, since
leaked memory nobody touches is the first to be compressed or swapped.native_code(indiagnose): the native libraries in applications'
privdirectories, OTP's own apart, and the loaded port drivers.module_codemarks NIFs: functions in-nifs, or whose stub calls
erlang:nif_error, with a note that the code shown is only the stub.examples/erlang/run.sh native: an OTP application with a C NIF
library whosechecksum/1holds a scheduler for 80 ms and whose
parse/1leaks 256 KB a call. CI smoke-tests the release against it.
docs/scenarios.mdhas a scenario for native code (9; the ones after
it are renumbered), anddocs/walkthroughs.mda seventh recorded
session against the demo.
Other changes
module_codelabels each anonymous fn uniquely (anonymous fn/1 in init/1 (fun-2)), and takes a function as listed, as name/arity, or by
its compiled name (-init/1-fun-2-), quoted or not. Compiled names
with slashes used to be refused.- The Erlang demos are compiled with debug info, as rebar3 builds are, so
module_codeshows their code as Erlang. module_code: what a module on the target really contains, including
the functions macros generated, which its source doesn't show. It lists
each function's origin: written in the module, or generated by a macro
(which macro's module, the line that called it and, withquote location: :keep, the line in the macro). It shows a function's code
after macro expansion, and which function a stack frame'sfile:lineis
in. It reads the module's.beamfrom the node's code path and decodes
it here. With Elixir debug info, code is Elixir; with Erlang's, Erlang.
A release strips debug info by default, and then the line table, which
it keeps, still gives each function's file, and code is BEAM assembly.
A node on a newer OTP than the server's disassembles its own.beam.- A macro is listed and shown under the name its source gives it
(timed/2, kinddefmacro), not as the compiledMACRO-timed/3, with
a note that its code after expansion is the code that builds what it
returns. Function heads are formatted so a[do: block]argument stays
an argument. examples/elixirhas a billing demo (run.sh billing) whose routes are
defined with a macro DSL, and whosetimedmacro records every charge
twice: a bug that exists only after expansion.docs/scenarios.mdhas a
scenario for code that macros generated, anddocs/walkthroughs.mda
fifth recorded session, finding that bug from the node alone.examples/elixir/phoenix_shop(run.sh phoenix): a Phoenix 1.8 JSON
API whose product search always returns 404, because theGET /products/:idroute thatresourcesgenerates matches it first. The
source never mentions:id, and it compiles without a warning;
module_codeshows the router's expanded__match_route__/3clauses
in matching order.module_codereports a function some macros define without recording
it as theirs, such as Phoenix.Router's__match_route__/3, as
"probably a macro of" the macro called on the line it's defined on,
rather than as written there.docs/walkthroughs.mdhas a sixth recorded session: the Phoenix demo's
...
v0.5.0
Profiling by function with profile, a Docker image of the server, and
debugging nodes in Docker containers and Kubernetes pods.
Profiling
profile: where the CPU time goes on the target, by function. For one
module, one function or one process (or, when asked, every module), it
counts calls, own time and memory allocated per function over a short
window (default 5s, at most 30s), with the runtime's call_time and
call_memory counters, and ranks them, with the processes making the
calls. On OTP 27+ it runs in its own trace session and never sees or
disturbs other tracing; on older nodes it refuses while there is any,
unless forced. It needs nothing from OTP'stoolsapplication, which
releases usually leave out, and leaves no counters or trace flags
behind. Anonymous functions are named as Elixir does
(anonymous fn/1 in MyMod.fun/1). The smoke test profiles each demo.
Docker and Kubernetes
- A Docker image of the server,
ghcr.io/ayarodionov/otp_debug_mcp,
for amd64 and arm64, published with each release (0.4.0 too) and
smoke-tested against a node in another container first. The
Dockerfilebuilds it from a published Linux release. docs/production.mdcovers nodes in Docker containers. Either run the
image in a container that shares the app container's network, which
needs no changes to the app and no published ports, or publish the
node's distribution port and have the server dial it, configured for a
Mix release by environment variables alone. There's a prompt that has
Claude Code set up the first.- Nodes in Kubernetes pods. The release has
bin/otp-debug-k8s, which
Claude Code runs as the MCP server: it starts the server as a pod in
the app's namespace, from the release's Docker image, with the cookie
from the app's Secret, and deletes the pod when the session ends, even
if the session is stopped.docs/production.mdhas the set-up, the
smallest Role it needs (no access to Secrets), and port forwarding.
Tested on a kind cluster with nodes named by pod IP and by DNS name. - The server can start distribution where the machine has no fully
qualified host name, such as in a container or a pod. Connecting to a
node with a long name used to fail there withnodistribution; the
server now falls back to naming itself by the address it reaches the
target from.
Docs
- The README's Setup says how to stop, restart and remove the server:
/mcp's Reconnect and Disable in Claude Code, the commands that undo
the install (including theepmdthe server leaves running), and
prompts that have Claude Code remove or upgrade it.