Skip to content

Commit 0cb0f49

Browse files
authored
Enhancement: stabilize LiveDebugger behavior on recompilation (#873)
* Add filesystem dep * Removed trace after recompile option * Changed spec * Working, needs polish * Removed dbg * Extracted live_module? function to ModuleAPI * Changed name of all_paths function * Better error handling in all_callbacks function * Better specs * Cleanup in TracingManager * Split functions * Removed obsolete tests * Removed obsolete handler test * Cleanup in tracing_manager_test * Wrapped FileSystem library and added mock for it * Added tests for paths * Added tests for all_callbacks/1 * Add tests for live_module? function * Added tests for refresh_tracing/1 * Add test for monitor recompilation * Added case for file_event * Fix settings e2e test * Installation of inotify-tools * Updated version in README * Updated docs with new version * Revert "Updated docs with new version" This reverts commit 13a3dae. * Revert "Updated version in README" This reverts commit f1c0641.
1 parent 294834b commit 0cb0f49

File tree

21 files changed

+477
-210
lines changed

21 files changed

+477
-210
lines changed

.github/workflows/backward-compatibility-ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929
steps:
3030
- uses: actions/checkout@v4
3131

32+
- name: Install inotify-tools
33+
run: sudo apt-get update && sudo apt-get install -y inotify-tools
34+
3235
- name: Set up Elixir
3336
uses: erlef/[email protected]
3437
with:

.github/workflows/elixir-ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929
steps:
3030
- uses: actions/checkout@v4
3131

32+
- name: Install inotify-tools
33+
run: sudo apt-get update && sudo apt-get install -y inotify-tools
34+
3235
- name: Set up Elixir
3336
uses: erlef/[email protected]
3437
with:

lib/live_debugger/api/settings_storage.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
defmodule LiveDebugger.API.SettingsStorage do
22
@available_settings [
33
:dead_view_mode,
4-
:tracing_update_on_code_reload,
54
:garbage_collection,
65
:debug_button,
76
:tracing_enabled_on_start,
@@ -77,7 +76,6 @@ defmodule LiveDebugger.API.SettingsStorage do
7776

7877
@default_settings %{
7978
dead_view_mode: true,
80-
tracing_update_on_code_reload: false,
8179
garbage_collection: true,
8280
debug_button: true,
8381
tracing_enabled_on_start: true,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule LiveDebugger.API.System.FileSystem do
2+
@moduledoc """
3+
API for interacting with file system monitoring functionalities.
4+
5+
This module wraps the `FileSystem` library to allow for easier testing and mocking.
6+
"""
7+
8+
@callback start_link(opts :: keyword()) :: GenServer.on_start()
9+
@callback subscribe(name :: atom()) :: :ok
10+
11+
@doc """
12+
Starts a FileSystem monitor process with the given options.
13+
14+
Options:
15+
- `:dirs` - list of directories to monitor
16+
- `:name` - name to register the process under
17+
"""
18+
@spec start_link(keyword()) :: GenServer.on_start()
19+
def start_link(opts), do: impl().start_link(opts)
20+
21+
@doc """
22+
Subscribes the current process to file system events from the named monitor.
23+
24+
The subscribing process will receive messages in the format:
25+
`{:file_event, pid, {path, events}}`
26+
"""
27+
@spec subscribe(atom()) :: :ok
28+
def subscribe(name), do: impl().subscribe(name)
29+
30+
defp impl() do
31+
Application.get_env(
32+
:live_debugger,
33+
:api_file_system,
34+
__MODULE__.Impl
35+
)
36+
end
37+
38+
defmodule Impl do
39+
@moduledoc false
40+
@behaviour LiveDebugger.API.System.FileSystem
41+
42+
@impl true
43+
def start_link(opts) do
44+
FileSystem.start_link(opts)
45+
end
46+
47+
@impl true
48+
def subscribe(name) do
49+
FileSystem.subscribe(name)
50+
end
51+
end
52+
end

lib/live_debugger/api/system/module.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule LiveDebugger.API.System.Module do
66
@callback all() :: [{charlist(), charlist(), boolean()}]
77
@callback loaded?(module :: module()) :: boolean()
88
@callback behaviours(module :: module()) :: [module()]
9+
@callback live_module?(module :: module()) :: boolean()
910

1011
@doc """
1112
Wrapper for `:code.all_available/0`.
@@ -26,6 +27,12 @@ defmodule LiveDebugger.API.System.Module do
2627
@spec behaviours(module :: module()) :: [module()]
2728
def behaviours(module), do: impl().behaviours(module)
2829

30+
@doc """
31+
Returns true if the module implements Phoenix.LiveView or Phoenix.LiveComponent behaviour.
32+
"""
33+
@spec live_module?(module :: module()) :: boolean()
34+
def live_module?(module), do: impl().live_module?(module)
35+
2936
defp impl() do
3037
Application.get_env(
3138
:live_debugger,
@@ -58,5 +65,12 @@ defmodule LiveDebugger.API.System.Module do
5865
# It is safe to return empty list in this case
5966
UndefinedFunctionError -> []
6067
end
68+
69+
@impl true
70+
def live_module?(module) do
71+
module
72+
|> behaviours()
73+
|> Enum.any?(&(&1 == Phoenix.LiveView || &1 == Phoenix.LiveComponent))
74+
end
6175
end
6276
end

lib/live_debugger/app/events.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule LiveDebugger.App.Events do
88
alias LiveDebugger.Structs.LvProcess
99

1010
defevent(UserChangedSettings,
11-
key: :dead_view_mode | :tracing_update_on_code_reload | :garbage_collection,
11+
key: :dead_view_mode | :garbage_collection,
1212
value: term(),
1313
from: pid()
1414
)

lib/live_debugger/app/settings/web/settings_live.ex

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,6 @@ defmodule LiveDebugger.App.Settings.Web.SettingsLive do
6969
phx-value-setting="dead_view_mode"
7070
/>
7171
72-
<SettingsComponents.settings_switch
73-
id="tracing-update-on-reload-switch"
74-
label="Refresh tracing after recompilation"
75-
description="Tracing in LiveDebugger may be interrupted when modules are recompiled. With this option enabled, LiveDebugger will refresh tracing after project recompilation. It may have a negative impact on application performance."
76-
checked={@settings[:tracing_update_on_code_reload]}
77-
phx-click="update"
78-
phx-value-setting="tracing_update_on_code_reload"
79-
/>
8072
<SettingsComponents.settings_switch
8173
id="garbage-collection-switch"
8274
label="Garbage Collection"

lib/live_debugger/services/callback_tracer/actions/tracing.ex

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
22
@moduledoc """
33
This module provides actions for tracing.
44
"""
5+
require Logger
56

67
alias LiveDebugger.Services.CallbackTracer.Queries.Callbacks, as: CallbackQueries
78
alias LiveDebugger.Services.CallbackTracer.Process.Tracer
89
alias LiveDebugger.API.System.Dbg
9-
alias LiveDebugger.API.SettingsStorage
10+
alias LiveDebugger.API.System.FileSystem, as: FileSystemAPI
11+
alias LiveDebugger.API.System.Module, as: ModuleAPI
12+
alias LiveDebugger.Utils.Modules, as: UtilsModules
13+
alias LiveDebugger.Services.CallbackTracer.Queries.Paths, as: PathQueries
1014

1115
@spec setup_tracing!() :: :ok
1216
def setup_tracing!() do
@@ -21,33 +25,40 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
2125
Dbg.process([:c, :timestamp])
2226
apply_trace_patterns()
2327

24-
if SettingsStorage.get(:tracing_update_on_code_reload) do
25-
Dbg.trace_pattern(
26-
{Mix.Tasks.Compile.Elixir, :run, 1},
27-
Dbg.flag_to_match_spec(:return_trace)
28-
)
29-
end
30-
3128
:ok
3229
end
3330

34-
@spec start_tracing_recompile_pattern() :: :ok
35-
def start_tracing_recompile_pattern() do
36-
Dbg.trace_pattern({Mix.Tasks.Compile.Elixir, :run, 1}, Dbg.flag_to_match_spec(:return_trace))
31+
@spec refresh_tracing() :: :ok
32+
def refresh_tracing() do
33+
apply_trace_patterns()
3734

3835
:ok
3936
end
4037

41-
@spec stop_tracing_recompile_pattern() :: :ok
42-
def stop_tracing_recompile_pattern() do
43-
Dbg.clear_trace_pattern({Mix.Tasks.Compile.Elixir, :run, 1})
38+
@spec refresh_tracing(String.t()) :: :ok
39+
def refresh_tracing(path) do
40+
with true <- beam_file?(path),
41+
module <- path |> Path.basename(".beam") |> String.to_existing_atom(),
42+
true <- ModuleAPI.loaded?(module),
43+
false <- UtilsModules.debugger_module?(module),
44+
true <- ModuleAPI.live_module?(module) do
45+
refresh_tracing_for_module(module)
46+
end
4447

4548
:ok
4649
end
4750

48-
@spec refresh_tracing() :: :ok
49-
def refresh_tracing() do
50-
apply_trace_patterns()
51+
@doc """
52+
Starts FileSystem monitor and subscribes to it for compiled modules directories.
53+
When changes are detected in the monitored directories,
54+
process will receive `{:file_event, _pid, {path, events}}` message.
55+
"""
56+
57+
@spec monitor_recompilation() :: :ok
58+
def monitor_recompilation() do
59+
directories = PathQueries.compiled_modules_directories()
60+
FileSystemAPI.start_link(dirs: directories, name: :lvdbg_file_system_monitor)
61+
FileSystemAPI.subscribe(:lvdbg_file_system_monitor)
5162

5263
:ok
5364
end
@@ -59,6 +70,22 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
5970
:ok
6071
end
6172

73+
defp refresh_tracing_for_module(module) do
74+
module
75+
|> CallbackQueries.all_callbacks()
76+
|> case do
77+
{:error, error} ->
78+
Logger.error("Error refreshing tracing for module #{module}: #{error}")
79+
80+
callbacks ->
81+
callbacks
82+
|> Enum.each(fn mfa ->
83+
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:return_trace))
84+
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:exception_trace))
85+
end)
86+
end
87+
end
88+
6289
defp apply_trace_patterns() do
6390
# This is not a callback created by user
6491
# We trace it to refresh the components tree
@@ -70,4 +97,8 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
7097
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:exception_trace))
7198
end)
7299
end
100+
101+
defp beam_file?(path) do
102+
String.ends_with?(path, ".beam")
103+
end
73104
end

lib/live_debugger/services/callback_tracer/gen_servers/trace_handler.ex

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do
99

1010
alias LiveDebugger.Utils.Callbacks, as: CallbackUtils
1111
alias LiveDebugger.Services.CallbackTracer.Actions.FunctionTrace, as: TraceActions
12-
alias LiveDebugger.Services.CallbackTracer.Actions.Tracing, as: TracingActions
1312
alias LiveDebugger.Services.CallbackTracer.Actions.State, as: StateActions
1413
alias LiveDebugger.Services.CallbackTracer.Actions.DiffTrace, as: DiffActions
1514
alias LiveDebugger.Structs.Trace.FunctionTrace
@@ -53,39 +52,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do
5352
{:ok, %{}}
5453
end
5554

56-
#########################################################
57-
# Handling recompile events
58-
#
59-
# We catch this trace to know when modules were recompiled.
60-
# We do not display this trace to user, so we do not have to care about order
61-
# We need to catch that case because tracer disconnects from modules that were recompiled
62-
# and we need to reapply tracing patterns to them.
63-
# This will be replaced in the future with a more efficient way to handle this.
64-
# https://github.com/software-mansion/live-debugger/issues/592
65-
#
66-
#########################################################
67-
68-
@impl true
69-
def handle_cast(
70-
{:new_trace, {_, _, :return_from, {Mix.Tasks.Compile.Elixir, _, _}, {:ok, _}, _}, _n},
71-
state
72-
) do
73-
Task.start(fn ->
74-
Process.sleep(100)
75-
TracingActions.refresh_tracing()
76-
end)
77-
78-
{:noreply, state}
79-
end
80-
81-
def handle_cast({:new_trace, {_, _, _, {Mix.Tasks.Compile.Elixir, _, _}, _}, _}, state) do
82-
{:noreply, state}
83-
end
84-
85-
def handle_cast({:new_trace, {_, _, _, {Mix.Tasks.Compile.Elixir, _, _}, _, _}, _}, state) do
86-
{:noreply, state}
87-
end
88-
8955
#########################################################
9056
# Handling component deletion traces
9157
#
@@ -96,6 +62,7 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do
9662
#
9763
#########################################################
9864

65+
@impl true
9966
def handle_cast(
10067
{:new_trace,
10168
{_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}, ts}, n},

lib/live_debugger/services/callback_tracer/gen_servers/tracing_manager.ex

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TracingManager do
66
use GenServer
77

88
alias LiveDebugger.Bus
9-
alias LiveDebugger.App.Events.UserChangedSettings
109
alias LiveDebugger.App.Events.UserRefreshedTrace
1110
alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewBorn
1211

@@ -41,40 +40,36 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TracingManager do
4140
@impl true
4241
def handle_info(:setup_tracing, state) do
4342
TracingActions.setup_tracing!()
43+
TracingActions.monitor_recompilation()
4444

4545
{:noreply, state}
4646
end
4747

48-
@impl true
49-
def handle_info(%UserChangedSettings{key: :tracing_update_on_code_reload, value: true}, state) do
50-
TracingActions.start_tracing_recompile_pattern()
48+
def handle_info(%LiveViewBorn{pid: pid}, state) do
49+
TracingActions.start_outgoing_messages_tracing(pid)
5150

5251
{:noreply, state}
5352
end
5453

55-
@impl true
56-
def handle_info(%UserChangedSettings{key: :tracing_update_on_code_reload, value: false}, state) do
57-
TracingActions.stop_tracing_recompile_pattern()
54+
def handle_info(%UserRefreshedTrace{}, state) do
55+
TracingActions.refresh_tracing()
5856

5957
{:noreply, state}
6058
end
6159

62-
@impl true
63-
def handle_info(%LiveViewBorn{pid: pid}, state) do
64-
TracingActions.start_outgoing_messages_tracing(pid)
60+
def handle_info({:file_event, _pid, {path, events}}, state) do
61+
if correct_event?(events) do
62+
TracingActions.refresh_tracing(path)
63+
end
6564

6665
{:noreply, state}
6766
end
6867

69-
@impl true
70-
def handle_info(%UserRefreshedTrace{}, state) do
71-
TracingActions.refresh_tracing()
72-
68+
def handle_info(_, state) do
7369
{:noreply, state}
7470
end
7571

76-
@impl true
77-
def handle_info(_, state) do
78-
{:noreply, state}
72+
defp correct_event?(events) do
73+
Enum.any?(events, &(&1 == :modified || &1 == :created))
7974
end
8075
end

0 commit comments

Comments
 (0)