init
This commit is contained in:
175
lib/forum/processes.ex
Normal file
175
lib/forum/processes.ex
Normal file
@@ -0,0 +1,175 @@
|
||||
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
|
||||
Reference in New Issue
Block a user