176 lines
5.4 KiB
Elixir
176 lines
5.4 KiB
Elixir
defmodule Forum.Processes do
|
|
@moduledoc """
|
|
Snapshot of every BEAM process for the /processes endpoint. Pure data
|
|
function — the router serves a shell HTML and the WS handler returns
|
|
the rows produced here.
|
|
"""
|
|
|
|
@doc """
|
|
Returns a list of maps, one per live process, sorted by memory desc.
|
|
|
|
Each map has string keys (JSON-friendly):
|
|
"pid" — inspected PID string, e.g. "#PID<0.123.0>"
|
|
"parent" — inspected PID string of immediate parent (from
|
|
$ancestors), or nil for roots / non-proc_lib processes
|
|
"name" — registered name string, or nil
|
|
"initial_call" — real $initial_call MFA, formatted as "Mod.fun/arity"
|
|
"memory_kb" — process memory in KB
|
|
"tree_memory_kb" — process memory plus all descendant process memory in KB
|
|
"msgs" — message queue length
|
|
"status" — :running | :runnable | :waiting | :suspended | :exiting
|
|
"mine" — true if the process is under Forum.Supervisor's tree
|
|
"""
|
|
# Registries to consult for "via" names — processes registered as
|
|
# {:via, Registry, {Foo, key}} don't show up under :registered_name,
|
|
# so we look them up by pid here and use the key as the display name.
|
|
@registries [
|
|
Forum.ProcessRegistry,
|
|
Forum.FormsSubscriberRegistry
|
|
]
|
|
|
|
def list do
|
|
my_pids = my_pids()
|
|
|
|
Process.list()
|
|
|> Enum.map(&info_for(&1, my_pids))
|
|
|> Enum.reject(&is_nil/1)
|
|
|> add_tree_memory()
|
|
|> Enum.sort_by(& &1["memory_kb"], :desc)
|
|
end
|
|
|
|
defp add_tree_memory(rows) do
|
|
by_parent = Enum.group_by(rows, & &1["parent"])
|
|
{totals, _memo} =
|
|
Enum.map_reduce(rows, %{}, fn row, memo ->
|
|
{total, memo} = tree_memory(row, by_parent, memo)
|
|
{{row["pid"], total}, memo}
|
|
end)
|
|
|
|
totals = Map.new(totals)
|
|
|
|
Enum.map(rows, fn row ->
|
|
Map.put(row, "tree_memory_kb", Map.fetch!(totals, row["pid"]))
|
|
end)
|
|
end
|
|
|
|
defp tree_memory(row, by_parent, memo) do
|
|
pid = row["pid"]
|
|
|
|
case memo do
|
|
%{^pid => total} ->
|
|
{total, memo}
|
|
|
|
%{} ->
|
|
children = Map.get(by_parent, pid, [])
|
|
|
|
{children_total, memo} =
|
|
Enum.reduce(children, {0, memo}, fn child, {sum, memo} ->
|
|
{total, memo} = tree_memory(child, by_parent, memo)
|
|
{sum + total, memo}
|
|
end)
|
|
|
|
total = row["memory_kb"] + children_total
|
|
{total, Map.put(memo, pid, total)}
|
|
end
|
|
end
|
|
|
|
defp info_for(pid, my_pids) do
|
|
case Process.info(pid, [
|
|
:registered_name,
|
|
:initial_call,
|
|
:memory,
|
|
:message_queue_len,
|
|
:status,
|
|
:dictionary
|
|
]) do
|
|
nil ->
|
|
nil
|
|
|
|
info ->
|
|
dict = info[:dictionary] || []
|
|
real_call = Keyword.get(dict, :"$initial_call") || info[:initial_call]
|
|
ancestors = Keyword.get(dict, :"$ancestors", [])
|
|
|
|
name =
|
|
format_name(info[:registered_name]) || lookup_registry_name(pid)
|
|
|
|
%{
|
|
"pid" => inspect(pid),
|
|
"parent" => parent_string(ancestors),
|
|
"name" => name,
|
|
"initial_call" => format_mfa(real_call),
|
|
"memory_kb" => div(info[:memory] || 0, 1024),
|
|
"msgs" => info[:message_queue_len] || 0,
|
|
"status" => info[:status],
|
|
"mine" => MapSet.member?(my_pids, pid)
|
|
}
|
|
end
|
|
end
|
|
|
|
defp lookup_registry_name(pid) do
|
|
@registries
|
|
|> Enum.flat_map(fn registry ->
|
|
try do
|
|
Registry.keys(registry, pid) |> Enum.map(&format_registry_key/1)
|
|
rescue
|
|
# Registry not started yet (boot race) — skip silently.
|
|
ArgumentError -> []
|
|
end
|
|
end)
|
|
|> case do
|
|
[] -> nil
|
|
names -> Enum.join(names, ", ")
|
|
end
|
|
end
|
|
|
|
# {"Task", "1"} -> "Task#1". Falls back to inspect for unknown shapes.
|
|
defp format_registry_key({:form, {class, id}}) when is_binary(class), do: "#{class}##{id}"
|
|
defp format_registry_key({:network, slug}), do: "network:#{slug}"
|
|
defp format_registry_key({:public_site, network}), do: "public-site:#{network}"
|
|
defp format_registry_key({:http_server, :main}), do: "http-server:main"
|
|
defp format_registry_key({:http_server, {:network, network}}), do: "http-server:#{network}"
|
|
defp format_registry_key(:forms), do: "forms-subscriber:forms"
|
|
defp format_registry_key(other), do: inspect(other)
|
|
|
|
# The first entry in $ancestors is the immediate parent. proc_lib
|
|
# stores PIDs directly; for processes registered by name it can also
|
|
# be the registered name (atom), which we resolve to its current PID.
|
|
defp parent_string([pid | _]) when is_pid(pid), do: inspect(pid)
|
|
|
|
defp parent_string([name | _]) when is_atom(name) do
|
|
case Process.whereis(name) do
|
|
nil -> nil
|
|
pid -> inspect(pid)
|
|
end
|
|
end
|
|
|
|
defp parent_string(_), do: nil
|
|
|
|
defp my_pids do
|
|
case Process.whereis(Forum.Supervisor) do
|
|
nil -> MapSet.new()
|
|
sup -> MapSet.new([sup | all_descendants(sup)])
|
|
end
|
|
end
|
|
|
|
defp all_descendants(sup) do
|
|
Supervisor.which_children(sup)
|
|
|> Enum.flat_map(fn {_id, pid, type, _modules} ->
|
|
cond do
|
|
not is_pid(pid) -> []
|
|
type == :supervisor -> [pid | all_descendants(pid)]
|
|
true -> [pid]
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp format_name([]), do: nil
|
|
defp format_name(name) when is_atom(name), do: inspect(name)
|
|
defp format_name(_), do: nil
|
|
|
|
defp format_mfa({m, f, a}) when is_atom(m) and is_atom(f) and is_integer(a),
|
|
do: "#{inspect(m)}.#{f}/#{a}"
|
|
|
|
defp format_mfa(_), do: "-"
|
|
end
|