Skip to content

Commit 5c8d1b6

Browse files
authored
Task: Implement filtering by execution time (#379)
* Filter loading existing traces * Apply execution time filter * Filter new traces * Update CallbackTracingServer tests * Update TraceService tests * Fix empty inputs * Add e2e tests
1 parent 0f77924 commit 5c8d1b6

6 files changed

Lines changed: 234 additions & 84 deletions

File tree

lib/live_debugger/services/trace_service.ex

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,14 @@ defmodule LiveDebugger.Services.TraceService do
9797
defp existing_traces_start(table_id, opts) do
9898
limit = Keyword.get(opts, :limit, @default_limit)
9999
functions = Keyword.get(opts, :functions, [])
100+
execution_times = Keyword.get(opts, :execution_times, [])
100101
node_id = Keyword.get(opts, :node_id)
101102

102103
if limit < 1 do
103104
raise ArgumentError, "limit must be >= 1"
104105
end
105106

106-
match_spec = match_spec(node_id, functions)
107+
match_spec = match_spec(node_id, functions, execution_times)
107108

108109
table_id
109110
|> ets_table!()
@@ -118,36 +119,65 @@ defmodule LiveDebugger.Services.TraceService do
118119
:ets.select(cont)
119120
end
120121

121-
defp match_spec(node_id, functions) when is_pid(node_id) do
122+
defp match_spec(node_id, functions, execution_times) when is_pid(node_id) do
122123
[
123124
{{:_, %{function: :"$1", execution_time: :"$2", pid: node_id, cid: nil}},
124-
to_spec(functions), [:"$_"]}
125+
to_spec(functions, execution_times), [:"$_"]}
125126
]
126127
end
127128

128-
defp match_spec(%CID{} = node_id, functions) do
129-
[{{:_, %{function: :"$1", execution_time: :"$2", cid: node_id}}, to_spec(functions), [:"$_"]}]
129+
defp match_spec(%CID{} = node_id, functions, execution_times) do
130+
[
131+
{{:_, %{function: :"$1", execution_time: :"$2", cid: node_id}},
132+
to_spec(functions, execution_times), [:"$_"]}
133+
]
134+
end
135+
136+
defp match_spec(nil, functions, execution_times) do
137+
[
138+
{{:_, %{function: :"$1", execution_time: :"$2"}}, to_spec(functions, execution_times),
139+
[:"$_"]}
140+
]
141+
end
142+
143+
def to_spec([], []), do: [{:"/=", :"$2", nil}]
144+
145+
def to_spec(functions, []) do
146+
[{:andalso, List.first(functions_to_spec(functions)), {:"/=", :"$2", nil}}]
130147
end
131148

132-
defp match_spec(nil, functions) do
133-
[{{:_, %{function: :"$1", execution_time: :"$2"}}, to_spec(functions), [:"$_"]}]
149+
def to_spec([], execution_times) do
150+
[{:andalso, List.first(execution_times_to_spec(execution_times)), {:"/=", :"$2", nil}}]
134151
end
135152

136-
def to_spec([]), do: [{:"/=", :"$2", nil}]
153+
def to_spec(functions, execution_times) do
154+
[
155+
{:andalso,
156+
{:andalso, List.first(functions_to_spec(functions)),
157+
List.first(execution_times_to_spec(execution_times))}, {:"/=", :"$2", nil}}
158+
]
159+
end
137160

138-
def to_spec([single]), do: [{:andalso, {:"=:=", :"$1", single}, {:"/=", :"$2", nil}}]
161+
def functions_to_spec([single]), do: [{:"=:=", :"$1", single}]
139162

140-
def to_spec([first, second | rest]) do
141-
initial_orelse = {:orelse, List.first(to_spec([first])), List.first(to_spec([second]))}
163+
def functions_to_spec([first, second | rest]) do
164+
initial_orelse =
165+
{:orelse, List.first(functions_to_spec([first])), List.first(functions_to_spec([second]))}
142166

143167
result =
144168
Enum.reduce(rest, initial_orelse, fn x, acc ->
145-
{:orelse, acc, List.first(to_spec([x]))}
169+
{:orelse, acc, List.first(functions_to_spec([x]))}
146170
end)
147171

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

175+
def execution_times_to_spec(execution_times) do
176+
min_time = Keyword.get(execution_times, :exec_time_min, 0)
177+
max_time = Keyword.get(execution_times, :exec_time_max, :infinity)
178+
[{:andalso, {:>=, :"$2", min_time}, {:"=<", :"$2", max_time}}]
179+
end
180+
151181
@spec ets_table!(pid :: ets_table_id()) :: :ets.table()
152182
defp ets_table!(pid) when is_pid(pid) do
153183
EtsTableServer.table!(pid)

lib/live_debugger_web/live/traces_live.ex

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,20 @@ defmodule LiveDebuggerWeb.TracesLive do
244244
def handle_info({:updated_trace, trace}, socket) when socket.assigns.trace_callback_running? do
245245
trace_display = TraceDisplay.from_trace(trace, true)
246246

247-
socket
247+
execution_time = get_execution_times(socket)
248+
min_time = Keyword.get(execution_time, :exec_time_min, 0)
249+
max_time = Keyword.get(execution_time, :exec_time_max, :infinity)
250+
251+
if trace.execution_time >= min_time and trace.execution_time <= max_time do
252+
socket
253+
|> stream_insert(:existing_traces, trace_display, at: 0, limit: @live_stream_limit)
254+
else
255+
socket
256+
|> stream_delete(:existing_traces, trace_display)
257+
end
248258
|> assign(trace_callback_running?: false)
249259
|> TracingHelper.maybe_disable_tracing_after_update()
250260
|> push_event("stop-timer", %{})
251-
|> stream_insert(:existing_traces, trace_display, at: 0, limit: @live_stream_limit)
252261
|> noreply()
253262
end
254263

@@ -366,6 +375,7 @@ defmodule LiveDebuggerWeb.TracesLive do
366375
pid = socket.assigns.lv_process.pid
367376
node_id = socket.assigns.node_id
368377
active_functions = get_active_functions(socket)
378+
execution_times = get_execution_times(socket)
369379

370380
socket
371381
|> assign(:existing_traces_status, :loading)
@@ -374,7 +384,8 @@ defmodule LiveDebuggerWeb.TracesLive do
374384
TraceService.existing_traces(pid,
375385
node_id: node_id,
376386
limit: @page_size,
377-
functions: active_functions
387+
functions: active_functions,
388+
execution_times: execution_times
378389
)
379390
end)
380391
end
@@ -384,6 +395,7 @@ defmodule LiveDebuggerWeb.TracesLive do
384395
node_id = socket.assigns.node_id
385396
cont = socket.assigns.traces_continuation
386397
active_functions = get_active_functions(socket)
398+
execution_times = get_execution_times(socket)
387399

388400
socket
389401
|> assign(:traces_continuation, :loading)
@@ -392,7 +404,8 @@ defmodule LiveDebuggerWeb.TracesLive do
392404
node_id: node_id,
393405
limit: @page_size,
394406
cont: cont,
395-
functions: active_functions
407+
functions: active_functions,
408+
execution_times: execution_times
396409
)
397410
end)
398411
end
@@ -411,7 +424,7 @@ defmodule LiveDebuggerWeb.TracesLive do
411424
functions: functions,
412425
execution_time: [
413426
{:exec_time_max, ""},
414-
{:exec_time_min, "0"}
427+
{:exec_time_min, ""}
415428
]
416429
}
417430
end
@@ -422,6 +435,12 @@ defmodule LiveDebuggerWeb.TracesLive do
422435
|> Enum.map(fn {function, _} -> function end)
423436
end
424437

438+
defp get_execution_times(socket) do
439+
socket.assigns.current_filters.execution_time
440+
|> Enum.filter(fn {_, value} -> value != "" end)
441+
|> Enum.map(fn {filter, value} -> {filter, String.to_integer(value)} end)
442+
end
443+
425444
defp log_async_error(operation, reason) do
426445
Logger.error(
427446
"LiveDebugger encountered unexpected error while #{operation}: #{inspect(reason)}"

lib/live_debugger_web/live_components/filters_form.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,21 @@ defmodule LiveDebuggerWeb.LiveComponents.FiltersForm do
144144
{filter, Map.get(params, Atom.to_string(filter), value)}
145145
end)
146146

147+
case validate_execution_time(execution_time) do
148+
:ok -> {:ok, %{functions: functions, execution_time: execution_time}}
149+
{:error, errors} -> {:error, errors}
150+
end
151+
end
152+
153+
defp validate_execution_time(execution_time) do
147154
min_time = Keyword.get(execution_time, :exec_time_min, 0)
148155
max_time = Keyword.get(execution_time, :exec_time_max, :infinity)
149156

150-
if String.to_integer(min_time) > String.to_integer(max_time) do
157+
if min_time != "" and max_time != "" and
158+
String.to_integer(min_time) > String.to_integer(max_time) do
151159
{:error, [exec_time_min: "min must be less than max", exec_time_max: ""]}
152160
else
153-
{:ok, %{functions: functions, execution_time: execution_time}}
161+
:ok
154162
end
155163
end
156164

test/gen_servers/callback_tracing_server_test.exs

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ defmodule LiveDebugger.GenServers.CallbackTracingServerTest do
8585
module = Phoenix.LiveView.Diff
8686
fun = :delete_component
8787
args = [cid, %{}]
88+
timestamp = :erlang.timestamp()
89+
90+
expected_trace = %Trace{
91+
id: 0,
92+
module: module,
93+
function: fun,
94+
arity: 2,
95+
args: args,
96+
socket_id: socket_id,
97+
transport_pid: transport_pid,
98+
pid: pid,
99+
cid: %Phoenix.LiveComponent.CID{cid: cid},
100+
timestamp: :timer.now_diff(timestamp, {0, 0, 0})
101+
}
88102

89103
component_deleted_topic =
90104
PubSubUtils.component_deleted_topic()
@@ -95,26 +109,14 @@ defmodule LiveDebugger.GenServers.CallbackTracingServerTest do
95109
end)
96110

97111
MockPubSubUtils
98-
|> expect(:broadcast, fn ^component_deleted_topic, {:component_deleted, trace} ->
99-
send(parent, {:trace, trace})
112+
|> expect(:broadcast, fn ^component_deleted_topic, {:component_deleted, ^expected_trace} ->
113+
send(parent, :broadcasted)
100114
end)
101115

102116
assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{})
103117
assert_receive handle_trace
104-
assert 0 = handle_trace.({:trace, pid, :call, {module, fun, args}, :erlang.timestamp()}, 0)
105-
assert_receive {:trace, trace}
106-
107-
assert %Trace{
108-
id: 0,
109-
module: ^module,
110-
function: ^fun,
111-
arity: 2,
112-
args: ^args,
113-
socket_id: ^socket_id,
114-
transport_pid: ^transport_pid,
115-
pid: ^pid,
116-
cid: %Phoenix.LiveComponent.CID{cid: ^cid}
117-
} = trace
118+
assert 0 = handle_trace.({:trace, pid, :call, {module, fun, args}, timestamp}, 0)
119+
assert_receive :broadcasted
118120
end
119121

120122
test "handle standard live view trace" do
@@ -131,19 +133,28 @@ defmodule LiveDebugger.GenServers.CallbackTracingServerTest do
131133

132134
table = :ets.new(:test_table, [:ordered_set, :public])
133135

134-
expected_trace_topic = PubSubUtils.trace_topic(socket_id, transport_pid, pid, fun)
136+
expected_trace_call_topic =
137+
PubSubUtils.trace_topic(socket_id, transport_pid, pid, fun, :call)
138+
139+
expected_trace_return_topic =
140+
PubSubUtils.trace_topic(socket_id, transport_pid, pid, fun, :return)
135141

136142
MockEtsTableServer
137-
|> expect(:table!, fn ^pid -> table end)
143+
|> expect(:table!, 2, fn ^pid -> table end)
138144

139145
MockPubSubUtils
140-
|> expect(:broadcast, fn ^expected_trace_topic, {:new_trace, _trace} -> :ok end)
146+
|> expect(:broadcast, fn ^expected_trace_call_topic, {:new_trace, _trace} -> :ok end)
147+
|> expect(:broadcast, fn ^expected_trace_return_topic, {:updated_trace, _trace} -> :ok end)
141148

142149
assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{})
143150
assert_receive handle_trace
144-
assert -1 = handle_trace.({:trace, pid, :call, {module, fun, args}, :erlang.timestamp()}, 0)
151+
152+
call_timestamp = :erlang.timestamp()
153+
assert -1 = handle_trace.({:trace, pid, :call, {module, fun, args}, call_timestamp}, 0)
145154
assert [{0, trace}] = :ets.tab2list(table)
146155

156+
expected_timestamp = :timer.now_diff(call_timestamp, {0, 0, 0})
157+
147158
assert %Trace{
148159
id: 0,
149160
module: ^module,
@@ -153,8 +164,25 @@ defmodule LiveDebugger.GenServers.CallbackTracingServerTest do
153164
socket_id: ^socket_id,
154165
transport_pid: ^transport_pid,
155166
pid: ^pid,
156-
cid: nil
167+
cid: nil,
168+
timestamp: ^expected_timestamp,
169+
execution_time: nil
157170
} = trace
171+
172+
return_timestamp = :erlang.timestamp()
173+
174+
assert -1 =
175+
handle_trace.(
176+
{:trace, pid, :return_from, {module, fun, length(args)}, {:noreply, %{}},
177+
return_timestamp},
178+
-1
179+
)
180+
181+
assert [{0, updated_trace}] = :ets.tab2list(table)
182+
183+
expected_execution_time = :timer.now_diff(return_timestamp, call_timestamp)
184+
185+
assert %{trace | execution_time: expected_execution_time} == updated_trace
158186
end
159187

160188
test "handle unexpected trace" do

test/live_debugger/channel_dashboard_test.exs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule LiveDebugger.ChannelDashboardTest do
1616
|> assert_has(counter_in_assigns(text: "0"))
1717
|> assert_has(traces(count: 2))
1818

19+
# Callback traces appear in debugger
1920
dev_app
2021
|> click(button("increment-button"))
2122
|> click(button("increment-button"))
@@ -46,6 +47,7 @@ defmodule LiveDebugger.ChannelDashboardTest do
4647
|> assert_has(traces(count: 4))
4748
|> click(clear_traces_button())
4849

50+
# Callback traces have proper execution times displayed
4951
dev_app
5052
|> click(button("slow-increment-button"))
5153

@@ -58,7 +60,6 @@ defmodule LiveDebugger.ChannelDashboardTest do
5860
execution_time =
5961
debugger
6062
|> click(refresh_button())
61-
|> take_screenshot()
6263
|> find(traces(count: 2))
6364
|> List.last()
6465
|> find(css("span.text-warning-text"))
@@ -78,6 +79,35 @@ defmodule LiveDebugger.ChannelDashboardTest do
7879
|> find(traces(count: 4))
7980
|> Enum.at(1)
8081
|> assert_has(css("span.text-error-text", text: "2.50 s"))
82+
83+
# Filtering callback traces by execution time works
84+
debugger
85+
|> click(toggle_tracing_button())
86+
|> click(filters_button())
87+
|> fill_in(text_field("exec_time_min"), with: 100)
88+
|> fill_in(text_field("exec_time_max"), with: 2_000_000)
89+
|> send_keys([:enter])
90+
|> find(traces(count: 1))
91+
|> find(css("span.text-warning-text"))
92+
|> Element.text()
93+
|> String.match?(~r"^40\d ms$")
94+
95+
debugger
96+
|> click(toggle_tracing_button())
97+
98+
dev_app
99+
|> click(button("increment-button"))
100+
|> click(button("slow-increment-button"))
101+
102+
Process.sleep(405)
103+
104+
debugger
105+
|> find(traces(count: 2))
106+
|> Enum.each(fn trace ->
107+
find(trace, css("span.text-warning-text"))
108+
|> Element.text()
109+
|> String.match?(~r"^40\d ms$")
110+
end)
81111
end
82112

83113
defp first_link(), do: css("#live-sessions a", count: 1)
@@ -95,4 +125,6 @@ defmodule LiveDebugger.ChannelDashboardTest do
95125
defp toggle_tracing_button(), do: css("button[phx-click=\"switch-tracing\"]")
96126

97127
defp clear_traces_button(), do: css("button[phx-click=\"clear-traces\"]")
128+
129+
defp filters_button(), do: css("button[phx-click=\"open\"]")
98130
end

0 commit comments

Comments
 (0)