diff --git a/dev/live_views/side.ex b/dev/live_views/side.ex index 0a31bfb..3f98151 100644 --- a/dev/live_views/side.ex +++ b/dev/live_views/side.ex @@ -6,7 +6,7 @@ defmodule LiveDebuggerDev.LiveViews.Side do Task.start(fn -> for _ <- 1..100_000 do - Process.sleep(10) + Process.sleep(8) send(current_pid, :hello) end end) diff --git a/lib/live_debugger/live_components/traces_list.ex b/lib/live_debugger/live_components/traces_list.ex index d2dcb53..0f356fb 100644 --- a/lib/live_debugger/live_components/traces_list.ex +++ b/lib/live_debugger/live_components/traces_list.ex @@ -7,6 +7,7 @@ defmodule LiveDebugger.LiveComponents.TracesList do require Logger + alias LiveDebugger.LiveHelpers.TracingHelper alias LiveDebugger.Structs.Trace alias LiveDebugger.Components.ElixirDisplay alias LiveDebugger.Services.TraceService @@ -19,32 +20,29 @@ defmodule LiveDebugger.LiveComponents.TracesList do @impl true def mount(socket) do socket - |> assign(:tracing_started?, false) |> assign(:displayed_trace, nil) |> ok() end @impl true - def update( - %{new_trace: %{trace: trace, counter: counter}}, - %{assigns: %{tracing_started?: true}} = socket - ) do + def update(%{new_trace: trace}, socket) do socket - |> stream_insert(:existing_traces, TraceDisplay.form_live_trace(trace, counter), - at: 0, - limit: @stream_limit - ) - |> ok() - end + |> TracingHelper.check_fuse() + |> case do + {:ok, socket} -> + trace_display = TraceDisplay.from_live_trace(trace) + stream_insert(socket, :existing_traces, trace_display, at: 0, limit: @stream_limit) - @impl true - def update(%{new_trace: _trace}, %{assigns: %{tracing_started?: false}} = socket) do - {:ok, socket} + {_, socket} -> + # Add disappearing flash here in case of :stopped. (Issue 173) + socket + end + |> ok() end def update(assigns, socket) do socket - |> assign(:tracing_started?, false) + |> TracingHelper.init() |> assign(debugged_node_id: assigns.debugged_node_id) |> assign(id: assigns.id) |> assign(ets_table_id: TraceService.ets_table_id(assigns.socket_id)) @@ -63,8 +61,27 @@ defmodule LiveDebugger.LiveComponents.TracesList do <.collapsible_section title="Callback traces" id="traces" inner_class="p-4"> <:right_panel>
<%= @callback_name %>
- <.aggregate_count :if={@counter && @counter > 1} count={@counter} /> -<%= @callback_name %>
<.short_trace_content trace={@trace} /><%= Parsers.parse_timestamp(@trace.timestamp) %> @@ -261,14 +281,6 @@ defmodule LiveDebugger.LiveComponents.TracesList do """ end - defp aggregate_count(assigns) do - ~H""" - - +<%= assigns.count %> - - """ - end - defp short_trace_content(assigns) do assigns = assign(assigns, :content, Enum.map_join(assigns.trace.args, " ", &inspect/1)) diff --git a/lib/live_debugger/live_helpers/tracing_helper.ex b/lib/live_debugger/live_helpers/tracing_helper.ex new file mode 100644 index 0000000..1bbae65 --- /dev/null +++ b/lib/live_debugger/live_helpers/tracing_helper.ex @@ -0,0 +1,94 @@ +defmodule LiveDebugger.LiveHelpers.TracingHelper do + @moduledoc """ + This module provides a helper to manage tracing. + It is responsible for determining if the tracing should be stopped. + It introduces a fuse mechanism to prevent LiveView from being overloaded with traces. + """ + + import Phoenix.Component, only: [assign: 3] + + alias Phoenix.LiveView.Socket + + @assign_name :tracing_helper + @time_period 1_000_000 + @trace_limit_per_period 100 + + @spec init(Socket.t()) :: Socket.t() + def init(socket) do + clear_tracing(socket) + end + + @spec switch_tracing(Socket.t()) :: Socket.t() + def switch_tracing(socket) do + if socket.assigns[@assign_name].tracing_started? do + clear_tracing(socket) + else + start_tracing(socket) + end + end + + @doc """ + Checks if the fuse is blown and stops tracing if it is. + It uses the `#{@assign_name}` assign to store information. + When tracing is not started returns `{:noop, socket}`. + """ + @spec check_fuse(Socket.t()) :: {:ok | :stopped | :noop, Socket.t()} + def check_fuse(%{assigns: %{@assign_name => %{tracing_started?: false}}} = socket) do + {:noop, socket} + end + + def check_fuse(%{assigns: %{@assign_name => %{tracing_started?: true}}} = socket) do + fuse = socket.assigns[@assign_name].fuse + + cond do + period_exceeded?(fuse) -> {:ok, reset_fuse(socket)} + count_exceeded?(fuse) -> {:stopped, clear_tracing(socket)} + true -> {:ok, increment_fuse(socket)} + end + end + + defp period_exceeded?(fuse) do + now() - fuse.start_time >= @time_period + end + + defp count_exceeded?(fuse) do + fuse.count + 1 >= @trace_limit_per_period + end + + defp increment_fuse(socket) do + fuse = socket.assigns[@assign_name].fuse + + assigns = %{ + tracing_started?: true, + fuse: %{fuse | count: fuse.count + 1} + } + + assign(socket, @assign_name, assigns) + end + + defp reset_fuse(socket) do + start_tracing(socket) + end + + defp start_tracing(socket) do + assigns = %{ + tracing_started?: true, + fuse: %{count: 0, start_time: now()} + } + + assign(socket, @assign_name, assigns) + end + + def clear_tracing(socket) do + assigns = %{ + tracing_started?: false, + fuse: nil + } + + assign(socket, @assign_name, assigns) + end + + defp now() do + :os.system_time(:microsecond) + end +end diff --git a/lib/live_debugger/live_views/channel_dashboard.ex b/lib/live_debugger/live_views/channel_dashboard.ex index 73bff49..97363b6 100644 --- a/lib/live_debugger/live_views/channel_dashboard.ex +++ b/lib/live_debugger/live_views/channel_dashboard.ex @@ -18,7 +18,6 @@ defmodule LiveDebugger.LiveViews.ChannelDashboard do |> assign(:socket_id, socket_id) |> assign(:tracing_session, nil) |> assign(:debugged_module, nil) - |> assign_rate_limiter_pid() |> assign_async_debugged_lv_process() |> assign_base_url() |> ok() @@ -102,10 +101,7 @@ defmodule LiveDebugger.LiveViews.ChannelDashboard do Process.monitor(fetched_lv_process.pid) socket.assigns.socket_id - |> CallbackTracingService.start_tracing( - fetched_lv_process.pid, - socket.assigns.rate_limiter_pid - ) + |> CallbackTracingService.start_tracing(fetched_lv_process.pid, self()) |> case do {:ok, tracing_session} -> socket @@ -148,18 +144,14 @@ defmodule LiveDebugger.LiveViews.ChannelDashboard do end @impl true - def handle_info({:new_trace, %{trace: trace, counter: _} = wrapped_trace}, socket) do + def handle_info({:new_trace, trace}, socket) do debugged_node_id = socket.assigns.node_id || (socket.assigns.debugged_lv_process.result && socket.assigns.debugged_lv_process.result.pid) if Trace.node_id(trace) == debugged_node_id do - send_update(LiveDebugger.LiveComponents.TracesList, %{ - id: "trace-list", - new_trace: wrapped_trace - }) - + send_update(LiveDebugger.LiveComponents.TracesList, %{id: "trace-list", new_trace: trace}) send_update(LiveDebugger.LiveComponents.DetailView, %{id: "detail_view", new_trace: trace}) end @@ -212,15 +204,6 @@ defmodule LiveDebugger.LiveViews.ChannelDashboard do end) end - defp assign_rate_limiter_pid(socket) do - if connected?(socket) do - {:ok, pid} = LiveDebugger.Services.TraceRateLimiter.start_link() - assign(socket, :rate_limiter_pid, pid) - else - assign(socket, :rate_limiter_pid, nil) - end - end - defp fetch_lv_process_after(socket_id, milliseconds) do Process.sleep(milliseconds) LiveViewDiscoveryService.lv_process(socket_id) diff --git a/lib/live_debugger/services/trace_rate_limiter.ex b/lib/live_debugger/services/trace_rate_limiter.ex deleted file mode 100644 index c24ca78..0000000 --- a/lib/live_debugger/services/trace_rate_limiter.ex +++ /dev/null @@ -1,57 +0,0 @@ -defmodule LiveDebugger.Services.TraceRateLimiter do - @moduledoc """ - This module provides a rate limiter for traces. - It should be used as a proxy between the `:dbg` tracer and LiveDebugger dashboard. - It limits the number of traces that are sent to the dashboard. - You can configure the number of traces and the period in which they are counted via module attributes. - """ - use GenServer - - @traces_number 25 - - @period_ms 1000 - @interval_ms div(@period_ms, @traces_number) - - def start_link(target_pid \\ self()) do - GenServer.start_link(__MODULE__, %{target_pid: target_pid}) - end - - @impl true - def init(%{target_pid: target_pid}) do - schedule_processing() - {:ok, %{target_pid: target_pid, traces: []}} - end - - @impl true - def handle_info(:__do_process__, state) do - schedule_processing() - - state.traces - |> Enum.reverse() - |> Enum.each(fn {_key, %{last_trace: trace, counter: counter}} -> - send(state.target_pid, {:new_trace, %{trace: trace, counter: counter}}) - end) - - {:noreply, %{state | traces: []}} - end - - @impl true - def handle_info({:new_trace, %{function: key} = trace}, %{traces: traces} = state) do - updated_traces = - traces - |> Keyword.get(key) - |> case do - nil -> - Keyword.put(traces, key, %{last_trace: trace, counter: 1}) - - %{counter: counter} -> - Keyword.put(traces, key, %{last_trace: trace, counter: counter + 1}) - end - - {:noreply, %{state | traces: updated_traces}} - end - - defp schedule_processing() do - Process.send_after(self(), :__do_process__, @interval_ms) - end -end diff --git a/lib/live_debugger/structs/trace_display.ex b/lib/live_debugger/structs/trace_display.ex index a635fd8..53a5fcb 100644 --- a/lib/live_debugger/structs/trace_display.ex +++ b/lib/live_debugger/structs/trace_display.ex @@ -11,16 +11,15 @@ defmodule LiveDebugger.Structs.TraceDisplay do @type t() :: %__MODULE__{ id: integer(), trace: Trace.t(), - render_body?: boolean(), - counter: non_neg_integer() | nil + render_body?: boolean() } def from_historical_trace(%Trace{} = trace) do %__MODULE__{id: trace.id, trace: trace, render_body?: true} end - def form_live_trace(%Trace{} = trace, counter) do - %__MODULE__{id: trace.id, trace: trace, render_body?: false, counter: counter} + def from_live_trace(%Trace{} = trace) do + %__MODULE__{id: trace.id, trace: trace, render_body?: false} end def render_body(%__MODULE__{} = trace) do