diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 075795be5b..be073bc896 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -794,6 +794,27 @@ defmodule Code do [:docs, :debug_info, :ignore_module_conflict, :relative_paths, :warnings_as_errors] end + @doc """ + Purge compiler modules. + + The compiler utilizes temporary modules to compile code. For example, + `elixir_compiler_1`, `elixir_compiler_2`, etc. In case the compiled code + stores references to anonymous functions or similar, the Elixir compiler + may be unable to reclaim those modules, keeping an unecessary amount of + code in memory and eventually leading to modules such as `elixir_compiler_12345`. + + This function purges all modules currently kept by the compiler, allowing + old compiler module names to be resued. If there are any processes running + any code from such modules, they will be terminated too. + + It returns `{:ok, number_of_modules_purged}`. + """ + @since "1.7.0" + @spec purge_compiler_modules() :: {:ok, non_neg_integer()} + def purge_compiler_modules() do + :elixir_code_server.call(:purge_compiler_modules) + end + @doc """ Sets compilation options. diff --git a/lib/elixir/src/elixir_code_server.erl b/lib/elixir/src/elixir_code_server.erl index 3e6826a690..d05fc08b44 100644 --- a/lib/elixir/src/elixir_code_server.erl +++ b/lib/elixir/src/elixir_code_server.erl @@ -7,7 +7,7 @@ -define(timeout, 30000). -record(elixir_code_server, { required=#{}, - mod_pool={[], 0}, + mod_pool={[], [], 0}, mod_ets=#{}, compilation_status=#{} }). @@ -65,12 +65,18 @@ handle_call({compilation_status, CompilerPid}, _From, Config) -> handle_call(retrieve_compiler_module, _From, Config) -> case Config#elixir_code_server.mod_pool of - {[H | T], Counter} -> - {reply, module_tuple(H), Config#elixir_code_server{mod_pool={T, Counter}}}; - {[], Counter} -> - {reply, module_tuple(Counter), Config#elixir_code_server{mod_pool={[], Counter+1}}} + {Used, [Mod | Unused], Counter} -> + {reply, Mod, Config#elixir_code_server{mod_pool={Used, Unused, Counter}}}; + {Used, [], Counter} -> + {reply, compiler_module(Counter), Config#elixir_code_server{mod_pool={Used, [], Counter+1}}} end; +handle_call(purge_compiler_modules, _From, Config) -> + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + _ = [code:purge(Module) || Module <- Used], + ModPool = {[], Used ++ Unused, Counter}, + {reply, {ok, length(Used)}, Config#elixir_code_server{mod_pool=ModPool}}; + handle_call(Request, _From, Config) -> {stop, {badcall, Request}, Config}. @@ -107,8 +113,16 @@ handle_cast({unrequire_files, Files}, Config) -> Unrequired = maps:without(Files, Current), {noreply, Config#elixir_code_server{required=Unrequired}}; -handle_cast({return_compiler_module, H}, #elixir_code_server{mod_pool={T, Counter}} = Config) -> - {noreply, Config#elixir_code_server{mod_pool={[H | T], Counter}}}; +handle_cast({return_compiler_module, Module, Purgeable}, Config) -> + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + + ModPool = + case Purgeable of + true -> {Used, [Module | Unused], Counter}; + false -> {[Module | Used], Unused, Counter} + end, + + {noreply, Config#elixir_code_server{mod_pool=ModPool}}; handle_cast(Request, Config) -> {stop, {badcast, Request}, Config}. @@ -125,8 +139,8 @@ terminate(_Reason, _Config) -> code_change(_Old, Config, _Extra) -> {ok, Config}. -module_tuple(I) -> - {list_to_atom("elixir_compiler_" ++ integer_to_list(I)), I}. +compiler_module(I) -> + list_to_atom("elixir_compiler_" ++ integer_to_list(I)). defmodule(Pid, Tuple, #elixir_code_server{mod_ets=ModEts} = Config) -> ets:insert(elixir_modules, Tuple), diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 2194889b88..bd6d17d778 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -58,28 +58,21 @@ compile(Forms, Vars, #{line := Line, file := File} = E) -> {Expr, EE, _S} = elixir:quoted_to_erl(Forms, E, S), elixir_env:check_unused_vars(EE), - {Module, I} = retrieve_compiler_module(), + Module = retrieve_compiler_module(), Fun = code_fun(?key(E, module)), Form = code_mod(Fun, Expr, Line, File, Module, Vars), Args = list_to_tuple([V || {_, _, _, V} <- Vars]), {Module, Binary} = elixir_erl_compiler:noenv_forms(Form, File, [nowarn_nomatch]), code:load_binary(Module, "", Binary), + {dispatch(Module, Fun, Args, is_purgeable(Module, Binary)), EE}. - Purgeable = beam_lib:chunks(Binary, [labeled_locals]) == - {ok, {Module, [{labeled_locals, []}]}}, - dispatch(Module, Fun, Args, Purgeable, I, EE). - -dispatch(Module, Fun, Args, Purgeable, I, E) -> +dispatch(Module, Fun, Args, Purgeable) -> Res = Module:Fun(Args), code:delete(Module), - if Purgeable -> - code:purge(Module), - return_compiler_module(I); - true -> - ok - end, - {Res, E}. + Purgeable andalso code:purge(Module), + return_compiler_module(Module, Purgeable), + Res. code_fun(nil) -> '__FILE__'; code_fun(_) -> '__MODULE__'. @@ -102,13 +95,18 @@ code_mod(Fun, Expr, Line, File, Module, Vars) when is_binary(File), is_integer(L retrieve_compiler_module() -> elixir_code_server:call(retrieve_compiler_module). -return_compiler_module(I) -> - elixir_code_server:cast({return_compiler_module, I}). +return_compiler_module(Module, Purgeable) -> + elixir_code_server:cast({return_compiler_module, Module, Purgeable}). + +is_purgeable(Module, Binary) -> + beam_lib:chunks(Binary, [labeled_locals]) == {ok, {Module, [{labeled_locals, []}]}}. allows_fast_compilation({'__block__', _, Exprs}) -> lists:all(fun allows_fast_compilation/1, Exprs); -allows_fast_compilation({defmodule, _, _}) -> true; -allows_fast_compilation(_) -> false. +allows_fast_compilation({defmodule, _, _}) -> + true; +allows_fast_compilation(_) -> + false. %% Bootstrapper diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index d1f70a33c0..c5d5b180a4 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -4,7 +4,6 @@ defmodule CodeTest do use ExUnit.Case, async: true doctest Code - import PathHelpers def genmodule(name) do @@ -32,7 +31,7 @@ defmodule CodeTest do assert {3, _} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__) end - test "can return bindings from a different context" do + test "returns bindings from a different context" do assert Code.eval_string("var!(a, Sample) = 1") == {1, [{{:a, Sample}, 1}]} end @@ -281,4 +280,15 @@ defmodule Code.SyncTest do Code.delete_path(path) refute to_charlist(path) in :code.get_path() end + + test "purges compiler modules" do + quoted = quote(do: Agent.start_link(fn -> :ok end)) + Code.compile_quoted(quoted) + + {:ok, claimed} = Code.purge_compiler_modules() + assert claimed > 0 + + {:ok, claimed} = Code.purge_compiler_modules() + assert claimed == 0 + end end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 6c865b7fb0..86ed2354b0 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -202,6 +202,7 @@ defmodule Mix.Compilers.Elixir do warnings = Enum.map(warnings, &diagnostic(&1, :warning)) ++ warning_diagnostics(sources) {:error, warnings ++ errors} after + Code.purge_compiler_modules() delete_compiler_info() end end