Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5755e53
Add filesystem dep
kraleppa Dec 4, 2025
3058978
Removed trace after recompile option
kraleppa Dec 4, 2025
b2efde0
Changed spec
kraleppa Dec 5, 2025
eddc8ad
Working, needs polish
kraleppa Dec 5, 2025
a00b64e
Removed dbg
kraleppa Dec 5, 2025
5f92e23
Extracted live_module? function to ModuleAPI
kraleppa Dec 5, 2025
4ac0c25
Changed name of all_paths function
kraleppa Dec 5, 2025
9b0d63c
Better error handling in all_callbacks function
kraleppa Dec 5, 2025
092ae2a
Better specs
kraleppa Dec 5, 2025
ab579b0
Cleanup in TracingManager
kraleppa Dec 5, 2025
cbbb504
Split functions
kraleppa Dec 5, 2025
11efa9d
Removed obsolete tests
kraleppa Dec 5, 2025
96b94af
Removed obsolete handler test
kraleppa Dec 5, 2025
0e671c3
Cleanup in tracing_manager_test
kraleppa Dec 5, 2025
1e48478
Wrapped FileSystem library and added mock for it
kraleppa Dec 5, 2025
cb16706
Added tests for paths
kraleppa Dec 5, 2025
e473f07
Added tests for all_callbacks/1
kraleppa Dec 5, 2025
5636807
Add tests for live_module? function
kraleppa Dec 5, 2025
607dfac
Added tests for refresh_tracing/1
kraleppa Dec 5, 2025
8f57d13
Add test for monitor recompilation
kraleppa Dec 5, 2025
f337209
Added case for file_event
kraleppa Dec 5, 2025
fb12f7c
Fix settings e2e test
kraleppa Dec 5, 2025
1c7368b
Installation of inotify-tools
kraleppa Dec 5, 2025
f1c0641
Updated version in README
kraleppa Dec 8, 2025
13a3dae
Updated docs with new version
kraleppa Dec 8, 2025
7aa9cab
Revert "Updated docs with new version"
kraleppa Dec 8, 2025
04df40d
Revert "Updated version in README"
kraleppa Dec 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/backward-compatibility-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install inotify-tools
run: sudo apt-get update && sudo apt-get install -y inotify-tools

- name: Set up Elixir
uses: erlef/[email protected]
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/elixir-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install inotify-tools
run: sudo apt-get update && sudo apt-get install -y inotify-tools

- name: Set up Elixir
uses: erlef/[email protected]
with:
Expand Down
2 changes: 0 additions & 2 deletions lib/live_debugger/api/settings_storage.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
defmodule LiveDebugger.API.SettingsStorage do
@available_settings [
:dead_view_mode,
:tracing_update_on_code_reload,
:garbage_collection,
:debug_button,
:tracing_enabled_on_start,
Expand Down Expand Up @@ -77,7 +76,6 @@ defmodule LiveDebugger.API.SettingsStorage do

@default_settings %{
dead_view_mode: true,
tracing_update_on_code_reload: false,
garbage_collection: true,
debug_button: true,
tracing_enabled_on_start: true,
Expand Down
52 changes: 52 additions & 0 deletions lib/live_debugger/api/system/file_system.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule LiveDebugger.API.System.FileSystem do
@moduledoc """
API for interacting with file system monitoring functionalities.

This module wraps the `FileSystem` library to allow for easier testing and mocking.
"""

@callback start_link(opts :: keyword()) :: GenServer.on_start()
@callback subscribe(name :: atom()) :: :ok

@doc """
Starts a FileSystem monitor process with the given options.

Options:
- `:dirs` - list of directories to monitor
- `:name` - name to register the process under
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts), do: impl().start_link(opts)

@doc """
Subscribes the current process to file system events from the named monitor.

The subscribing process will receive messages in the format:
`{:file_event, pid, {path, events}}`
"""
@spec subscribe(atom()) :: :ok
def subscribe(name), do: impl().subscribe(name)

defp impl() do
Application.get_env(
:live_debugger,
:api_file_system,
__MODULE__.Impl
)
end

defmodule Impl do
@moduledoc false
@behaviour LiveDebugger.API.System.FileSystem

@impl true
def start_link(opts) do
FileSystem.start_link(opts)
end

@impl true
def subscribe(name) do
FileSystem.subscribe(name)
end
end
end
14 changes: 14 additions & 0 deletions lib/live_debugger/api/system/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule LiveDebugger.API.System.Module do
@callback all() :: [{charlist(), charlist(), boolean()}]
@callback loaded?(module :: module()) :: boolean()
@callback behaviours(module :: module()) :: [module()]
@callback live_module?(module :: module()) :: boolean()

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

@doc """
Returns true if the module implements Phoenix.LiveView or Phoenix.LiveComponent behaviour.
"""
@spec live_module?(module :: module()) :: boolean()
def live_module?(module), do: impl().live_module?(module)

defp impl() do
Application.get_env(
:live_debugger,
Expand Down Expand Up @@ -58,5 +65,12 @@ defmodule LiveDebugger.API.System.Module do
# It is safe to return empty list in this case
UndefinedFunctionError -> []
end

@impl true
def live_module?(module) do
module
|> behaviours()
|> Enum.any?(&(&1 == Phoenix.LiveView || &1 == Phoenix.LiveComponent))
end
end
end
2 changes: 1 addition & 1 deletion lib/live_debugger/app/events.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule LiveDebugger.App.Events do
alias LiveDebugger.Structs.LvProcess

defevent(UserChangedSettings,
key: :dead_view_mode | :tracing_update_on_code_reload | :garbage_collection,
key: :dead_view_mode | :garbage_collection,
value: term(),
from: pid()
)
Expand Down
8 changes: 0 additions & 8 deletions lib/live_debugger/app/settings/web/settings_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,6 @@ defmodule LiveDebugger.App.Settings.Web.SettingsLive do
phx-value-setting="dead_view_mode"
/>

<SettingsComponents.settings_switch
id="tracing-update-on-reload-switch"
label="Refresh tracing after recompilation"
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."
checked={@settings[:tracing_update_on_code_reload]}
phx-click="update"
phx-value-setting="tracing_update_on_code_reload"
/>
<SettingsComponents.settings_switch
id="garbage-collection-switch"
label="Garbage Collection"
Expand Down
65 changes: 48 additions & 17 deletions lib/live_debugger/services/callback_tracer/actions/tracing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
@moduledoc """
This module provides actions for tracing.
"""
require Logger

alias LiveDebugger.Services.CallbackTracer.Queries.Callbacks, as: CallbackQueries
alias LiveDebugger.Services.CallbackTracer.Process.Tracer
alias LiveDebugger.API.System.Dbg
alias LiveDebugger.API.SettingsStorage
alias LiveDebugger.API.System.FileSystem, as: FileSystemAPI
alias LiveDebugger.API.System.Module, as: ModuleAPI
alias LiveDebugger.Utils.Modules, as: UtilsModules
alias LiveDebugger.Services.CallbackTracer.Queries.Paths, as: PathQueries

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

if SettingsStorage.get(:tracing_update_on_code_reload) do
Dbg.trace_pattern(
{Mix.Tasks.Compile.Elixir, :run, 1},
Dbg.flag_to_match_spec(:return_trace)
)
end

:ok
end

@spec start_tracing_recompile_pattern() :: :ok
def start_tracing_recompile_pattern() do
Dbg.trace_pattern({Mix.Tasks.Compile.Elixir, :run, 1}, Dbg.flag_to_match_spec(:return_trace))
@spec refresh_tracing() :: :ok
def refresh_tracing() do
apply_trace_patterns()

:ok
end

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

:ok
end

@spec refresh_tracing() :: :ok
def refresh_tracing() do
apply_trace_patterns()
@doc """
Starts FileSystem monitor and subscribes to it for compiled modules directories.
When changes are detected in the monitored directories,
process will receive `{:file_event, _pid, {path, events}}` message.
"""

@spec monitor_recompilation() :: :ok
def monitor_recompilation() do
directories = PathQueries.compiled_modules_directories()
FileSystemAPI.start_link(dirs: directories, name: :lvdbg_file_system_monitor)
FileSystemAPI.subscribe(:lvdbg_file_system_monitor)

:ok
end
Expand All @@ -59,6 +70,22 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
:ok
end

defp refresh_tracing_for_module(module) do
module
|> CallbackQueries.all_callbacks()
|> case do
{:error, error} ->
Logger.error("Error refreshing tracing for module #{module}: #{error}")

callbacks ->
callbacks
|> Enum.each(fn mfa ->
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:return_trace))
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:exception_trace))
end)
end
end

defp apply_trace_patterns() do
# This is not a callback created by user
# We trace it to refresh the components tree
Expand All @@ -70,4 +97,8 @@ defmodule LiveDebugger.Services.CallbackTracer.Actions.Tracing do
Dbg.trace_pattern(mfa, Dbg.flag_to_match_spec(:exception_trace))
end)
end

defp beam_file?(path) do
String.ends_with?(path, ".beam")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do

alias LiveDebugger.Utils.Callbacks, as: CallbackUtils
alias LiveDebugger.Services.CallbackTracer.Actions.FunctionTrace, as: TraceActions
alias LiveDebugger.Services.CallbackTracer.Actions.Tracing, as: TracingActions
alias LiveDebugger.Services.CallbackTracer.Actions.State, as: StateActions
alias LiveDebugger.Services.CallbackTracer.Actions.DiffTrace, as: DiffActions
alias LiveDebugger.Structs.Trace.FunctionTrace
Expand Down Expand Up @@ -53,39 +52,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do
{:ok, %{}}
end

#########################################################
# Handling recompile events
#
# We catch this trace to know when modules were recompiled.
# We do not display this trace to user, so we do not have to care about order
# We need to catch that case because tracer disconnects from modules that were recompiled
# and we need to reapply tracing patterns to them.
# This will be replaced in the future with a more efficient way to handle this.
# https://github.com/software-mansion/live-debugger/issues/592
#
#########################################################

@impl true
def handle_cast(
{:new_trace, {_, _, :return_from, {Mix.Tasks.Compile.Elixir, _, _}, {:ok, _}, _}, _n},
state
) do
Task.start(fn ->
Process.sleep(100)
TracingActions.refresh_tracing()
end)

{:noreply, state}
end

def handle_cast({:new_trace, {_, _, _, {Mix.Tasks.Compile.Elixir, _, _}, _}, _}, state) do
{:noreply, state}
end

def handle_cast({:new_trace, {_, _, _, {Mix.Tasks.Compile.Elixir, _, _}, _, _}, _}, state) do
{:noreply, state}
end

#########################################################
# Handling component deletion traces
#
Expand All @@ -96,6 +62,7 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TraceHandler do
#
#########################################################

@impl true
def handle_cast(
{:new_trace,
{_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}, ts}, n},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TracingManager do
use GenServer

alias LiveDebugger.Bus
alias LiveDebugger.App.Events.UserChangedSettings
alias LiveDebugger.App.Events.UserRefreshedTrace
alias LiveDebugger.Services.ProcessMonitor.Events.LiveViewBorn

Expand Down Expand Up @@ -41,40 +40,36 @@ defmodule LiveDebugger.Services.CallbackTracer.GenServers.TracingManager do
@impl true
def handle_info(:setup_tracing, state) do
TracingActions.setup_tracing!()
TracingActions.monitor_recompilation()

{:noreply, state}
end

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

{:noreply, state}
end

@impl true
def handle_info(%UserChangedSettings{key: :tracing_update_on_code_reload, value: false}, state) do
TracingActions.stop_tracing_recompile_pattern()
def handle_info(%UserRefreshedTrace{}, state) do
TracingActions.refresh_tracing()

{:noreply, state}
end

@impl true
def handle_info(%LiveViewBorn{pid: pid}, state) do
TracingActions.start_outgoing_messages_tracing(pid)
def handle_info({:file_event, _pid, {path, events}}, state) do
if correct_event?(events) do
TracingActions.refresh_tracing(path)
end

{:noreply, state}
end

@impl true
def handle_info(%UserRefreshedTrace{}, state) do
TracingActions.refresh_tracing()

def handle_info(_, state) do
{:noreply, state}
end

@impl true
def handle_info(_, state) do
{:noreply, state}
defp correct_event?(events) do
Enum.any?(events, &(&1 == :modified || &1 == :created))
end
end
Loading