Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7de8593
Get execution time using dbg trace return_from
hhubert6 Apr 1, 2025
11a1499
Implement callback execution time measuring
hhubert6 Apr 9, 2025
4b8bc36
Display callback execution time
hhubert6 Apr 10, 2025
df49d4d
Add spinner
hhubert6 Apr 10, 2025
1f34611
Add parsing elapsed time
hhubert6 Apr 10, 2025
89a958d
Merge branch 'main' into 136-research-displaying-event-handling-time
hhubert6 Apr 11, 2025
24967b4
Change execution time visualization
hhubert6 Apr 11, 2025
e3ff32f
Add timer
hhubert6 Apr 16, 2025
55fc762
Small tweaks
hhubert6 Apr 16, 2025
2408464
Remove console.log
hhubert6 Apr 17, 2025
3c1737a
Rename hook
hhubert6 Apr 17, 2025
9e11c9a
Merge branch 'main' into 276-implement-displaying-event-handling-time
hhubert6 Apr 17, 2025
0867379
Change timer stopping
hhubert6 Apr 17, 2025
667cb27
Refactor
hhubert6 Apr 22, 2025
0aeda41
Merge branch 'main' into 276-implement-displaying-event-handling-time
hhubert6 May 5, 2025
eff4e60
Add tooltips
hhubert6 May 5, 2025
f1fdbf2
Refactor
hhubert6 May 5, 2025
1ef77ac
Fix infinite loading spinner
hhubert6 May 7, 2025
b5714fd
Show only finished callbacks on refresh
hhubert6 May 8, 2025
7b7d76b
Fix trace callback return after tracing stopped
hhubert6 May 8, 2025
0ad0fef
Save timestamp to trace from tracer handler
hhubert6 May 8, 2025
a473dfe
Update TraceService test
hhubert6 May 8, 2025
0777e66
fix
hhubert6 May 8, 2025
08748e8
Merge branch 'main' into 276-implement-displaying-event-handling-time
hhubert6 May 8, 2025
8433c7d
Update tests
hhubert6 May 8, 2025
8fb5b95
Add e2e test cases
hhubert6 May 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 assets/css/themes/dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,8 @@
--error-border: var(--gray-700);
--error-icon: var(--swm-pink-80);
--error-text: var(--swm-pink-80);

/* Warning */
--warning-text: var(--swm-yellow-40);
}
}
3 changes: 3 additions & 0 deletions assets/css/themes/light.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,8 @@
--error-border: var(--red-100);
--error-icon: var(--red-500);
--error-text: var(--red-800);

/* Warning */
--warning-text: #9a7e00;
}
}
2 changes: 2 additions & 0 deletions assets/js/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Tooltip from './hooks/tooltip';
import Highlight from './hooks/highlight';
import LiveDropdown from './hooks/live_dropdown';
import AutoClearFlash from './hooks/auto_clear_flash';
import TraceExecutionTime from './hooks/trace_execution_time';

import topbar from '../vendor/topbar';

Expand All @@ -28,6 +29,7 @@ function createHooks() {
Highlight,
LiveDropdown,
AutoClearFlash,
TraceExecutionTime,
};
}

Expand Down
25 changes: 25 additions & 0 deletions assets/js/hooks/trace_execution_time.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const TraceExecutionTime = {
mounted() {
let start = Date.now();
let current = start;
let handled = false;

this.intervalId = setInterval(() => {
current = Date.now() - start;
this.el.textContent = current + ' ms';
}, 16);

this.handleEvent('stop-timer', () => {
if (!handled) {
clearInterval(this.intervalId);
this.el.closest('details').open = false;
handled = true;
}
});
},
destroyed() {
clearInterval(this.intervalId);
},
};

export default TraceExecutionTime;
1 change: 1 addition & 0 deletions assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ module.exports = {
'error-border': 'var(--error-border)',
'error-icon': 'var(--error-icon)',
'error-text': 'var(--error-text)',
'warning-text': 'var(--warning-text)',
},
screens: { xs: '380px' },
fontFamily: {
Expand Down
26 changes: 25 additions & 1 deletion dev/live_views/main.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule LiveDebuggerDev.LiveViews.Main do
socket =
socket
|> assign(counter: 0)
|> assign(counter_slow: 0)
|> assign(counter_very_slow: 0)
|> assign(datetime: nil)
|> assign(name: random_name())
|> assign(single_element_list: [%Phoenix.LiveComponent.CID{cid: 1}])
Expand All @@ -29,6 +31,18 @@ defmodule LiveDebuggerDev.LiveViews.Main do
</.button>
<span class="text-xl"><%= @counter %></span>
</div>
<div class="flex items-center gap-2">
<.button id="slow-increment-button" phx-click="slow-increment" color="blue">
Slow Increment
</.button>
<span class="text-xl"><%= @counter_slow %></span>
</div>
<div class="flex items-center gap-2">
<.button id="very-slow-increment-button" phx-click="very-slow-increment" color="blue">
Very Slow Increment
</.button>
<span class="text-xl"><%= @counter_very_slow %></span>
</div>
<div class="flex items-center gap-1">
<.button id="update-button" phx-click="change_name" color="red">
Update
Expand Down Expand Up @@ -72,7 +86,17 @@ defmodule LiveDebuggerDev.LiveViews.Main do
end

def handle_event("increment", _, socket) do
{:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
{:noreply, update(socket, :counter, &(&1 + 1))}
end

def handle_event("slow-increment", _, socket) do
Process.sleep(400)
{:noreply, update(socket, :counter_slow, &(&1 + 1))}
end

def handle_event("very-slow-increment", _, socket) do
Process.sleep(2500)
{:noreply, update(socket, :counter_very_slow, &(&1 + 1))}
end

def handle_event("change_name", _, socket) do
Expand Down
61 changes: 55 additions & 6 deletions lib/live_debugger/gen_servers/callback_tracing_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
@impl true
def handle_info(:setup_tracing, state) do
Dbg.tracer(:process, {&handle_trace/2, 0})
Dbg.p(:all, :c)
Dbg.p(:all, [:c, :timestamp])

all_modules = ModuleDiscoveryService.all_modules()

Expand All @@ -58,7 +58,10 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
|> ModuleDiscoveryService.live_component_modules()
|> CallbackUtils.live_component_callbacks()
|> Enum.concat(callbacks)
|> Enum.each(fn mfa -> Dbg.tp(mfa, []) end)
|> Enum.each(fn mfa ->
Dbg.tp(mfa, [{:_, [], [{:return_trace}]}])
Dbg.tp(mfa, [{:_, [], [{:exception_trace}]}])
end)

# This is not a callback created by user
# We trace it to refresh the components tree
Expand All @@ -76,7 +79,10 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
# Because of that we do it asynchronously to speed up tracer a bit
# We do not persist this trace because it is not displayed to user
@spec handle_trace(term(), n :: integer()) :: integer()
defp handle_trace({_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}}, n) do
defp handle_trace(
{_, pid, _, {Phoenix.LiveView.Diff, :delete_component, [cid | _] = args}, timestamp},
n
) do
Task.start(fn ->
with cid <- %Phoenix.LiveComponent.CID{cid: cid},
{:ok, %{socket: socket}} <- ChannelService.state(pid),
Expand All @@ -89,6 +95,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
:delete_component,
args,
pid,
timestamp,
socket_id: socket_id,
transport_pid: transport_pid,
cid: cid
Expand All @@ -102,16 +109,36 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do

# This handles callbacks created by user that will be displayed to user
# It cannot be async because we care about order
defp handle_trace({_, pid, _, {module, fun, args}}, n) when fun in @callback_functions do
with trace <- Trace.new(n, module, fun, args, pid),
defp handle_trace({_, pid, :call, {module, fun, args}, timestamp}, n)
when fun in @callback_functions do
with trace <- Trace.new(n, module, fun, args, pid, timestamp),
true <- is_pid(trace.transport_pid),
:ok <- persist_trace(trace) do
:erlang.put({pid, module, fun}, {timestamp, trace})
publish_trace(trace)
end

n - 1
end

defp handle_trace({_, pid, :return_from, {module, fun, _arity}, _, return_ts}, n)
when fun in @callback_functions do
with {call_ts, trace} <- :erlang.get({pid, module, fun}),
execution_time <- :timer.now_diff(return_ts, call_ts),
trace <- %{trace | execution_time: execution_time},
:ok <- persist_trace(trace) do
:erlang.erase({pid, module, fun})
publish_update_trace(trace)
end

n
end

defp handle_trace({_, _pid, :exception_from, {_module, fun, _}, _, _timestamp}, n)
when fun in @callback_functions do
n
end

defp handle_trace(trace, n) do
Logger.info("Ignoring unexpected trace: #{inspect(trace)}")
n
Expand All @@ -138,6 +165,16 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
{:error, err}
end

@spec publish_update_trace(Trace.t()) :: :ok | {:error, term()}
defp publish_update_trace(%Trace{} = trace) do
do_publish_update(trace)
:ok
rescue
err ->
Logger.error("Error while publishing trace: #{inspect(err)}")
{:error, err}
end

@spec do_publish(Trace.t()) :: :ok
defp do_publish(%{module: Phoenix.LiveView.Diff} = trace) do
trace
Expand All @@ -152,11 +189,23 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
fun = trace.function

socket_id
|> PubSubUtils.tsnf_topic(transport_pid, node_id, fun)
|> PubSubUtils.tsnf_topic(transport_pid, node_id, fun, :call)
|> PubSubUtils.broadcast({:new_trace, trace})

socket_id
|> PubSubUtils.ts_f_topic(transport_pid, fun)
|> PubSubUtils.broadcast({:new_trace, trace})
end

@spec do_publish_update(Trace.t()) :: :ok
defp do_publish_update(trace) do
socket_id = trace.socket_id
node_id = Trace.node_id(trace)
transport_pid = trace.transport_pid
fun = trace.function

socket_id
|> PubSubUtils.tsnf_topic(transport_pid, node_id, fun, :return)
|> PubSubUtils.broadcast({:updated_trace, trace})
end
end
13 changes: 7 additions & 6 deletions lib/live_debugger/services/trace_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,22 @@ defmodule LiveDebugger.Services.TraceService do

defp match_spec(node_id, functions) when is_pid(node_id) do
[
{{:_, %{function: :"$1", pid: node_id, cid: nil}}, to_spec(functions), [:"$_"]}
{{:_, %{function: :"$1", execution_time: :"$2", pid: node_id, cid: nil}},
to_spec(functions), [:"$_"]}
]
end

defp match_spec(%CID{} = node_id, functions) do
[{{:_, %{function: :"$1", cid: node_id}}, to_spec(functions), [:"$_"]}]
[{{:_, %{function: :"$1", execution_time: :"$2", cid: node_id}}, to_spec(functions), [:"$_"]}]
end

defp match_spec(nil, functions) do
[{{:_, %{function: :"$1"}}, to_spec(functions), [:"$_"]}]
[{{:_, %{function: :"$1", execution_time: :"$2"}}, to_spec(functions), [:"$_"]}]
end

def to_spec([]), do: []
def to_spec([]), do: [{:"/=", :"$2", nil}]

def to_spec([single]), do: [{:"=:=", :"$1", single}]
def to_spec([single]), do: [{:andalso, {:"=:=", :"$1", single}, {:"/=", :"$2", nil}}]

def to_spec([first, second | rest]) do
initial_orelse = {:orelse, List.first(to_spec([first])), List.first(to_spec([second]))}
Expand All @@ -144,7 +145,7 @@ defmodule LiveDebugger.Services.TraceService do
{:orelse, acc, List.first(to_spec([x]))}
end)

[result]
[{:andalso, result, {:"/=", :"$2", nil}}]
end

@spec ets_table!(pid :: ets_table_id()) :: :ets.table()
Expand Down
15 changes: 10 additions & 5 deletions lib/live_debugger/structs/trace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ defmodule LiveDebugger.Structs.Trace do
:transport_pid,
:pid,
:cid,
:timestamp
:timestamp,
:execution_time
]

@type timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}

@type t() :: %__MODULE__{
id: integer(),
module: atom(),
Expand All @@ -36,14 +39,15 @@ defmodule LiveDebugger.Structs.Trace do
transport_pid: pid() | nil,
pid: pid(),
cid: struct() | nil,
timestamp: non_neg_integer()
timestamp: timestamp(),
execution_time: non_neg_integer() | nil
}

@doc """
Creates a new trace struct.
"""
@spec new(integer(), atom(), atom(), list(), pid(), Keyword.t()) :: t()
def new(id, module, function, args, pid, opts \\ []) do
@spec new(integer(), atom(), atom(), list(), pid(), timestamp(), Keyword.t()) :: t()
def new(id, module, function, args, pid, timestamp, opts \\ []) do
socket_id = Keyword.get(opts, :socket_id, get_socket_id_from_args(args))
transport_pid = Keyword.get(opts, :transport_pid, get_transport_pid_from_args(args))
cid = Keyword.get(opts, :cid, get_cid_from_args(args))
Expand All @@ -58,7 +62,8 @@ defmodule LiveDebugger.Structs.Trace do
transport_pid: transport_pid,
pid: pid,
cid: cid,
timestamp: :os.system_time(:microsecond)
timestamp: :timer.now_diff(timestamp, {0, 0, 0}),
execution_time: nil
}
end

Expand Down
9 changes: 5 additions & 4 deletions lib/live_debugger/structs/trace_display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ defmodule LiveDebugger.Structs.TraceDisplay do

alias LiveDebugger.Structs.Trace

defstruct [:id, :trace, :render_body?, :counter]
defstruct [:id, :trace, :render_body?, :from_tracing?, :counter]

@type t() :: %__MODULE__{
id: integer(),
trace: Trace.t(),
render_body?: boolean()
render_body?: boolean(),
from_tracing?: boolean()
}

def from_trace(%Trace{} = trace) do
%__MODULE__{id: trace.id, trace: trace, render_body?: false}
def from_trace(%Trace{} = trace, from_tracing? \\ false) do
%__MODULE__{id: trace.id, trace: trace, render_body?: false, from_tracing?: from_tracing?}
end

def render_body(%__MODULE__{} = trace) do
Expand Down
4 changes: 3 additions & 1 deletion lib/live_debugger/utils/parsers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ defmodule LiveDebugger.Utils.Parsers do
end
end

@spec parse_elapsed_time(non_neg_integer()) :: String.t()
@spec parse_elapsed_time(non_neg_integer() | nil) :: String.t()
def parse_elapsed_time(nil), do: ""

def parse_elapsed_time(microseconds) do
cond do
microseconds < 1_000 -> "#{microseconds} µs"
Expand Down
7 changes: 4 additions & 3 deletions lib/live_debugger/utils/pubsub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ defmodule LiveDebugger.Utils.PubSub do
socket_id :: String.t(),
transport_pid :: pid(),
node_id :: TreeNode.id(),
fun :: atom()
fun :: atom(),
type :: :call | :return
) :: String.t()
def tsnf_topic(socket_id, transport_pid, node_id, fun) do
"#{inspect(transport_pid)}/#{socket_id}/#{inspect(node_id)}/#{inspect(fun)}"
def tsnf_topic(socket_id, transport_pid, node_id, fun, type \\ :call) do
"#{inspect(transport_pid)}/#{socket_id}/#{inspect(node_id)}/#{inspect(fun)}/#{inspect(type)}"
end

@doc """
Expand Down
Loading