Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bcce056
Updated trace topics
kraleppa May 28, 2025
bfe7cdf
Added basic global traces view and basic routing
kraleppa May 28, 2025
f68b18d
Added navbar and nav menu to global callback traces
kraleppa May 28, 2025
49be828
Fixed navbar and nav menu
kraleppa May 28, 2025
31c9c67
Added selected states to navigation menu
kraleppa May 29, 2025
6814415
Updated tooltips
kraleppa May 29, 2025
cdb8fc8
Removed dot
kraleppa May 29, 2025
a866c72
Added tooltips for nav menu
kraleppa May 29, 2025
1de4a91
Extracted `assign_async_existing_traces`
kraleppa May 29, 2025
555e97f
Extracted `load_more_existing_traces`
kraleppa May 29, 2025
5cc1f82
Changed `page_size` to private
kraleppa May 29, 2025
1ff930a
Basic extraction of incoming traces and checks on required assigns
kraleppa May 29, 2025
39a20f5
Added checks
kraleppa May 29, 2025
8413c7a
Moved fuse check to tracing helper
kraleppa May 29, 2025
678a0ba
Fixed module doc
kraleppa May 29, 2025
3d876ee
Better organization
kraleppa May 29, 2025
9f3a254
Added helpers
kraleppa May 29, 2025
c3b130e
Cleanup
kraleppa May 29, 2025
c685f88
Updated descriptions
kraleppa May 29, 2025
ee5f8bf
Refactored helpers
kraleppa May 29, 2025
d2ac78a
Changed function to private
kraleppa May 30, 2025
e2fb11f
Moved load more button to a new component
kraleppa May 30, 2025
82d4e6a
Moved hooks
kraleppa May 30, 2025
da2a893
Moved helpers
kraleppa May 30, 2025
8a1c220
Renamed helpers and hooks
kraleppa May 30, 2025
707e783
Added error handling for pagination
kraleppa May 30, 2025
bd05de7
Added comment to traces_live_helper
kraleppa May 30, 2025
5b4f580
Extracted duplicated functions
kraleppa May 30, 2025
bccdc5a
Merge branch 'main' into 366-create-a-view-with-a-list-of-global-call…
kraleppa May 30, 2025
c97668e
Deleted obsolete tooltip classes
kraleppa May 30, 2025
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
45 changes: 45 additions & 0 deletions lib/live_debugger_web/components/traces.ex
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,49 @@ defmodule LiveDebuggerWeb.Components.Traces do
true -> ""
end
end

attr(:traces_continuation, :any, required: true)
attr(:tracing_helper, :any, required: true)

def load_more_button(assigns) do
~H"""
<div class="flex items-center justify-center mt-4">
<.load_more_button_content
traces_continuation={@traces_continuation}
tracing_helper={@tracing_helper}
/>
</div>
"""
end

defp load_more_button_content(%{traces_continuation: nil} = assigns), do: ~H""
defp load_more_button_content(%{traces_continuation: :end_of_table} = assigns), do: ~H""
defp load_more_button_content(%{tracing_helper: %{tracing_started?: true}} = assigns), do: ~H""

defp load_more_button_content(%{traces_continuation: :loading} = assigns) do
~H"""
<.spinner size="sm" class="mb-4" />
"""
end

defp load_more_button_content(%{traces_continuation: :error} = assigns) do
~H"""
<.alert
variant="danger"
with_icon={true}
heading="Error while loading more traces"
class="w-full mb-4"
>
Check logs for more details.
</.alert>
"""
end

defp load_more_button_content(%{traces_continuation: cont} = assigns) when is_tuple(cont) do
~H"""
<.button phx-click="load-more" class="w-4 mb-4" variant="secondary">
Load more
</.button>
"""
end
end
76 changes: 76 additions & 0 deletions lib/live_debugger_web/helpers/traces_live_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule LiveDebuggerWeb.Helpers.TracesLiveHelper do
@moduledoc """
This module provides helpers for the TracesLive and especially its hooks.
Since these hooks get more complex and they touch different assigns and streams,
this module was created to check if the assigns and streams are present in the socket.

This way we can catch errors early and not have to debug them in the hooks.
"""

alias LiveDebugger.Utils.Callbacks, as: UtilsCallbacks
alias LiveDebugger.Structs.TreeNode

@doc """
Checks if the assign is present in the socket.
If not, it raises an error.
"""
def check_assign!(socket, assign_name) do
if Map.has_key?(socket.assigns, assign_name) do
socket
else
raise "Assign #{assign_name} is required."
end
end

@doc """
Checks if the stream is present in the socket.
If not, it raises an error.
"""
def check_stream!(socket, stream_name) do
if Map.has_key?(socket.assigns.streams, stream_name) do
socket
else
raise "Stream #{stream_name} is required."
end
end

@doc """
Returns the default filters for the traces.
"""
def default_filters(node_id) do
functions =
node_id
|> TreeNode.type()
|> case do
:live_view -> UtilsCallbacks.live_view_callbacks()
:live_component -> UtilsCallbacks.live_component_callbacks()
end
|> Enum.map(fn {function, _} -> {function, true} end)

%{
functions: functions,
execution_time: [
{:exec_time_max, ""},
{:exec_time_min, ""}
]
}
end

@doc """
Returns the execution times for the traces.
"""
def get_execution_times(socket) do
socket.assigns.current_filters.execution_time
|> Enum.filter(fn {_, value} -> value != "" end)
|> Enum.map(fn {filter, value} -> {filter, String.to_integer(value)} end)
end

@doc """
Returns the active functions for the traces.
"""
def get_active_functions(socket) do
socket.assigns.current_filters.functions
|> Enum.filter(fn {_, active?} -> active? end)
|> Enum.map(fn {function, _} -> function end)
end
end
151 changes: 151 additions & 0 deletions lib/live_debugger_web/hooks/traces_live/existing_traces.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
defmodule LiveDebuggerWeb.Hooks.TracesLive.ExistingTraces do
@moduledoc """
This hook is responsible for fetching the existing traces and displaying them in the LiveView.
It encapsulates logic for async fetching of traces.
It attaches a hook to the `:existing_traces` stream to handle the async fetching.

Required assigns (that are used somehow in the hook):
- `:lv_process` - the LiveView process
- `:current_filters` - the current filters
- `:node_id` - the node ID
- `:traces_empty?` - whether the existing traces are empty, possible values: `true`, `false`

Required stream:
- `:existing_traces` - the stream of existing traces.

Assigns introduced by this hook (they can be used outside of the hook):
- `:traces_continuation` - the continuation token for the existing traces, possible values: `nil`, `:end_of_table`, `ets_continuation()`
- `:existing_traces_status` - the status of the existing traces, possible values: `:loading`, `:ok`, `:error`
"""

require Logger

import Phoenix.LiveView
import Phoenix.Component
import LiveDebuggerWeb.Helpers
import LiveDebuggerWeb.Helpers.TracesLiveHelper

alias LiveDebugger.Services.TraceService
alias LiveDebugger.Structs.TraceDisplay

def init_hook(socket, page_size) do
socket
|> check_assign!(:lv_process)
|> check_assign!(:node_id)
|> check_assign!(:current_filters)
|> check_assign!(:traces_empty?)
|> check_stream!(:existing_traces)
|> assign(:traces_continuation, nil)
|> put_private(:page_size, page_size)
|> attach_hook(:existing_traces, :handle_async, &handle_async/3)
end

@doc """
It loads asynchronously the existing traces and assigns them to the `:existing_traces` stream.
"""
@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
node_id = socket.assigns.node_id
page_size = socket.private.page_size
active_functions = get_active_functions(socket)
execution_times = get_execution_times(socket)

socket
|> assign(:existing_traces_status, :loading)
|> stream(:existing_traces, [], reset: true)
|> start_async(:fetch_existing_traces, fn ->
TraceService.existing_traces(pid,
node_id: node_id,
limit: page_size,
functions: active_functions,
execution_times: execution_times
)
end)
end

@doc """
It loads asynchronously more existing traces and assigns them to the `:existing_traces` stream.
"""
@spec assign_async_more_existing_traces(Phoenix.LiveView.Socket.t()) ::
Phoenix.LiveView.Socket.t()
def assign_async_more_existing_traces(socket) do
pid = socket.assigns.lv_process.pid
node_id = socket.assigns.node_id
cont = socket.assigns.traces_continuation
page_size = socket.private.page_size
active_functions = get_active_functions(socket)
execution_times = get_execution_times(socket)

socket
|> assign(:traces_continuation, :loading)
|> start_async(:load_more_existing_traces, fn ->
TraceService.existing_traces(pid,
node_id: node_id,
limit: page_size,
cont: cont,
functions: active_functions,
execution_times: execution_times
)
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)
|> assign(:traces_empty?, false)
|> assign(:traces_continuation, cont)
|> stream(:existing_traces, trace_list)
|> halt()
end

defp handle_async(:fetch_existing_traces, {:ok, :end_of_table}, socket) do
socket
|> assign(:existing_traces_status, :ok)
|> assign(:traces_continuation, :end_of_table)
|> halt()
end

defp handle_async(:fetch_existing_traces, {:exit, reason}, socket) do
log_async_error("fetching existing traces", reason)

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

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

socket
|> assign(:traces_continuation, cont)
|> stream(:existing_traces, trace_list)
|> halt()
end

defp handle_async(:load_more_existing_traces, {:ok, :end_of_table}, socket) do
socket
|> assign(:traces_continuation, :end_of_table)
|> halt()
end

defp handle_async(:load_more_existing_traces, {:exit, reason}, socket) do
log_async_error("loading more existing traces", reason)

socket
|> assign(:traces_continuation, :error)
|> halt()
end

defp handle_async(_, _, socket) do
{:cont, socket}
end

defp log_async_error(operation, reason) do
Logger.error(
"LiveDebugger encountered unexpected error while #{operation}: #{inspect(reason)}"
)
end
end
73 changes: 73 additions & 0 deletions lib/live_debugger_web/hooks/traces_live/incoming_traces.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule LiveDebuggerWeb.Hooks.TracesLive.IncomingTraces do
@moduledoc """
This hook is responsible for handling incoming traces
It is responsible for inserting new traces into the `:existing_traces` stream.
It also handles the case when the trace callback is running

This hook has to be added after TracingFuse hook - they're both handling `:new_trace` and `:updated_trace` messages.
TracingFuse has to be added first because it's responsible for stopping the trace callback.

Required assigns (that are used somehow in the hook):
- `:current_filters` - the current filters
- `:traces_empty?` - whether the existing traces are empty, possible values: `true`, `false`
- `:trace_callback_running?` - whether the trace callback is running

Required stream:
- `:existing_traces` - the stream of existing traces.
"""

import Phoenix.LiveView
import Phoenix.Component
import LiveDebuggerWeb.Helpers
import LiveDebuggerWeb.Helpers.TracesLiveHelper

alias LiveDebugger.Structs.TraceDisplay

@live_stream_limit 128

def init_hook(socket) do
socket
|> check_assign!(:current_filters)
|> check_assign!(:traces_empty?)
|> check_stream!(:existing_traces)
|> check_assign!(:trace_callback_running?)
|> attach_hook(:incoming_traces, :handle_info, &handle_info/2)
end

defp handle_info({:new_trace, trace}, socket) do
trace_display = TraceDisplay.from_trace(trace, true)

socket
|> stream_insert(:existing_traces, trace_display, at: 0, limit: @live_stream_limit)
|> assign(traces_empty?: false)
|> assign(trace_callback_running?: true)
|> halt()
end

defp handle_info({:updated_trace, trace}, socket) when socket.assigns.trace_callback_running? do
trace_display = TraceDisplay.from_trace(trace, true)

execution_time = get_execution_times(socket)
min_time = Keyword.get(execution_time, :exec_time_min, 0)
max_time = Keyword.get(execution_time, :exec_time_max, :infinity)

if trace.execution_time >= min_time and trace.execution_time <= max_time do
socket
|> stream_insert(:existing_traces, trace_display, at: 0, limit: @live_stream_limit)
else
socket
|> stream_delete(:existing_traces, trace_display)
end
|> assign(trace_callback_running?: false)
|> push_event("stop-timer", %{})
|> halt()
end

defp handle_info({:updated_trace, _trace}, socket) do
{:halt, socket}
end

defp handle_info(_, socket) do
{:cont, socket}
end
end
Loading
Loading