Skip to content

Commit 4f07670

Browse files
jecholjosevalim
authored andcommitted
Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)
When :elixir app starts, store the dbg_callback value in dbg_callback_initial. Mix compiler now compares against dbg_callback_initial instead of dbg_callback. This prevents unnecessary recompilation when tools like Kino modify dbg_callback at runtime. Previously, such runtime modifications would trigger a full recompilation of all files using dbg/2, even though the config hadn't actually changed. The key insight is that dbg/2 is a compile-time macro, so runtime modifications to dbg_callback don't affect already-compiled code. Only actual config changes (reflected in dbg_callback_initial) should trigger recompilation. This is a more general solution than detecting specific wrapping patterns, as it works with any tool that modifies dbg_callback at runtime.
1 parent dcff0ff commit 4f07670

File tree

3 files changed

+45
-2
lines changed

3 files changed

+45
-2
lines changed

lib/elixir/src/elixir.erl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ start(_Type, _Args) ->
6464
application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true)
6565
end,
6666

67+
%% Store the initial dbg_callback value before any runtime modifications.
68+
%% This allows Mix compiler to detect config changes vs runtime changes
69+
%% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation).
70+
{ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback),
71+
6772
Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of
6873
{module, Mod} -> Mod;
6974
_ -> elixir_tokenizer
@@ -90,6 +95,7 @@ start(_Type, _Args) ->
9095
{docs, true},
9196
{ignore_already_consolidated, false},
9297
{ignore_module_conflict, false},
98+
{initial_dbg_callback, InitialDbgCallback},
9399
{infer_signatures, [elixir]},
94100
{on_undefined_variable, raise},
95101
{parser_options, [{columns, true}]},

lib/mix/lib/mix/compilers/elixir.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,13 @@ defmodule Mix.Compilers.Elixir do
287287
end
288288

289289
defp deps_config_compile_env_apps(deps_config) do
290-
if deps_config[:dbg] != Application.fetch_env!(:elixir, :dbg_callback) do
290+
# Use initial_dbg_callback instead of dbg_callback to ignore runtime modifications.
291+
# Tools like Kino modify dbg_callback at runtime to customize dbg/2 behavior,
292+
# but this should not trigger recompilation since the config hasn't actually changed.
293+
# initial_dbg_callback is set when :elixir app starts, before any runtime modifications.
294+
initial_dbg = :elixir_config.get(:initial_dbg_callback)
295+
296+
if deps_config[:dbg] != initial_dbg do
291297
[:elixir]
292298
else
293299
[]

lib/mix/test/mix/tasks/compile.elixir_test.exs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
278278
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
279279
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
280280

281-
# Change the dbg_callback at runtime
281+
# Simulate a config change by updating both dbg_callback and initial_dbg_callback.
282+
# This represents the case where the user actually changed the config file.
282283
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
283284
Application.put_env(:elixir, :dbg_callback, {__MODULE__, :dbg, []})
285+
:elixir_config.put(:initial_dbg_callback, {__MODULE__, :dbg, []})
284286

285287
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
286288
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -289,6 +291,35 @@ defmodule Mix.Tasks.Compile.ElixirTest do
289291
end)
290292
after
291293
Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []})
294+
:elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []})
295+
end
296+
297+
test "does not recompile when dbg_callback is modified at runtime but initial is unchanged" do
298+
in_fixture("no_mixfile", fn ->
299+
Mix.Project.push(MixTest.Case.Sample)
300+
301+
File.write!("lib/a.ex", """
302+
defmodule A do
303+
def a, do: dbg(:ok)
304+
end
305+
""")
306+
307+
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
308+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
309+
310+
# Simulate a tool like Kino modifying dbg_callback at runtime.
311+
# Since initial_dbg_callback remains unchanged, this should NOT trigger recompilation.
312+
original_dbg = Application.fetch_env!(:elixir, :dbg_callback)
313+
:elixir_config.put(:initial_dbg_callback, original_dbg)
314+
Application.put_env(:elixir, :dbg_callback, {SomeDebugTool, :dbg, [original_dbg]})
315+
316+
# Should NOT trigger recompilation since initial_dbg_callback is unchanged
317+
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []}
318+
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
319+
end)
320+
after
321+
Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []})
322+
:elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []})
292323
end
293324

294325
test "recompiles files when config changes export dependencies" do

0 commit comments

Comments
 (0)