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"""
<%= if @error do %>
<% end %>
<%= if @loading do %>
Loading...
<% 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 |
<%= for proc <- @filtered_processes do %>
(if @selected_pid == proc.pid, do: "bg-zinc-800", else: "")}
phx-click="select_process" phx-value-pid={proc.pid}>
| <%= 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 %>
|
<% end %>
<%= 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