defmodule DalaDev.Server.ObserverLive.Processes do @moduledoc """ LiveView for process list display with sorting and filtering. """ use Phoenix.LiveView, layout: {DalaDev.Server.Layouts, :app} alias DalaDev.Observer @refresh_interval 5_000 @page_size 100 def mount(_params, _session, socket) do if connected?(socket) do :timer.send_interval(@refresh_interval, self(), :refresh) end socket = socket |> assign(:node, Node.self()) |> assign(:available_nodes, [Node.self() | Node.list()]) |> assign(:processes, []) |> assign(:filtered_processes, []) |> assign(:error, nil) |> assign(:loading, false) |> assign(:sort_by, "memory") |> assign(:sort_order, :desc) |> assign(:filter, "") |> assign(:selected_pid, nil) {:ok, fetch_processes(socket)} end def handle_params(%{"node" => node_str}, _uri, socket) do try do node = String.to_existing_atom(":#{node_str}") {:noreply, assign(socket, :node, node) |> fetch_processes()} rescue _ -> {:noreply, assign(socket, :error, "Invalid node name: #{node_str}")} end end def handle_params(_params, _uri, socket), do: {:noreply, socket} def handle_info(:refresh, socket), do: {:noreply, fetch_processes(socket)} def handle_event("refresh", _params, socket), do: {:noreply, fetch_processes(socket)} def handle_event("select_node", %{"node" => node_str}, socket) do try do node = String.to_existing_atom(node_str) {:noreply, assign(socket, :node, node) |> fetch_processes()} rescue _ -> {:noreply, assign(socket, :error, "Invalid node: #{node_str}")} end end def handle_event("sort", %{"by" => sort_by}, socket) do current_sort = socket.assigns[:sort_by] {new_sort, new_order} = if sort_by == current_sort do {sort_by, toggle_order(socket.assigns[:sort_order])} else {sort_by, :desc} end socket = socket |> assign(:sort_by, new_sort) |> assign(:sort_order, new_order) {:noreply, apply_sort_and_filter(socket)} end def handle_event("filter", %{"filter" => filter}, socket) do {:noreply, assign(socket, :filter, filter) |> apply_sort_and_filter()} end def handle_event("select_process", %{"pid" => pid_str}, socket) do {:noreply, assign(socket, :selected_pid, pid_str)} end def render(assigns) do ~H"""
← Back to Dashboard

Processes: <%= @node %>

<%= length(@filtered_processes) %> / <%= length(@processes) %> processes
<%= if @error do %>

<%= @error %>

<% end %> <%= if @loading do %>
Loading...
<% end %>
<%= for proc <- @filtered_processes do %> (if @selected_pid == proc.pid, do: "bg-zinc-800", else: "")} phx-click="select_process" phx-value-pid={proc.pid}> <% end %>
PID Name Memory <%= if @sort_by == "memory", do: sort_indicator(@sort_order) %> Reductions <%= if @sort_by == "reductions", do: sort_indicator(@sort_order) %> Msg Queue <%= if @sort_by == "message_queue", do: sort_indicator(@sort_order) %> Current Function Status
<%= proc.pid %> <%= proc.name || proc.registered_name || "-" %> <%= format_bytes(proc.memory) %> <%= format_number(proc.reductions) %> 0, do: "text-yellow-400", else: ""}> <%= proc.message_queue_len %> <%= proc.current_function %> (if proc.status == :running, do: "bg-green-900 text-green-300", else: "bg-zinc-700 text-zinc-300")}> <%= proc.status %>
<%= if @selected_pid && find_process(@processes, @selected_pid) do %> <% proc = find_process(@processes, @selected_pid) %>

Process Details

PID
<%= proc.pid %>
Name
<%= proc.name || proc.registered_name || "-" %>
Memory
<%= format_bytes(proc.memory) %>
Reductions
<%= format_number(proc.reductions) %>
Message Queue
<%= proc.message_queue_len %>
Current Function
<%= proc.current_function %>
Status
<%= proc.status %>
<% end %>
""" end defp fetch_processes(socket) do node = socket.assigns[:node] socket = socket |> assign(:loading, true) |> assign(:available_nodes, [Node.self() | Node.list()]) case Observer.process_list(node) do processes when is_list(processes) -> socket |> assign(:processes, processes) |> assign(:error, nil) |> assign(:loading, false) |> apply_sort_and_filter() _ -> assign(socket, :error, "Failed to get process list for #{inspect(node)}") |> assign(:loading, false) end end defp apply_sort_and_filter(socket) do processes = socket.assigns[:processes] sort_by = socket.assigns[:sort_by] sort_order = socket.assigns[:sort_order] filter = socket.assigns[:filter] filtered = processes |> Enum.filter(&matches_filter?(&1, filter)) |> sort_processes(sort_by, sort_order) |> Enum.take(@page_size) assign(socket, :filtered_processes, filtered) end defp sort_processes(processes, "memory", :desc), do: Enum.sort_by(processes, & &1.memory, &>=/2) defp sort_processes(processes, "memory", :asc), do: Enum.sort_by(processes, & &1.memory, &<=/2) defp sort_processes(processes, "reductions", :desc), do: Enum.sort_by(processes, & &1.reductions, &>=/2) defp sort_processes(processes, "reductions", :asc), do: Enum.sort_by(processes, & &1.reductions, &<=/2) defp sort_processes(processes, "message_queue", :desc), do: Enum.sort_by(processes, & &1.message_queue_len, &>=/2) defp sort_processes(processes, "message_queue", :asc), do: Enum.sort_by(processes, & &1.message_queue_len, &<=/2) defp sort_processes(processes, _, _), do: processes defp matches_filter?(_proc, ""), do: true defp matches_filter?(proc, filter) do String.contains?(proc.pid, filter) || (proc.name && String.contains?(proc.name, filter)) || (proc.registered_name && String.contains?(proc.registered_name, filter)) || String.contains?(proc.current_function, filter) end defp toggle_order(:asc), do: :desc defp toggle_order(:desc), do: :asc defp find_process(processes, pid) do Enum.find(processes, &(&1.pid == pid)) end defp format_bytes(nil), do: "0 B" defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 1)} KB" defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024, do: "#{Float.round(bytes / (1024 * 1024), 1)} MB" defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024 * 1024), 1)} GB" defp format_number(n) when n < 1000, do: "#{n}" defp format_number(n) when n < 1_000_000, do: "#{Float.round(n / 1000, 1)}K" defp format_number(n), do: "#{Float.round(n / 1_000_000, 1)}M" defp sort_indicator(:asc), do: " ↑" defp sort_indicator(:desc), do: " ↓" end