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
2 changes: 2 additions & 0 deletions lib/live_debugger_refactor/api/traces_storage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ defmodule LiveDebuggerRefactor.API.TracesStorage do
* `:limit` - Maximum number of traces to return (default: 100)
* `:cont` - Used to get next page of items in the following queries
* `:functions` - List of function names to filter traces by e.g ["handle_info/2", "render/1"]
* `:execution_times` - Map specifying minimum and maximum execution time of a callback in microseconds
e.g. %{"exec_time_min" => 0, "exec_time_max" => 100}
* `:search_query` - String to filter traces by, performs a case-sensitive substring search on the entire Trace struct
"""
@spec get!(table_identifier(), opts :: keyword()) ::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ defmodule LiveDebuggerRefactor.App.Debugger.CallbackTracing.Structs.TraceDisplay

alias LiveDebuggerRefactor.Structs.Trace

defstruct [:trace, :from_event?, :counter, render_body?: false]
defstruct [:id, :trace, :from_event?, :counter, render_body?: false]

@type t() :: %__MODULE__{
id: Trace.id(),
trace: Trace.t(),
from_event?: boolean(),
render_body?: boolean()
}

@spec from_trace(Trace.t(), from_event? :: boolean()) :: t()
def from_trace(%Trace{} = trace, from_event? \\ false) do
%__MODULE__{trace: trace, from_event?: from_event?}
%__MODULE__{id: trace.id, trace: trace, from_event?: from_event?}
end

@spec render_body(t()) :: t()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,43 @@ defmodule LiveDebuggerRefactor.App.Debugger.CallbackTracing.Web.Helpers.Filters
end)
end

@doc """
Returns the active functions from the current filters.
It uses the `current_filters` assigns to determine the active functions.
"""
@spec get_active_functions(current_filters :: %{functions: map()}) :: [String.t()]
def get_active_functions(current_filters) do
current_filters.functions
|> Enum.filter(fn {_, active?} -> active? end)
|> Enum.map(fn {function, _} -> function end)
end

@doc """
Returns the execution times from the current filters.
It uses the `current_filters` assigns to determine the execution times.
"""
@spec get_execution_times(current_filters :: %{execution_time: map()}) ::
%{String.t() => non_neg_integer()}
def get_execution_times(%{
execution_time: %{
"exec_time_min" => min_time,
"exec_time_max" => max_time,
"min_unit" => min_time_unit,
"max_unit" => max_time_unit
}
}) do
%{}
|> maybe_put_exec_time("exec_time_min", min_time, min_time_unit)
|> maybe_put_exec_time("exec_time_max", max_time, max_time_unit)
end

defp maybe_put_exec_time(execution_times, key, value, unit) do
case value do
"" -> execution_times
value -> Map.put(execution_times, key, apply_unit_factor(value, unit))
end
end

defp node_callbacks(node_id) when is_nil(node_id) or is_node_id(node_id) do
type = if node_id, do: TreeNode.type(node_id), else: :global

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
defmodule LiveDebuggerRefactor.App.Debugger.CallbackTracing.Web.Hooks.ExistingTraces do
@moduledoc """
This hook is responsible for fetching the existing traces.
"""

use LiveDebuggerRefactor.App.Web, :hook

require Logger

alias LiveDebuggerRefactor.API.TracesStorage
alias LiveDebuggerRefactor.App.Debugger.CallbackTracing.Structs.TraceDisplay
alias LiveDebuggerRefactor.App.Debugger.CallbackTracing.Web.Helpers.Filters, as: FiltersHelpers

@required_assigns [
:lv_process,
:current_filters,
:traces_empty?,
:traces_continuation,
:existing_traces_status
]

@doc """
Initializes the hook by attaching the hook to the socket and checking the required assigns.
"""
@spec init(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
def init(socket) do
socket
|> check_assigns!(@required_assigns)
|> check_stream!(:existing_traces)
|> check_private!(:page_size)
|> attach_hook(:existing_traces, :handle_async, &handle_async/3)
|> register_hook(:existing_traces)
|> assign_async_existing_traces()
end

@doc """
Loads the existing traces asynchronously and assigns them to the socket.
"""
@spec assign_async_existing_traces(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
def assign_async_existing_traces(socket) do
pid = socket.assigns.lv_process.pid

opts =
[
limit: socket.private.page_size,
functions: FiltersHelpers.get_active_functions(socket.assigns.current_filters),
execution_times: FiltersHelpers.get_execution_times(socket.assigns.current_filters),
node_id: Map.get(socket.assigns, :node_id),
search_query: Map.get(socket.assigns, :trace_search_query, "")
]

start_async(socket, :fetch_existing_traces, fn -> TracesStorage.get!(pid, opts) end)
end

defp handle_async(:fetch_existing_traces, {:ok, {trace_list, cont}}, socket) do
trace_list = Enum.map(trace_list, &TraceDisplay.from_trace/1)

socket
|> assign(
existing_traces_status: :ok,
traces_empty?: false,
traces_continuation: cont
)
|> stream(:existing_traces, trace_list, reset: true)
|> halt()
end

defp handle_async(:fetch_existing_traces, {:ok, :end_of_table}, socket) do
socket
|> assign(
existing_traces_status: :ok,
traces_continuation: :end_of_table
)
|> stream(:existing_traces, [], reset: true)
|> halt()
end

defp handle_async(:fetch_existing_traces, {:exit, reason}, socket) do
Logger.error(
"LiveDebugger encountered unexpected error while fetching existing traces: #{inspect(reason)}"
)

socket
|> assign(existing_traces_status: :error)
|> halt()
end

defp handle_async(_, _, socket), do: {:cont, socket}
end
13 changes: 13 additions & 0 deletions lib/live_debugger_refactor/app/web/helpers/hooks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ defmodule LiveDebuggerRefactor.App.Web.Helpers.Hooks do
end
end

@doc """
Checks if the given key is present in the private of the socket.
Raises an error if the key is not found.
"""
@spec check_private!(LiveViewSocket.t(), atom()) :: LiveViewSocket.t()
def check_private!(%LiveViewSocket{private: private} = socket, key) do
if Map.has_key?(private, key) do
socket
else
raise "Private key #{key} not found in socket.private"
end
end

@doc """
Add a hook to the socket via `socket.private.hooks`.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,63 @@ defmodule LiveDebuggerRefactor.App.Debugger.CallbackTracing.Web.Helpers.FiltersT

assert FiltersHelpers.count_selected_filters(default_filters, current_filters) == 2
end

test "get_active_functions/1 returns currently active functions from filters" do
filters = %{
functions: %{
"mount/3" => false,
"render/1" => true,
"handle_info/2" => true
},
execution_time: %{
"exec_time_min" => "10",
"exec_time_max" => "",
"min_unit" => "ms",
"max_unit" => "ms"
}
}

assert functions = FiltersHelpers.get_active_functions(filters)
assert 2 == length(functions)
assert "render/1" in functions
assert "handle_info/2" in functions
end

describe "get_execution_times/1" do
test "returns currently active execution time limits from filters" do
filters = %{
functions: %{
"mount/3" => false
},
execution_time: %{
"exec_time_min" => "14",
"exec_time_max" => "2",
"min_unit" => "ms",
"max_unit" => "s"
}
}

assert %{
"exec_time_min" => 14_000,
"exec_time_max" => 2_000_000
} =
FiltersHelpers.get_execution_times(filters)
end

test "returns only non empty execution time limits from filters" do
filters = %{
functions: %{
"mount/3" => false
},
execution_time: %{
"exec_time_min" => "140",
"exec_time_max" => "",
"min_unit" => "µs",
"max_unit" => "s"
}
}

assert %{"exec_time_min" => 140} = FiltersHelpers.get_execution_times(filters)
end
end
end