Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions dev/live_components/crash.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule LiveDebuggerDev.LiveComponents.Crash do
use DevWeb, :live_component

def render(assigns) do
~H"""
<div>
<.box title="Crash [LiveComponent]" color="red">
<.button phx-click="crash" color="red" phx-target={@myself}>Crash</.button>
</.box>
</div>
"""
end
end
1 change: 1 addition & 0 deletions dev/live_views/main.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ defmodule LiveDebuggerDev.LiveViews.Main do

<.live_component id="many_assigns" module={LiveComponents.ManyAssigns} />
<.live_component id="name_outer" name={@name} module={LiveComponents.Name} />
<.live_component id="crash" module={LiveComponents.Crash} />
<.live_component id="send_outer" module={LiveComponents.Send}>
<.live_component id="name_inner" name={@name} module={LiveComponents.Name} />
<.live_component id="long_name" module={LiveComponents.LiveComponentWithVeryVeryLongName} />
Expand Down
1 change: 1 addition & 0 deletions lib/live_debugger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule LiveDebugger do
else
children ++
[
{LiveDebugger.GenServers.StateServer, []},
{LiveDebugger.GenServers.CallbackTracingServer, []},
{LiveDebugger.GenServers.EtsTableServer, []}
]
Expand Down
20 changes: 10 additions & 10 deletions lib/live_debugger/gen_servers/callback_tracing_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,23 +177,18 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do

@spec do_publish(Trace.t()) :: :ok
defp do_publish(%{module: Phoenix.LiveView.Diff} = trace) do
trace
|> PubSubUtils.component_deleted_topic()
|> PubSubUtils.broadcast({:new_trace, trace})
PubSubUtils.component_deleted_topic()
|> PubSubUtils.broadcast({:component_deleted, trace})
end

defp do_publish(trace) do
defp do_publish(%Trace{} = trace) do
socket_id = trace.socket_id
node_id = Trace.node_id(trace)
transport_pid = trace.transport_pid
fun = trace.function

socket_id
|> PubSubUtils.tsnf_topic(transport_pid, node_id, fun, :call)
|> PubSubUtils.broadcast({:new_trace, trace})

socket_id
|> PubSubUtils.ts_f_topic(transport_pid, fun)
|> PubSubUtils.trace_topic(transport_pid, node_id, fun, :call)
|> PubSubUtils.broadcast({:new_trace, trace})
end

Expand All @@ -204,8 +199,13 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
transport_pid = trace.transport_pid
fun = trace.function

if fun == :render do
Comment thread
kraleppa marked this conversation as resolved.
PubSubUtils.node_rendered_topic()
|> PubSubUtils.broadcast({:render_trace, trace})
end

socket_id
|> PubSubUtils.tsnf_topic(transport_pid, node_id, fun, :return)
|> PubSubUtils.trace_topic(transport_pid, node_id, fun, :return)
|> PubSubUtils.broadcast({:updated_trace, trace})
end
end
53 changes: 24 additions & 29 deletions lib/live_debugger/gen_servers/ets_table_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,13 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
It creates table if none is associated with given pid
"""
@spec table!(pid :: pid()) :: :ets.table()
def table!(pid) when is_pid(pid) do
impl().table!(pid)
end
def table!(pid) when is_pid(pid), do: impl().table!(pid)

@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
impl().delete_table!(pid)
end

def impl() do
Application.get_env(:live_debugger, :ets_table_server, __MODULE__.Impl)
end

defmodule Impl do
@moduledoc false
@behaviour LiveDebugger.GenServers.EtsTableServer
@server_module LiveDebugger.GenServers.EtsTableServer

@impl true
def table!(pid) do
GenServer.call(@server_module, {:get_or_create_table, pid}, 1000)
end

@impl true
def delete_table!(pid) do
GenServer.call(@server_module, {:delete_table, pid}, 1000)
end
end
def delete_table!(pid) when is_pid(pid), do: impl().delete_table!(pid)

## GenServer

Expand All @@ -69,9 +45,8 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
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})
PubSubUtils.process_status_topic()
|> PubSubUtils.broadcast({:process_status, {:dead, closed_pid}})
Comment thread
kraleppa marked this conversation as resolved.

{:noreply, table_refs}
end
Expand All @@ -95,6 +70,26 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
{:reply, :ok, table_refs}
end

defp impl() do
Application.get_env(:live_debugger, :ets_table_server, __MODULE__.Impl)
end

defmodule Impl do
@moduledoc false
@behaviour LiveDebugger.GenServers.EtsTableServer
@server_module LiveDebugger.GenServers.EtsTableServer

@impl true
def table!(pid) do
GenServer.call(@server_module, {:get_or_create_table, pid}, 1000)
end

@impl true
def delete_table!(pid) do
GenServer.call(@server_module, {:delete_table, pid}, 1000)
end
end

@spec create_ets_table() :: :ets.table()
defp create_ets_table() do
:ets.new(@ets_table_name, [:ordered_set, :public])
Expand Down
114 changes: 114 additions & 0 deletions lib/live_debugger/gen_servers/state_server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
defmodule LiveDebugger.GenServers.StateServer do
@moduledoc """
This gen_server is responsible for storing the state of the application.
It collects state when `render` or `delete_component` callbacks are traced.
It uses named ETS table to store the state of the LiveView channel process.
When process dies, it removes the state from the table.
"""

use GenServer

alias LiveDebugger.Services.System.ProcessService
alias LiveDebugger.Utils.PubSub, as: PubSubUtils
alias LiveDebugger.CommonTypes
alias LiveDebugger.Structs.Trace

@ets_table_name :lvdbg_states

@callback get(pid :: pid()) :: {:ok, CommonTypes.channel_state()} | {:error, term()}

@doc """
Returns previously stored state of the LiveView channel process identified by `pid`.
If the state is not found, it returns `{:error, :not_found}`.
"""
@spec get(pid :: pid()) :: {:ok, CommonTypes.channel_state()} | {:error, term()}
def get(pid) when is_pid(pid) do
impl().get(pid)
end

@doc false
@spec ets_table_name() :: atom()
def ets_table_name(), do: @ets_table_name

@doc false
def record_id(pid), do: "#{inspect(pid)}"

@doc false
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end

@impl true
def init(_args) do
:ets.new(@ets_table_name, [:named_table, :public, :ordered_set])

PubSubUtils.node_rendered_topic()
|> PubSubUtils.subscribe!()

PubSubUtils.component_deleted_topic()
|> PubSubUtils.subscribe!()

PubSubUtils.process_status_topic()
|> PubSubUtils.subscribe!()

{:ok, []}
end

@impl true
def handle_info({:component_deleted, trace}, state) do
save_state(trace)

{:noreply, state}
end

def handle_info({:render_trace, trace}, state) do
save_state(trace)

{:noreply, state}
end

def handle_info({:process_status, {:dead, pid}}, state) do
:ets.delete(@ets_table_name, record_id(pid))

{:noreply, state}
end

defp save_state(%Trace{pid: pid} = trace) do
with {:ok, channel_state} <- ProcessService.state(pid) do
record_id = record_id(pid)
:ets.insert(@ets_table_name, {record_id, channel_state})

publish_state_changed(trace, channel_state)
end
end

defp publish_state_changed(%Trace{} = trace, channel_state) do
socket_id = trace.socket_id
transport_pid = trace.transport_pid
node_id = trace.cid || trace.pid

PubSubUtils.state_changed_topic(socket_id, transport_pid, node_id)
Comment thread
GuzekAlan marked this conversation as resolved.
|> PubSubUtils.broadcast({:state_changed, channel_state, trace})

PubSubUtils.state_changed_topic(socket_id, transport_pid)
|> PubSubUtils.broadcast({:state_changed, channel_state, trace})
end

defp impl() do
Application.get_env(:live_debugger, :state_server, __MODULE__.Impl)
end

defmodule Impl do
@moduledoc false

@behaviour LiveDebugger.GenServers.StateServer
@server_module LiveDebugger.GenServers.StateServer

def get(pid) do
case :ets.lookup(@server_module.ets_table_name(), @server_module.record_id(pid)) do
[{_, channel_state}] -> {:ok, channel_state}
[] -> {:error, :not_found}
end
end
end
end
9 changes: 2 additions & 7 deletions lib/live_debugger/services/channel_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,15 @@ defmodule LiveDebugger.Services.ChannelService do
"""

alias LiveDebugger.Structs.TreeNode
alias LiveDebugger.Services.System.ProcessService
alias LiveDebugger.CommonTypes
alias LiveDebugger.GenServers.StateServer

@doc """
Retrieves the state of the LiveView channel process identified by `pid`.
"""
@spec state(pid :: pid()) :: {:ok, CommonTypes.channel_state()} | {:error, term()}
def state(pid) do
case ProcessService.state(pid) do
{:ok, %{socket: %Phoenix.LiveView.Socket{}, components: _} = state} -> {:ok, state}
{:ok, _} -> {:error, "PID: #{inspect(pid)} is not a LiveView process"}
{:error, :not_alive} -> {:error, :not_alive}
{:error, _} -> {:error, "Could not get state from pid: #{inspect(pid)}"}
end
StateServer.get(pid)
end

@doc """
Expand Down
12 changes: 3 additions & 9 deletions lib/live_debugger/services/system/dbg_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,22 @@ defmodule LiveDebugger.Services.System.DbgService do
@spec tracer(:process, handler_spec()) :: {:ok, pid()} | {:error, term()}
@spec tracer(:module, module_spec()) :: {:ok, pid()} | {:error, term()}
@spec tracer(:file, filename :: :file.name_all()) :: {:ok, pid()} | {:error, term()}
def tracer(type, handler_spec) do
impl().tracer(type, handler_spec)
end
def tracer(type, handler_spec), do: impl().tracer(type, handler_spec)

@doc """
Wrapper for `:dbg.p/2`.
Traces `Item` in accordance to the value specified by `Flags`.
`p` stands for **p**rocess.
"""
@spec p(item :: term(), flags :: term()) :: {:ok, match_desc()} | {:error, term()}
def p(item, flags) do
impl().p(item, flags)
end
def p(item, flags), do: impl().p(item, flags)

@doc """
Wrapper for `:dbg.tp/2` that sets up a trace pattern.
Enables call trace for one or more exported functions specified by `ModuleOrMFA`.
"""
@spec tp(module() | mfa(), match_spec :: term()) :: {:ok, match_desc()} | {:error, term()}
def tp(module, match_spec) do
impl().tp(module, match_spec)
end
def tp(module, match_spec), do: impl().tp(module, match_spec)

defp impl() do
Application.get_env(
Expand Down
6 changes: 3 additions & 3 deletions lib/live_debugger/structs/trace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ defmodule LiveDebugger.Structs.Trace do

@type t() :: %__MODULE__{
id: integer(),
module: atom(),
module: module(),
function: atom(),
arity: non_neg_integer(),
args: list(),
socket_id: String.t(),
transport_pid: pid() | nil,
pid: pid(),
cid: struct() | nil,
timestamp: timestamp(),
timestamp: integer(),
execution_time: non_neg_integer() | nil
}

@doc """
Creates a new trace struct.
"""
@spec new(integer(), atom(), atom(), list(), pid(), timestamp(), Keyword.t()) :: t()
@spec new(integer(), module(), atom(), list(), pid(), timestamp(), Keyword.t()) :: t()
def new(id, module, function, args, pid, timestamp, opts \\ []) do
socket_id = Keyword.get(opts, :socket_id, get_socket_id_from_args(args))
transport_pid = Keyword.get(opts, :transport_pid, get_transport_pid_from_args(args))
Expand Down
Loading