From 0660e7806f62ae73b2625560b7c71088dbd31c57 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Fri, 12 Sep 2025 13:51:59 +0200 Subject: [PATCH 1/7] Add to_remove flag for garbage collecting state --- .../actions/garbage_collecting.ex | 28 +++++++++++++++---- .../gen_servers/table_watcher.ex | 6 +--- lib/live_debugger/structs/lv_state.ex | 5 ++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex index 5c695a879..6ee131e74 100644 --- a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex +++ b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex @@ -3,6 +3,7 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do Actions for LiveDebugger.Services.GarbageCollector. """ + alias LiveDebugger.Structs.LvState alias LiveDebugger.API.StatesStorage alias LiveDebugger.API.TracesStorage @@ -18,11 +19,18 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do def garbage_collect_traces!(watched_pids, alive_pids) do TracesStorage.get_all_tables() |> Enum.reduce(false, fn {pid, table}, acc -> + to_remove = + case StatesStorage.get!(pid) do + nil -> true + %LvState{to_remove: to_remove} -> to_remove + end + result = cond do MapSet.member?(watched_pids, pid) -> maybe_trim_traces_table!(table, :watched) MapSet.member?(alive_pids, pid) -> maybe_trim_traces_table!(table, :non_watched) - true -> delete_traces_table!(table) + to_remove -> delete_traces_table!(table) + true -> false end acc or result @@ -32,18 +40,26 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do @spec garbage_collect_states!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() def garbage_collect_states!(watched_pids, alive_pids) do StatesStorage.get_all_states() - |> Enum.reduce(false, fn {pid, _}, acc -> + |> Enum.reduce(false, fn {pid, state}, acc -> result = - if MapSet.member?(watched_pids, pid) or MapSet.member?(alive_pids, pid) do - false - else - delete_state!(pid) + cond do + watched_or_alive?(pid, watched_pids, alive_pids) -> false + state.to_remove -> delete_state!(pid) + true -> mark_for_removal!(state) end acc or result end) end + defp watched_or_alive?(pid, watched_pids, alive_pids) do + MapSet.member?(watched_pids, pid) or MapSet.member?(alive_pids, pid) + end + + defp mark_for_removal!(state) do + StatesStorage.save!(%{state | to_remove: true}) + end + defp maybe_trim_traces_table!(table, type) when type in [:watched, :non_watched] do size = TracesStorage.table_size(table) max_size = max_table_size(type) diff --git a/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex b/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex index d95ea00ed..fea30ca19 100644 --- a/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex +++ b/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex @@ -134,11 +134,7 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcher do end defp add_watcher(state, pid, watcher) do - if Process.alive?(pid) do - Map.put(state, pid, %ProcessInfo{watchers: MapSet.new([watcher])}) - else - state - end + Map.put(state, pid, %ProcessInfo{alive?: Process.alive?(pid), watchers: MapSet.new([watcher])}) end @spec remove_watcher(state(), pid(), pid()) :: state() diff --git a/lib/live_debugger/structs/lv_state.ex b/lib/live_debugger/structs/lv_state.ex index 5b0a85a95..add7d32d1 100644 --- a/lib/live_debugger/structs/lv_state.ex +++ b/lib/live_debugger/structs/lv_state.ex @@ -3,7 +3,7 @@ defmodule LiveDebugger.Structs.LvState do This module provides a struct to represent a LiveView state. """ - defstruct [:pid, :socket, :components] + defstruct [:pid, :socket, :components, to_remove: false] @type component() :: %{ id: String.t(), @@ -16,6 +16,7 @@ defmodule LiveDebugger.Structs.LvState do @type t() :: %__MODULE__{ pid: pid(), socket: Phoenix.LiveView.Socket.t(), - components: [component()] + components: [component()], + to_remove: boolean() } end From 8ad33d7ab9dea634b29652efa4addd996f82d147 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Tue, 30 Sep 2025 12:49:59 +0200 Subject: [PATCH 2/7] Replace DebuggerTerminated event with Process.monitor --- .../app/debugger/web/debugger_live.ex | 8 -------- lib/live_debugger/app/events.ex | 1 - lib/live_debugger/app/web/components.ex | 2 +- .../garbage_collector/gen_servers/table_watcher.ex | 14 ++++---------- .../gen_servers/table_watcher_test.exs | 9 ++++----- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/lib/live_debugger/app/debugger/web/debugger_live.ex b/lib/live_debugger/app/debugger/web/debugger_live.ex index 495509119..e407d52f2 100644 --- a/lib/live_debugger/app/debugger/web/debugger_live.ex +++ b/lib/live_debugger/app/debugger/web/debugger_live.ex @@ -19,7 +19,6 @@ defmodule LiveDebugger.App.Debugger.Web.DebuggerLive do alias LiveDebugger.Bus alias LiveDebugger.App.Debugger.Events.NodeIdParamChanged - alias LiveDebugger.App.Events.DebuggerTerminated @impl true def mount(%{"pid" => string_pid}, _session, socket) do @@ -104,13 +103,6 @@ defmodule LiveDebugger.App.Debugger.Web.DebuggerLive do @impl true def handle_info(_, socket), do: {:noreply, socket} - @impl true - def terminate(_, _) do - Bus.broadcast_event!(%DebuggerTerminated{ - debugger_pid: self() - }) - end - defp init_debugger(socket, pid) when is_pid(pid) do socket |> Hooks.AsyncLvProcess.init(pid) diff --git a/lib/live_debugger/app/events.ex b/lib/live_debugger/app/events.ex index f5bfcdb13..ed4f8252b 100644 --- a/lib/live_debugger/app/events.ex +++ b/lib/live_debugger/app/events.ex @@ -16,7 +16,6 @@ defmodule LiveDebugger.App.Events do defevent(UserRefreshedTrace) defevent(DebuggerMounted, debugged_pid: pid(), debugger_pid: pid()) - defevent(DebuggerTerminated, debugger_pid: pid()) defevent(FindSuccessor, lv_process: LvProcess.t()) end diff --git a/lib/live_debugger/app/web/components.ex b/lib/live_debugger/app/web/components.ex index 01f3a3430..7303be889 100644 --- a/lib/live_debugger/app/web/components.ex +++ b/lib/live_debugger/app/web/components.ex @@ -83,7 +83,7 @@ defmodule LiveDebugger.App.Web.Components do attr(:variant, :string, default: "primary", values: ["primary", "secondary"]) attr(:size, :string, default: "md", values: ["md", "sm"]) attr(:class, :any, default: nil, doc: "Additional classes to add to the button.") - attr(:rest, :global) + attr(:rest, :global, include: ~w(disabled)) slot(:inner_block, required: true) diff --git a/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex b/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex index fea30ca19..000876e33 100644 --- a/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex +++ b/lib/live_debugger/services/garbage_collector/gen_servers/table_watcher.ex @@ -7,7 +7,6 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcher do alias LiveDebugger.Bus alias LiveDebugger.App.Events.DebuggerMounted - alias LiveDebugger.App.Events.DebuggerTerminated alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewBorn alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewDied @@ -60,7 +59,6 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcher do {:reply, pids, state} end - @impl true def handle_call(:watched_pids, _, state) do pids = state @@ -78,22 +76,21 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcher do |> noreply() end - @impl true def handle_info(%LiveViewDied{pid: pid}, state) when is_map_key(state, pid) do state |> update_live_view_died(pid) |> noreply() end - @impl true def handle_info(%DebuggerMounted{debugged_pid: debugged_pid, debugger_pid: debugger_pid}, state) do + Process.monitor(debugger_pid) + state |> add_watcher(debugged_pid, debugger_pid) |> noreply() end - @impl true - def handle_info(%DebuggerTerminated{debugger_pid: debugger_pid}, state) do + def handle_info({:DOWN, _ref, :process, debugger_pid, _reason}, state) do state |> Enum.find(fn {_, %ProcessInfo{watchers: watchers}} -> MapSet.member?(watchers, debugger_pid) @@ -109,10 +106,7 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcher do end end - @impl true - def handle_info(_, state) do - {:noreply, state} - end + def handle_info(_, state), do: {:noreply, state} @spec update_live_view_died(state(), pid()) :: state() defp update_live_view_died(state, pid) do diff --git a/test/services/garbage_collector/gen_servers/table_watcher_test.exs b/test/services/garbage_collector/gen_servers/table_watcher_test.exs index 757669d87..72b16de7c 100644 --- a/test/services/garbage_collector/gen_servers/table_watcher_test.exs +++ b/test/services/garbage_collector/gen_servers/table_watcher_test.exs @@ -3,7 +3,6 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcherTest do import Mox - alias LiveDebugger.App.Events.DebuggerTerminated alias LiveDebugger.App.Events.DebuggerMounted alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewDied alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewBorn @@ -123,18 +122,18 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcherTest do assert {:noreply, %{}} = TableWatcher.handle_info(event, state) end - test "for DebuggerTerminated event" do + test "for debugger :DOWN event" do debugged_pid = self() debugger_pid = :c.pid(0, 12, 0) state = %{debugged_pid => %ProcessInfo{alive?: true, watchers: MapSet.new([debugger_pid])}} - event = %DebuggerTerminated{debugger_pid: debugger_pid} + event = {:DOWN, 1, :process, debugger_pid, :normal} assert {:noreply, new_state} = TableWatcher.handle_info(event, state) assert new_state == %{debugged_pid => %ProcessInfo{alive?: true, watchers: MapSet.new()}} end - test "for DebuggerTerminated event when `debugger_pid` is not in the state" do + test "for debugger :DOWN event when `debugger_pid` is not in the state" do debugger_pid = :c.pid(0, 12, 0) other_debugger_pid = :c.pid(0, 13, 0) debugged_pid = :c.pid(0, 14, 0) @@ -143,7 +142,7 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.TableWatcherTest do debugged_pid => %ProcessInfo{alive?: true, watchers: MapSet.new([other_debugger_pid])} } - event = %DebuggerTerminated{debugger_pid: debugger_pid} + event = {:DOWN, 1, :process, debugger_pid, :normal} assert {:noreply, ^state} = TableWatcher.handle_info(event, state) end From 6f25388c83bbefd3c3983d8694a1df4b7fb764fd Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Tue, 30 Sep 2025 14:08:46 +0200 Subject: [PATCH 3/7] Refactor GC remove flag mechanism --- .../actions/garbage_collecting.ex | 61 ++++++++++--------- .../gen_servers/garbage_collector.ex | 13 ++-- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex index 6ee131e74..0b1b2a73b 100644 --- a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex +++ b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex @@ -15,40 +15,50 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do @watched_table_size 50 * @megabyte_unit @non_watched_table_size 5 * @megabyte_unit - @spec garbage_collect_traces!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() - def garbage_collect_traces!(watched_pids, alive_pids) do - TracesStorage.get_all_tables() - |> Enum.reduce(false, fn {pid, table}, acc -> - to_remove = - case StatesStorage.get!(pid) do - nil -> true - %LvState{to_remove: to_remove} -> to_remove - end + # @spec garbage_collect_traces!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() + def garbage_collect_traces!(state, watched_pids, alive_pids) do + %{to_remove: to_remove} = state + TracesStorage.get_all_tables() + |> Enum.map(fn {pid, table} -> result = cond do MapSet.member?(watched_pids, pid) -> maybe_trim_traces_table!(table, :watched) MapSet.member?(alive_pids, pid) -> maybe_trim_traces_table!(table, :non_watched) - to_remove -> delete_traces_table!(table) - true -> false + MapSet.member?(to_remove, pid) -> delete_traces_table!(table) + true -> :to_remove end - acc or result + {pid, result} + end) + |> Enum.reduce(MapSet.new(), fn {pid, result}, to_remove -> + case result do + :to_remove -> MapSet.put(to_remove, pid) + _ -> to_remove + end end) end - @spec garbage_collect_states!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() - def garbage_collect_states!(watched_pids, alive_pids) do + # @spec garbage_collect_states!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() + def garbage_collect_states!(state, watched_pids, alive_pids) do + %{to_remove: to_remove} = state + StatesStorage.get_all_states() - |> Enum.reduce(false, fn {pid, state}, acc -> + |> Enum.map(fn {pid, _} -> result = cond do - watched_or_alive?(pid, watched_pids, alive_pids) -> false - state.to_remove -> delete_state!(pid) - true -> mark_for_removal!(state) + watched_or_alive?(pid, watched_pids, alive_pids) -> :keep + MapSet.member?(to_remove, pid) -> delete_state!(pid) + true -> :to_remove end - acc or result + {pid, result} + end) + |> Enum.reduce(MapSet.new(), fn {pid, result}, to_remove -> + case result do + :to_remove -> MapSet.put(to_remove, pid) + _ -> to_remove + end end) end @@ -56,10 +66,6 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do MapSet.member?(watched_pids, pid) or MapSet.member?(alive_pids, pid) end - defp mark_for_removal!(state) do - StatesStorage.save!(%{state | to_remove: true}) - end - defp maybe_trim_traces_table!(table, type) when type in [:watched, :non_watched] do size = TracesStorage.table_size(table) max_size = max_table_size(type) @@ -67,22 +73,21 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do if size > max_size do TracesStorage.trim_table!(table, max_size) Bus.broadcast_event!(%TableTrimmed{}) - true - else - false end + + :keep end defp delete_traces_table!(table) do TracesStorage.delete_table!(table) Bus.broadcast_event!(%TableDeleted{}) - true + :removed end defp delete_state!(pid) do StatesStorage.delete!(pid) Bus.broadcast_event!(%TableTrimmed{}) - true + :removed end defp max_table_size(:watched), do: @watched_table_size diff --git a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex index 1fb6dcf4b..484fc3309 100644 --- a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex +++ b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex @@ -28,7 +28,8 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector do {:ok, %{ - garbage_collection_enabled?: SettingsStorage.get(:garbage_collection) + garbage_collection_enabled?: SettingsStorage.get(:garbage_collection), + to_remove: MapSet.new() }} end @@ -37,16 +38,12 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector do watched_pids = TableWatcher.watched_pids() alive_pids = TableWatcher.alive_pids() - traces_collected = GarbageCollectingActions.garbage_collect_traces!(watched_pids, alive_pids) - states_collected = GarbageCollectingActions.garbage_collect_states!(watched_pids, alive_pids) - - if traces_collected or states_collected do - Bus.broadcast_event!(%GarbageCollected{}) - end + to_remove1 = GarbageCollectingActions.garbage_collect_traces!(state, watched_pids, alive_pids) + to_remove2 = GarbageCollectingActions.garbage_collect_states!(state, watched_pids, alive_pids) loop_garbage_collection() - {:noreply, state} + {:noreply, %{state | to_remove: MapSet.union(to_remove1, to_remove2)}} end # Handle messages related to ETS table transfers from TracesStorage From 94f0865dbe53e5d590b8c99bade2daceb53e7cf8 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Tue, 30 Sep 2025 19:42:10 +0200 Subject: [PATCH 4/7] Update specs and refactor --- lib/live_debugger/app/web/components.ex | 2 +- .../actions/garbage_collecting.ex | 35 ++++++++----------- .../services/garbage_collector/events.ex | 1 - .../gen_servers/garbage_collector.ex | 1 - lib/live_debugger/structs/lv_state.ex | 5 ++- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/lib/live_debugger/app/web/components.ex b/lib/live_debugger/app/web/components.ex index 7303be889..01f3a3430 100644 --- a/lib/live_debugger/app/web/components.ex +++ b/lib/live_debugger/app/web/components.ex @@ -83,7 +83,7 @@ defmodule LiveDebugger.App.Web.Components do attr(:variant, :string, default: "primary", values: ["primary", "secondary"]) attr(:size, :string, default: "md", values: ["md", "sm"]) attr(:class, :any, default: nil, doc: "Additional classes to add to the button.") - attr(:rest, :global, include: ~w(disabled)) + attr(:rest, :global) slot(:inner_block, required: true) diff --git a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex index 0b1b2a73b..c83fe2682 100644 --- a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex +++ b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex @@ -3,7 +3,6 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do Actions for LiveDebugger.Services.GarbageCollector. """ - alias LiveDebugger.Structs.LvState alias LiveDebugger.API.StatesStorage alias LiveDebugger.API.TracesStorage @@ -15,10 +14,8 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do @watched_table_size 50 * @megabyte_unit @non_watched_table_size 5 * @megabyte_unit - # @spec garbage_collect_traces!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() - def garbage_collect_traces!(state, watched_pids, alive_pids) do - %{to_remove: to_remove} = state - + @spec garbage_collect_traces!(map(), MapSet.t(pid()), MapSet.t(pid())) :: MapSet.t(pid()) + def garbage_collect_traces!(%{to_remove: to_remove}, watched_pids, alive_pids) do TracesStorage.get_all_tables() |> Enum.map(fn {pid, table} -> result = @@ -31,18 +28,11 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do {pid, result} end) - |> Enum.reduce(MapSet.new(), fn {pid, result}, to_remove -> - case result do - :to_remove -> MapSet.put(to_remove, pid) - _ -> to_remove - end - end) + |> aggregate_results() end - # @spec garbage_collect_states!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() - def garbage_collect_states!(state, watched_pids, alive_pids) do - %{to_remove: to_remove} = state - + @spec garbage_collect_states!(map(), MapSet.t(pid()), MapSet.t(pid())) :: MapSet.t(pid()) + def garbage_collect_states!(%{to_remove: to_remove}, watched_pids, alive_pids) do StatesStorage.get_all_states() |> Enum.map(fn {pid, _} -> result = @@ -54,12 +44,7 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do {pid, result} end) - |> Enum.reduce(MapSet.new(), fn {pid, result}, to_remove -> - case result do - :to_remove -> MapSet.put(to_remove, pid) - _ -> to_remove - end - end) + |> aggregate_results() end defp watched_or_alive?(pid, watched_pids, alive_pids) do @@ -90,6 +75,14 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do :removed end + defp aggregate_results(gc_result) do + gc_result + |> Enum.reduce(MapSet.new(), fn + {pid, :to_remove}, acc -> MapSet.put(acc, pid) + _, acc -> acc + end) + end + defp max_table_size(:watched), do: @watched_table_size defp max_table_size(:non_watched), do: @non_watched_table_size end diff --git a/lib/live_debugger/services/garbage_collector/events.ex b/lib/live_debugger/services/garbage_collector/events.ex index e138bfc8c..003c4fd87 100644 --- a/lib/live_debugger/services/garbage_collector/events.ex +++ b/lib/live_debugger/services/garbage_collector/events.ex @@ -5,7 +5,6 @@ defmodule LiveDebugger.Services.GarbageCollector.Events do use LiveDebugger.Event - defevent(GarbageCollected) defevent(TableDeleted) defevent(TableTrimmed) end diff --git a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex index 484fc3309..c465e0fc9 100644 --- a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex +++ b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex @@ -12,7 +12,6 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector do as: GarbageCollectingActions alias LiveDebugger.Bus - alias LiveDebugger.Services.GarbageCollector.Events.GarbageCollected alias LiveDebugger.App.Events.UserChangedSettings @garbage_collect_interval 2000 diff --git a/lib/live_debugger/structs/lv_state.ex b/lib/live_debugger/structs/lv_state.ex index add7d32d1..5b0a85a95 100644 --- a/lib/live_debugger/structs/lv_state.ex +++ b/lib/live_debugger/structs/lv_state.ex @@ -3,7 +3,7 @@ defmodule LiveDebugger.Structs.LvState do This module provides a struct to represent a LiveView state. """ - defstruct [:pid, :socket, :components, to_remove: false] + defstruct [:pid, :socket, :components] @type component() :: %{ id: String.t(), @@ -16,7 +16,6 @@ defmodule LiveDebugger.Structs.LvState do @type t() :: %__MODULE__{ pid: pid(), socket: Phoenix.LiveView.Socket.t(), - components: [component()], - to_remove: boolean() + components: [component()] } end From 15d0d1ada30f84e958dbd8176a5dde0ce1b65bd9 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Tue, 30 Sep 2025 20:15:51 +0200 Subject: [PATCH 5/7] Add saving state mount and handle_params calls --- .../services/callback_tracer/actions/state.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/live_debugger/services/callback_tracer/actions/state.ex b/lib/live_debugger/services/callback_tracer/actions/state.ex index 932328756..4e9073ec8 100644 --- a/lib/live_debugger/services/callback_tracer/actions/state.ex +++ b/lib/live_debugger/services/callback_tracer/actions/state.ex @@ -6,6 +6,7 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.State do alias LiveDebugger.API.StatesStorage alias LiveDebugger.API.LiveViewDebug alias LiveDebugger.Structs.Trace + alias LiveDebugger.Structs.LvState alias LiveDebugger.Bus alias LiveDebugger.Services.CallbackTracer.Events.StateChanged @@ -18,6 +19,11 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.State do do_save_state!(pid) end + def maybe_save_state!(%Trace{pid: pid, function: function, args: [_, _, socket]}) + when function in [:mount, :handle_params] do + do_save_initial_state!(pid, socket) + end + def maybe_save_state!(%Trace{pid: pid, function: :delete_component, type: :call}) do do_save_state!(pid) end @@ -31,4 +37,10 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.State do :ok end end + + defp do_save_initial_state!(pid, socket) do + StatesStorage.save!(%LvState{pid: pid, socket: socket}) + Bus.broadcast_state!(%StateChanged{pid: pid}, pid) + :ok + end end From 165c0c1f6a35505213209967a464064246c64bc2 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Wed, 1 Oct 2025 10:55:31 +0200 Subject: [PATCH 6/7] Update tests --- .../actions/garbage_collecting.ex | 15 ++++- .../gen_servers/garbage_collector.ex | 13 ++--- .../actions/garbage_collecting_test.exs | 55 +++++++++++++++---- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex index c83fe2682..6a7a260d8 100644 --- a/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex +++ b/lib/live_debugger/services/garbage_collector/actions/garbage_collecting.ex @@ -5,6 +5,7 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do alias LiveDebugger.API.StatesStorage alias LiveDebugger.API.TracesStorage + alias LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector alias LiveDebugger.Bus alias LiveDebugger.Services.GarbageCollector.Events.TableTrimmed @@ -14,7 +15,12 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do @watched_table_size 50 * @megabyte_unit @non_watched_table_size 5 * @megabyte_unit - @spec garbage_collect_traces!(map(), MapSet.t(pid()), MapSet.t(pid())) :: MapSet.t(pid()) + @doc """ + Performs garbage collection on traces based on `to_remove`, `watched_pids`, and `alive_pids` sets. + Returns a set of PIDs marked for removal in next cycle. + """ + @spec garbage_collect_traces!(GarbageCollector.state(), MapSet.t(pid()), MapSet.t(pid())) :: + to_remove :: MapSet.t(pid()) def garbage_collect_traces!(%{to_remove: to_remove}, watched_pids, alive_pids) do TracesStorage.get_all_tables() |> Enum.map(fn {pid, table} -> @@ -31,7 +37,12 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollecting do |> aggregate_results() end - @spec garbage_collect_states!(map(), MapSet.t(pid()), MapSet.t(pid())) :: MapSet.t(pid()) + @doc """ + Performs garbage collection on states based on `to_remove`, `watched_pids`, and `alive_pids` sets. + Returns a set of PIDs marked for removal in next cycle. + """ + @spec garbage_collect_states!(GarbageCollector.state(), MapSet.t(pid()), MapSet.t(pid())) :: + to_remove :: MapSet.t(pid()) def garbage_collect_states!(%{to_remove: to_remove}, watched_pids, alive_pids) do StatesStorage.get_all_states() |> Enum.map(fn {pid, _} -> diff --git a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex index c465e0fc9..c2a28af59 100644 --- a/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex +++ b/lib/live_debugger/services/garbage_collector/gen_servers/garbage_collector.ex @@ -16,6 +16,11 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector do @garbage_collect_interval 2000 + @type state :: %{ + garbage_collection_enabled?: boolean(), + to_remove: MapSet.t(pid()) + } + def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @@ -46,26 +51,20 @@ defmodule LiveDebugger.Services.GarbageCollector.GenServers.GarbageCollector do end # Handle messages related to ETS table transfers from TracesStorage - @impl true def handle_info({:"ETS-TRANSFER", _ref, _from, _}, state) do {:noreply, state} end - @impl true def handle_info(%UserChangedSettings{key: :garbage_collection, value: true}, state) do resume_garbage_collection() {:noreply, Map.put(state, :garbage_collection_enabled?, true)} end - @impl true def handle_info(%UserChangedSettings{key: :garbage_collection, value: false}, state) do {:noreply, Map.put(state, :garbage_collection_enabled?, false)} end - @impl true - def handle_info(_, state) do - {:noreply, state} - end + def handle_info(_, state), do: {:noreply, state} defp loop_garbage_collection() do Process.send_after( diff --git a/test/services/garbage_collector/actions/garbage_collecting_test.exs b/test/services/garbage_collector/actions/garbage_collecting_test.exs index e047292b0..27beafe95 100644 --- a/test/services/garbage_collector/actions/garbage_collecting_test.exs +++ b/test/services/garbage_collector/actions/garbage_collecting_test.exs @@ -25,6 +25,7 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollectingTest d alive_pids = MapSet.new([pid2]) table1 = make_ref() table2 = make_ref() + state = %{to_remove: MapSet.new()} max_table_size_watched = 50 * @megabyte_unit max_table_size_non_watched = 5 * @megabyte_unit @@ -39,7 +40,8 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollectingTest d MockBus |> expect(:broadcast_event!, 2, fn %TableTrimmed{} -> :ok end) - assert true == GarbageCollectingActions.garbage_collect_traces!(watched_pids, alive_pids) + assert MapSet.new() == + GarbageCollectingActions.garbage_collect_traces!(state, watched_pids, alive_pids) end test "does not collect garbage if max size not exceeded" do @@ -49,6 +51,7 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollectingTest d alive_pids = MapSet.new([pid2]) table1 = make_ref() table2 = make_ref() + state = %{to_remove: MapSet.new()} MockAPITracesStorage |> expect(:get_all_tables, fn -> [{pid1, table1}, {pid2, table2}] end) @@ -59,44 +62,75 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollectingTest d MockBus |> deny(:broadcast_event!, 2) - assert false == GarbageCollectingActions.garbage_collect_traces!(watched_pids, alive_pids) + assert MapSet.new() == + GarbageCollectingActions.garbage_collect_traces!(state, watched_pids, alive_pids) end - test "deletes table if not watched and no alive" do + test "marks for removal if not watched and not alive" do watched_pids = MapSet.new() alive_pids = MapSet.new() pid1 = :c.pid(0, 11, 0) table1 = make_ref() + state = %{to_remove: MapSet.new([])} + + expect(MockAPITracesStorage, :get_all_tables, fn -> [{pid1, table1}] end) + + assert MapSet.new([pid1]) == + GarbageCollectingActions.garbage_collect_traces!(state, watched_pids, alive_pids) + end + + test "deletes table if not watched, not alive and marked to remove" do + watched_pids = MapSet.new() + alive_pids = MapSet.new() + pid1 = :c.pid(0, 11, 0) + table1 = make_ref() + state = %{to_remove: MapSet.new([pid1])} expect(MockAPITracesStorage, :get_all_tables, fn -> [{pid1, table1}] end) expect(MockAPITracesStorage, :delete_table!, fn ^table1 -> :ok end) expect(MockBus, :broadcast_event!, fn %TableDeleted{} -> :ok end) - assert true == GarbageCollectingActions.garbage_collect_traces!(watched_pids, alive_pids) + assert MapSet.new() == + GarbageCollectingActions.garbage_collect_traces!(state, watched_pids, alive_pids) end end describe "garbage_collect_states!/1" do - test "collects garbage for states if pids are not watched and not alive" do + test "marks for removal if not watched and not alive" do pid1 = :c.pid(0, 12, 0) watched_pids = MapSet.new([:c.pid(0, 11, 0)]) alive_pids = MapSet.new([:c.pid(0, 13, 0)]) + state = %{to_remove: MapSet.new()} + + MockAPIStatesStorage + |> expect(:get_all_states, fn -> [{pid1, :some_state}] end) + + assert MapSet.new([pid1]) == + GarbageCollectingActions.garbage_collect_states!(state, watched_pids, alive_pids) + end + + test "deletes states if not watched, not alive and marked for removal " do + pid1 = :c.pid(0, 12, 0) + watched_pids = MapSet.new([:c.pid(0, 11, 0)]) + alive_pids = MapSet.new([:c.pid(0, 13, 0)]) + state = %{to_remove: MapSet.new([pid1])} MockAPIStatesStorage |> expect(:get_all_states, fn -> [{pid1, :some_state}] end) |> expect(:delete!, fn ^pid1 -> :ok end) - MockBus - |> expect(:broadcast_event!, fn %TableTrimmed{} -> :ok end) + expect(MockBus, :broadcast_event!, fn %TableTrimmed{} -> :ok end) - assert true == GarbageCollectingActions.garbage_collect_states!(watched_pids, alive_pids) + assert MapSet.new() == + GarbageCollectingActions.garbage_collect_states!(state, watched_pids, alive_pids) end - test "does not collect garbage for states if pids are watched or alive" do + test "do nothing if pids are watched or alive" do pid1 = :c.pid(0, 12, 0) pid2 = :c.pid(0, 13, 0) watched_pids = MapSet.new([pid1]) alive_pids = MapSet.new([pid2]) + state = %{to_remove: MapSet.new()} MockAPIStatesStorage |> expect(:get_all_states, fn -> [{pid1, :some_state}, {pid2, :some_other_state}] end) @@ -105,7 +139,8 @@ defmodule LiveDebugger.Services.GarbageCollector.Actions.GarbageCollectingTest d MockBus |> deny(:broadcast_event!, 1) - assert false == GarbageCollectingActions.garbage_collect_states!(watched_pids, alive_pids) + assert MapSet.new() == + GarbageCollectingActions.garbage_collect_states!(state, watched_pids, alive_pids) end end end From 1de851e4fd91e1635bfdae60854946060bd2c7d6 Mon Sep 17 00:00:00 2001 From: Hubert Kasprzycki Date: Mon, 6 Oct 2025 13:54:25 +0200 Subject: [PATCH 7/7] Fix overwriting with incomplete state --- .../services/callback_tracer/actions/state.ex | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/live_debugger/services/callback_tracer/actions/state.ex b/lib/live_debugger/services/callback_tracer/actions/state.ex index 4e9073ec8..b5f4b57f4 100644 --- a/lib/live_debugger/services/callback_tracer/actions/state.ex +++ b/lib/live_debugger/services/callback_tracer/actions/state.ex @@ -19,8 +19,8 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.State do do_save_state!(pid) end - def maybe_save_state!(%Trace{pid: pid, function: function, args: [_, _, socket]}) - when function in [:mount, :handle_params] do + def maybe_save_state!(%Trace{pid: pid, function: function, type: type, args: [_, _, socket]}) + when function in [:mount, :handle_params] and type in [:return_from, :exception_from] do do_save_initial_state!(pid, socket) end @@ -39,8 +39,14 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.State do end defp do_save_initial_state!(pid, socket) do - StatesStorage.save!(%LvState{pid: pid, socket: socket}) - Bus.broadcast_state!(%StateChanged{pid: pid}, pid) - :ok + case StatesStorage.get!(pid) do + %LvState{} -> + :ok + + nil -> + StatesStorage.save!(%LvState{pid: pid, socket: socket}) + Bus.broadcast_state!(%StateChanged{pid: pid}, pid) + :ok + end end end