This commit is contained in:
Sam
2026-06-10 11:51:56 -05:00
commit 66ba338b81
57 changed files with 5509 additions and 0 deletions

175
lib/forum/processes.ex Normal file
View 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