-
Notifications
You must be signed in to change notification settings - Fork 18
Enhancement: Extend GarbageCollector
#766
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
base: main
Are you sure you want to change the base?
Changes from all commits
0660e78
8ad33d7
6f25388
94f0865
15d0d1a
165c0c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's made as a preparation for better debugging dead LiveViews |
||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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{}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we get rid of this event? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It wasn't used anywhere and so when I refactored this part of code I didn't try to keep it if it's not even needed. We can always add it when there will be such a need. |
||
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( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we exactly change it? Using event is more flexible because you have to monitor in a single place only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that this variant is used for LiveViews that dies immediately and we cannot fetch the state as usual via
LiveViewDebug.liveview_state/1
because it may be already dead, right? The problem with this approach is that you don't have a list of LiveComponents in the state storage which theoretically may affect LiveViews that do not die immediately.What will happen if someone triggers
handle_params/3
that does not change any assigns?handle_params/3
in this case will be the last callback triggered in given LiveView (render/1
will not be triggered because assigns did not change). In such case components inspection may breakThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow. If LiveView process dies so quickly that we can't fetch any information from it (e.g. LiveComponents) then we can't do much. But we can save socket from arguments of callbacks and show some information about this LiveView.
In what way?
If the LiveView does not crash to the first connected
render
then most likely we will save it's state (full information about LiveComponents). There is still some chance that it will crash right after thisrender
before processing our state fetch request but again we can't do much then.