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