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/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 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..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,34 +15,51 @@ 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 + @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.reduce(false, fn {pid, table}, acc -> + |> 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) - true -> delete_traces_table!(table) + MapSet.member?(to_remove, pid) -> delete_traces_table!(table) + true -> :to_remove end - acc or result + {pid, result} end) + |> aggregate_results() end - @spec garbage_collect_states!(MapSet.t(pid()), MapSet.t(pid())) :: boolean() - def garbage_collect_states!(watched_pids, alive_pids) do + @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.reduce(false, fn {pid, _}, acc -> + |> Enum.map(fn {pid, _} -> 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) -> :keep + MapSet.member?(to_remove, pid) -> delete_state!(pid) + true -> :to_remove end - acc or result + {pid, result} end) + |> aggregate_results() + end + + defp watched_or_alive?(pid, watched_pids, alive_pids) do + MapSet.member?(watched_pids, pid) or MapSet.member?(alive_pids, pid) end defp maybe_trim_traces_table!(table, type) when type in [:watched, :non_watched] do @@ -51,22 +69,29 @@ 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 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 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 1fb6dcf4b..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 @@ -12,11 +12,15 @@ 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 + @type state :: %{ + garbage_collection_enabled?: boolean(), + to_remove: MapSet.t(pid()) + } + def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @@ -28,7 +32,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,39 +42,29 @@ 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 - @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/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..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 @@ -134,11 +128,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/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 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