Skip to content
117 changes: 102 additions & 15 deletions lib/live_debugger/gen_servers/callback_tracing_server.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
defmodule LiveDebugger.GenServers.CallbackTracingServer do
@moduledoc """
This gen_server is responsible for tracing the callbacks of the LiveView processes.
This gen_server is responsible for tracing callbacks and managing ETS tables.
"""

use GenServer

require Logger

alias LiveDebugger.Services.TraceService
alias LiveDebugger.Services.ModuleDiscoveryService
alias LiveDebugger.Services.ChannelService
alias LiveDebugger.Services.TraceService
alias LiveDebugger.Structs.Trace
alias LiveDebugger.Services.System.ProcessService
alias LiveDebugger.Utils.Callbacks, as: CallbackUtils
alias LiveDebugger.Utils.PubSub, as: PubSubUtils

@ets_table_name :lvdbg_traces
@callback_functions CallbackUtils.callbacks_functions()

@type table_refs() :: %{pid() => :ets.table()}

## API

@doc """
Returns ETS table reference.
It creates table if none is associated with given pid
"""
@spec table!(pid :: pid()) :: :ets.table()
def table!(pid) when is_pid(pid) do
GenServer.call(__MODULE__, {:get_or_create_table, pid}, 1000)
end

@doc """
If table for given `pid` exists it deletes it from ETS.
"""
@spec delete_table!(pid :: pid()) :: :ok
def delete_table!(pid) when is_pid(pid) do
GenServer.call(__MODULE__, {:delete_table, pid}, 1000)
end

@doc """
Checks if GenServer has been loaded
"""
@spec ping!() :: :ok
def ping!() do
GenServer.call(__MODULE__, :ping)
end

## GenServer

@doc false
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
Expand All @@ -25,12 +58,12 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
tracing_setup_delay = Application.get_env(:live_debugger, :tracing_setup_delay, 0)
Process.send_after(self(), :setup_tracing, tracing_setup_delay)

{:ok, []}
{:ok, %{}}
end

@impl true
def handle_info(:setup_tracing, state) do
:dbg.tracer(:process, {&trace_handler/2, 0})
def handle_info(:setup_tracing, table_refs) do
:dbg.tracer(:process, {&handle_trace/2, 0})
:dbg.p(:all, :c)

all_modules = ModuleDiscoveryService.all_modules()
Expand All @@ -50,16 +83,69 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
# We trace it to refresh the components tree
:dbg.tp({Phoenix.LiveView.Diff, :delete_component, 2}, [])

{:noreply, state}
{:noreply, table_refs}
end

Comment thread
GuzekAlan marked this conversation as resolved.
@impl true
def handle_info({:DOWN, _, :process, closed_pid, _}, table_refs) do
{_, table_refs} = delete_ets_table(closed_pid, table_refs)

closed_pid
|> PubSubUtils.process_status_topic()
|> PubSubUtils.broadcast({:process_status, :dead})

{:noreply, table_refs}
end

@impl true
def handle_call({:get_or_create_table, pid}, _from, table_refs) do
case Map.get(table_refs, pid) do
nil ->
ref = create_ets_table()
Process.monitor(pid)
{:reply, ref, Map.put(table_refs, pid, ref)}

ref ->
{:reply, ref, table_refs}
end
end

@impl true
def handle_call({:delete_table, pid}, _from, table_refs) do
{_, table_refs} = delete_ets_table(pid, table_refs)
{:reply, :ok, table_refs}
end

@impl true
def handle_call(:ping, _from, state) do
{:reply, :ok, state}
end

@spec create_ets_table() :: :ets.table()
defp create_ets_table() do
:ets.new(@ets_table_name, [:ordered_set, :public])
end

@spec delete_ets_table(pid(), table_refs()) :: {boolean(), table_refs()}
defp delete_ets_table(pid, table_refs) do
case Map.pop(table_refs, pid) do
{nil, table_refs} ->
{false, table_refs}

{ref, updated_table_refs} ->
:ets.delete(ref)
{true, updated_table_refs}
end
end

# This handler is heavy because of fetching state and we do not care for order because it is not displayed to user
# Because of that we do it asynchronously to speed up tracer a bit
# We do not persist this trace because it is not displayed to user
defp trace_handler({_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}}, n) do
@spec handle_trace(term(), n :: integer()) :: integer()
defp handle_trace({_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}}, n) do
Task.start(fn ->
with cid <- %Phoenix.LiveComponent.CID{cid: cid},
{:ok, %{socket: socket}} <- ProcessService.state(pid),
{:ok, %{socket: socket}} <- ChannelService.state(pid),
%{id: socket_id, transport_pid: transport_pid} <- socket,
true <- is_pid(transport_pid),
trace <-
Expand All @@ -82,7 +168,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do

# This handles callbacks created by user that will be displayed to user
# It cannot be async because we care about order
defp trace_handler({_, pid, _, {module, fun, args}}, n) when fun in @callback_functions do
defp handle_trace({_, pid, _, {module, fun, args}}, n) when fun in @callback_functions do
with trace <- Trace.new(n, module, fun, args, pid),
true <- is_pid(trace.transport_pid),
:ok <- persist_trace(trace) do
Expand All @@ -92,15 +178,14 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
n - 1
end

defp trace_handler(trace, n) do
defp handle_trace(trace, n) do
Logger.info("Ignoring unexpected trace: #{inspect(trace)}")
n
end

defp persist_trace(trace) do
trace.transport_pid
|> TraceService.ets_table_id(trace.socket_id)
|> TraceService.insert(trace.id, trace)
@spec persist_trace(Trace.t()) :: :ok | {:error, term()}
defp persist_trace(%Trace{} = trace) do
TraceService.insert(trace)

:ok
rescue
Expand All @@ -109,6 +194,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
{:error, err}
end

@spec publish_trace(Trace.t()) :: :ok | {:error, term()}
Comment thread
kraleppa marked this conversation as resolved.
defp publish_trace(%Trace{} = trace) do
do_publish(trace)
:ok
Expand All @@ -118,6 +204,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
{:error, err}
end

@spec do_publish(Trace.t()) :: :ok
defp do_publish(%{module: Phoenix.LiveView.Diff} = trace) do
trace
|> PubSubUtils.component_deleted_topic()
Expand Down
10 changes: 8 additions & 2 deletions lib/live_debugger/live_views/channel_dashboard_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ defmodule LiveDebugger.LiveViews.ChannelDashboardLive do
end

def handle_async(:fetch_lv_process, {:ok, fetched_lv_process}, socket) do
Process.monitor(fetched_lv_process.pid)
subscribe_process_state(fetched_lv_process.pid)

socket
|> assign(:lv_process, AsyncResult.ok(fetched_lv_process))
Expand All @@ -138,7 +138,7 @@ defmodule LiveDebugger.LiveViews.ChannelDashboardLive do
end

@impl true
def handle_info({:DOWN, _, :process, _closed_pid, _}, socket) do
def handle_info({:process_status, :dead}, socket) do
socket
|> push_patch(to: URL.remove_query_param(socket.assigns.url, "node_id"))
|> start_async_assign_lv_process(%{"socket_id" => socket.assigns.socket_id})
Expand Down Expand Up @@ -214,4 +214,10 @@ defmodule LiveDebugger.LiveViews.ChannelDashboardLive do

push_patch(socket, to: url)
end

defp subscribe_process_state(pid) do
pid
|> PubSubUtils.process_status_topic()
|> PubSubUtils.subscribe!()
end
end
17 changes: 8 additions & 9 deletions lib/live_debugger/live_views/traces_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ defmodule LiveDebugger.LiveViews.TracesLive do
|> assign(node_id: node_id)
|> assign(id: session["id"])
|> assign(root_pid: session["root_pid"])
|> assign(ets_table_id: TraceService.ets_table_id(lv_process))
|> assign(lv_process: lv_process)
|> TracingHelper.init()
|> assign_async_existing_traces()
Expand Down Expand Up @@ -290,10 +289,10 @@ defmodule LiveDebugger.LiveViews.TracesLive do

@impl true
def handle_event("clear-traces", _, socket) do
ets_table_id = socket.assigns.ets_table_id
pid = socket.assigns.lv_process.pid
node_id = socket.assigns.node_id

TraceService.clear_traces(ets_table_id, node_id)
TraceService.clear_traces(pid, node_id)

socket
|> stream(:existing_traces, [], reset: true)
Comment thread
GuzekAlan marked this conversation as resolved.
Expand All @@ -305,7 +304,7 @@ defmodule LiveDebugger.LiveViews.TracesLive do
def handle_event("open-trace", %{"data" => string_id}, socket) do
trace_id = String.to_integer(string_id)

socket.assigns.ets_table_id
socket.assigns.lv_process.pid
|> TraceService.get(trace_id)
|> case do
nil ->
Expand All @@ -323,7 +322,7 @@ defmodule LiveDebugger.LiveViews.TracesLive do
def handle_event("toggle-collapsible", %{"trace-id" => string_trace_id}, socket) do
trace_id = String.to_integer(string_trace_id)

socket.assigns.ets_table_id
socket.assigns.lv_process.pid
|> TraceService.get(trace_id)
|> case do
nil ->
Expand All @@ -348,15 +347,15 @@ defmodule LiveDebugger.LiveViews.TracesLive do
end

defp assign_async_existing_traces(socket) do
ets_table_id = socket.assigns.ets_table_id
pid = socket.assigns.lv_process.pid
node_id = socket.assigns.node_id
active_functions = get_active_functions(socket)

socket
|> assign(:existing_traces_status, :loading)
|> stream(:existing_traces, [], reset: true)
|> start_async(:fetch_existing_traces, fn ->
TraceService.existing_traces(ets_table_id,
TraceService.existing_traces(pid,
node_id: node_id,
limit: @page_size,
functions: active_functions
Expand All @@ -365,15 +364,15 @@ defmodule LiveDebugger.LiveViews.TracesLive do
end

defp load_more_existing_traces(socket) do
ets_table_id = socket.assigns.ets_table_id
pid = socket.assigns.lv_process.pid
node_id = socket.assigns.node_id
cont = socket.assigns.traces_continuation
active_functions = get_active_functions(socket)

socket
|> assign(:traces_continuation, :loading)
|> start_async(:load_more_existing_traces, fn ->
TraceService.existing_traces(ets_table_id,
TraceService.existing_traces(pid,
node_id: node_id,
limit: @page_size,
cont: cont,
Expand Down
Loading