Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task: Adjusted rate limiting mechanism #172

Merged
merged 14 commits into from
Mar 7, 2025
2 changes: 1 addition & 1 deletion dev/live_views/side.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 43 additions & 31 deletions lib/live_debugger/live_components/traces_list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -63,8 +61,27 @@ defmodule LiveDebugger.LiveComponents.TracesList do
<.collapsible_section title="Callback traces" id="traces" inner_class="p-4">
<:right_panel>
<div class="flex gap-2 items-center">
<.toggle_tracing_button myself={@myself} tracing_started?={@tracing_started?} />
<.button variant="secondary" size="sm" phx-click="clear-traces" phx-target={@myself}>
<.toggle_tracing_button
myself={@myself}
tracing_started?={@tracing_helper.tracing_started?}
/>
<.button
:if={not @tracing_helper.tracing_started?}
phx-click="refresh-history"
phx-target={@myself}
class="flex gap-2"
variant="secondary"
size="sm"
>
Refresh
</.button>
<.button
:if={not @tracing_helper.tracing_started?}
variant="secondary"
size="sm"
phx-click="clear-traces"
phx-target={@myself}
>
Clear
</.button>
</div>
Expand Down Expand Up @@ -124,7 +141,7 @@ defmodule LiveDebugger.LiveComponents.TracesList do
@impl true
def handle_event("switch-tracing", _, socket) do
socket
|> assign(tracing_started?: not socket.assigns.tracing_started?)
|> TracingHelper.switch_tracing()
|> noreply()
end

Expand Down Expand Up @@ -180,6 +197,13 @@ defmodule LiveDebugger.LiveComponents.TracesList do
|> noreply()
end

@impl true
def handle_event("refresh-history", _, socket) do
socket
|> assign_async_existing_traces()
|> noreply()
end

attr(:tracing_started?, :boolean, required: true)
attr(:myself, :any, required: true)

Expand Down Expand Up @@ -209,7 +233,6 @@ defmodule LiveDebugger.LiveComponents.TracesList do
|> assign(:trace, assigns.wrapped_trace.trace)
|> assign(:render_body?, assigns.wrapped_trace.render_body?)
|> assign(:callback_name, Trace.callback_name(assigns.wrapped_trace.trace))
|> assign(:counter, assigns.wrapped_trace.counter)

~H"""
<.collapsible
Expand All @@ -228,10 +251,7 @@ defmodule LiveDebugger.LiveComponents.TracesList do
class="w-[90%] grow flex items-center ml-2 gap-1.5"
phx-update="ignore"
>
<div class="flex gap-1.5 items-center">
<p class="font-medium text-sm"><%= @callback_name %></p>
<.aggregate_count :if={@counter && @counter > 1} count={@counter} />
</div>
<p class="font-medium text-sm"><%= @callback_name %></p>
<.short_trace_content trace={@trace} />
<p class="w-max text-xs font-normal text-secondary-600 align-center">
<%= Parsers.parse_timestamp(@trace.timestamp) %>
Expand Down Expand Up @@ -261,14 +281,6 @@ defmodule LiveDebugger.LiveComponents.TracesList do
"""
end

defp aggregate_count(assigns) do
~H"""
<span class="rounded-full bg-white border border-secondary-200 text-2xs px-1.5">
+<%= assigns.count %>
</span>
"""
end

defp short_trace_content(assigns) do
assigns = assign(assigns, :content, Enum.map_join(assigns.trace.args, " ", &inspect/1))

Expand Down
94 changes: 94 additions & 0 deletions lib/live_debugger/live_helpers/tracing_helper.ex
Original file line number Diff line number Diff line change
@@ -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
23 changes: 3 additions & 20 deletions lib/live_debugger/live_views/channel_dashboard.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
57 changes: 0 additions & 57 deletions lib/live_debugger/services/trace_rate_limiter.ex

This file was deleted.

7 changes: 3 additions & 4 deletions lib/live_debugger/structs/trace_display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down