Skip to content

Experiment with loading modules lazily during compilation #14451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 53 additions & 8 deletions lib/elixir/lib/kernel/parallel_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ defmodule Kernel.ParallelCompiler do
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
spawn_workers(files, {:compile, path}, options)
spawn_workers(files, {:compile, path}, Keyword.put(options, :dest, path))
end

@doc """
Expand Down Expand Up @@ -320,6 +320,9 @@ defmodule Kernel.ParallelCompiler do
end

defp write_module_binaries(result, {:compile, path}, timestamp) do
File.mkdir_p!(path)
Code.prepend_path(path)

Enum.flat_map(result, fn
{{:module, module}, binary} when is_binary(binary) ->
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
Expand Down Expand Up @@ -420,8 +423,8 @@ defmodule Kernel.ParallelCompiler do

try do
case output do
{:compile, path} -> compile_file(file, path, parent)
:compile -> compile_file(file, dest, parent)
{:compile, _} -> compile_file(file, dest, false, parent)
:compile -> compile_file(file, dest, true, parent)
:require -> require_file(file, parent)
end
catch
Expand Down Expand Up @@ -527,9 +530,9 @@ defmodule Kernel.ParallelCompiler do
wait_for_messages([], spawned, waiting, files, result, warnings, errors, state)
end

defp compile_file(file, path, parent) do
defp compile_file(file, path, force_load?, parent) do
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
:erlang.put(:elixir_compiler_dest, path)
:erlang.put(:elixir_compiler_dest, {path, force_load?})
:elixir_compiler.file(file, &each_file(&1, &2, parent))
end

Expand Down Expand Up @@ -630,19 +633,35 @@ defmodule Kernel.ParallelCompiler do
state
)

{:module_available, child, ref, file, module, binary} ->
{:module_pending, child, ref, module} ->
pending? = match?(%{{:module, ^module} => [_ | _]}, result)
send(child, {ref, pending?})
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)

{:module_available, child, ref, file, module, binary, loaded?} ->
state.each_module.(file, module, binary)

available =
case Map.get(result, {:module, module}) do
[_ | _] = pids ->
# We prefer to load in the client, if possible,
# to avoid locking the compilation server.
loaded? or load_module(module, binary, state)
Enum.map(pids, &{&1, :found})

_ ->
[]
end

# Release the module loader which is waiting for an ack
send(child, {ref, :ack})
{available, result} = update_result(result, :module, module, binary)

spawn_workers(
available ++ queue,
spawned,
waiting,
files,
result,
Map.put(result, {:module, module}, binary),
warnings,
errors,
state
Expand All @@ -661,6 +680,8 @@ defmodule Kernel.ParallelCompiler do

{waiting, files, result} =
if not is_list(available_or_pending) or on in defining do
# If what we are waiting on was defined but not loaded, we do it now.
load_pending(kind, on, result, state)
send(child_pid, {ref, :found})
{waiting, files, result}
else
Expand Down Expand Up @@ -755,6 +776,30 @@ defmodule Kernel.ParallelCompiler do
{{:error, Enum.reverse(errors, fun.()), info}, state}
end

defp load_pending(kind, module, result, state) do
with true <- kind in [:module, :struct],
%{{:module, ^module} => binary} when is_binary(binary) <- result,
false <- :erlang.module_loaded(module) do
load_module(module, binary, state)
end
end

defp load_module(module, binary, state) do
beam_location =
case state.dest do
nil ->
[]

dest ->
:filename.join(
:elixir_utils.characters_to_list(dest),
Atom.to_charlist(module) ++ ~c".beam"
)
end

:code.load_binary(module, beam_location, binary)
end

defp update_result(result, kind, module, value) do
available =
case Map.get(result, {kind, module}) do
Expand Down
8 changes: 5 additions & 3 deletions lib/elixir/lib/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,11 @@ defmodule Module do
name/arity pairs. Inlining is applied locally, calls from another
module are not affected by this option

* `@compile {:autoload, false}` - disables automatic loading of
modules after compilation. Instead, the module will be loaded after
it is dispatched to
* `@compile {:autoload, true}` - configures if modules are automatically
loaded after definition. It defaults to `false` when compiling modules
to `.beam` files in disk (as the modules are then lazily loaded from
disk). If modules are not compiled to disk, then they are always loaded,
regardless of this flag

* `@compile {:no_warn_undefined, Mod}` or
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
Expand Down
2 changes: 2 additions & 0 deletions lib/elixir/src/elixir_erl_compiler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

spawn(Fun) ->
CompilerInfo = get(elixir_compiler_info),
{error_handler, ErrorHandler} = erlang:process_info(self(), error_handler),

CodeDiagnostics =
case get(elixir_code_diagnostics) of
Expand All @@ -13,6 +14,7 @@ spawn(Fun) ->

{_, Ref} =
spawn_monitor(fun() ->
erlang:process_flag(error_handler, ErrorHandler),
put(elixir_compiler_info, CompilerInfo),
put(elixir_code_diagnostics, CodeDiagnostics),

Expand Down
29 changes: 21 additions & 8 deletions lib/elixir/src/elixir_module.erl
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
put_compiler_modules([Module | CompilerModules]),
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
CheckerInfo = checker_info(),
BeamLocation = beam_location(ModuleAsCharlist),
{BeamLocation, Forceload} = beam_location(ModuleAsCharlist),

{Binary, PersistedAttributes, Autoload} =
elixir_erl_compiler:spawn(fun() ->
Expand Down Expand Up @@ -215,17 +215,18 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->

compile_error_if_tainted(DataSet, E),
Binary = elixir_erl:compile(ModuleMap),
Autoload = proplists:get_value(autoload, CompileOpts, true),
Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false) or
waiting_for_module(Module),
spawn_parallel_checker(CheckerInfo, Module, ModuleMap),
{Binary, PersistedAttributes, Autoload}
end),

Autoload andalso code:load_binary(Module, BeamLocation, Binary),
make_module_available(Module, Binary, Autoload),
put_compiler_modules(CompilerModules),
eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE),
elixir_env:trace({on_module, Binary, none}, ModuleE),
warn_unused_attributes(DataSet, DataBag, PersistedAttributes, E),
make_module_available(Module, Binary),
(element(2, CheckerInfo) == nil) andalso
[VerifyMod:VerifyFun(Module) ||
{VerifyMod, VerifyFun} <- bag_lookup_element(DataBag, {accumulate, after_verify}, 2)],
Expand Down Expand Up @@ -544,10 +545,12 @@ bag_lookup_element(Table, Name, Pos) ->

beam_location(ModuleAsCharlist) ->
case get(elixir_compiler_dest) of
Dest when is_binary(Dest) ->
filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam");
{Dest, ForceLoad} when is_binary(Dest) ->
BeamLocation =
filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"),
{BeamLocation, ForceLoad};
_ ->
""
{"", true}
end.

%% Integration with elixir_compiler that makes the module available
Expand All @@ -568,7 +571,7 @@ spawn_parallel_checker(CheckerInfo, Module, ModuleMap) ->
end,
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log).

make_module_available(Module, Binary) ->
make_module_available(Module, Binary, Loaded) ->
case get(elixir_module_binaries) of
Current when is_list(Current) ->
put(elixir_module_binaries, [{Module, Binary} | Current]);
Expand All @@ -581,10 +584,20 @@ make_module_available(Module, Binary) ->
ok;
{PID, _} ->
Ref = make_ref(),
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary},
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary, Loaded},
receive {Ref, ack} -> ok end
end.

waiting_for_module(Module) ->
case get(elixir_compiler_info) of
undefined ->
false;
{PID, _} ->
Ref = make_ref(),
PID ! {module_pending, self(), Ref, Module},
receive {Ref, Boolean} -> Boolean end
end.

%% Error handling and helpers.

%% We've reached the elixir_module or eval internals, skip it with the rest
Expand Down
21 changes: 11 additions & 10 deletions lib/mix/lib/mix/compilers/elixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,6 @@ defmodule Mix.Compilers.Elixir do

Mix.Utils.compiling_n(length(stale), :ex)
Mix.Project.ensure_structure()

# We don't want to cache this path as we will write to it
true = Code.prepend_path(dest)
previous_opts = set_compiler_opts(opts)

try do
Expand Down Expand Up @@ -738,16 +735,22 @@ defmodule Mix.Compilers.Elixir do

defp remove_and_purge(beam, module) do
_ = File.rm(beam)
:code.purge(module)
:code.delete(module)

if Code.loaded?(module) do
:code.purge(module)
:code.delete(module)
end
end

defp purge_modules_in_path(path) do
with {:ok, beams} <- File.ls(path) do
Enum.each(beams, fn beam ->
module = beam |> Path.rootname() |> String.to_atom()
:code.purge(module)
:code.delete(module)

if Code.loaded?(module) do
:code.purge(module)
:code.delete(module)
end
end)
end
end
Expand Down Expand Up @@ -925,9 +928,7 @@ defmodule Mix.Compilers.Elixir do
end

for {module, _} <- data do
File.rm(beam_path(compile_path, module))
:code.purge(module)
:code.delete(module)
remove_and_purge(beam_path(compile_path, module), module)
end
rescue
_ ->
Expand Down
Loading