Skip to content

Commit 64a387d

Browse files
authored
Load modules lazily and only if needed (#14453)
1 parent 5e050f2 commit 64a387d

File tree

16 files changed

+258
-152
lines changed

16 files changed

+258
-152
lines changed

lib/elixir/lib/kernel/error_handler.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ defmodule Kernel.ErrorHandler do
4141
:erlang.garbage_collect(self())
4242

4343
receive do
44-
{^ref, value} -> value
44+
{^ref, {:loading, pid}} ->
45+
ref = :erlang.monitor(:process, pid)
46+
47+
receive do
48+
{:DOWN, ^ref, _, _, _} -> :found
49+
end
50+
51+
{^ref, value} ->
52+
value
4553
end
4654
end
4755
end

lib/elixir/lib/kernel/parallel_compiler.ex

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ defmodule Kernel.ParallelCompiler do
198198
{:ok, [atom], [warning] | info()}
199199
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
200200
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
201-
spawn_workers(files, {:compile, path}, options)
201+
spawn_workers(files, {:compile, path}, Keyword.put(options, :dest, path))
202202
end
203203

204204
@doc """
@@ -338,8 +338,11 @@ defmodule Kernel.ParallelCompiler do
338338
end
339339

340340
defp write_module_binaries(result, {:compile, path}, timestamp) do
341+
File.mkdir_p!(path)
342+
Code.prepend_path(path)
343+
341344
Enum.flat_map(result, fn
342-
{{:module, module}, binary} when is_binary(binary) ->
345+
{{:module, module}, {binary, _}} when is_binary(binary) ->
343346
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
344347
File.write!(full_path, binary)
345348
if timestamp, do: File.touch!(full_path, timestamp)
@@ -351,7 +354,7 @@ defmodule Kernel.ParallelCompiler do
351354
end
352355

353356
defp write_module_binaries(result, _output, _timestamp) do
354-
for {{:module, module}, binary} when is_binary(binary) <- result, do: module
357+
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
355358
end
356359

357360
## Verification
@@ -366,7 +369,7 @@ defmodule Kernel.ParallelCompiler do
366369

367370
defp maybe_check_modules(result, runtime_modules, state) do
368371
compiled_modules =
369-
for {{:module, module}, binary} when is_binary(binary) <- result,
372+
for {{:module, module}, {binary, _}} when is_binary(binary) <- result,
370373
do: module
371374

372375
profile(
@@ -439,8 +442,8 @@ defmodule Kernel.ParallelCompiler do
439442

440443
try do
441444
case output do
442-
{:compile, path} -> compile_file(file, path, parent)
443-
:compile -> compile_file(file, dest, parent)
445+
{:compile, _} -> compile_file(file, dest, false, parent)
446+
:compile -> compile_file(file, dest, true, parent)
444447
:require -> require_file(file, parent)
445448
end
446449
catch
@@ -546,9 +549,9 @@ defmodule Kernel.ParallelCompiler do
546549
wait_for_messages([], spawned, waiting, files, result, warnings, errors, state)
547550
end
548551

549-
defp compile_file(file, path, parent) do
552+
defp compile_file(file, path, force_load?, parent) do
550553
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
551-
:erlang.put(:elixir_compiler_dest, path)
554+
:erlang.put(:elixir_compiler_dest, {path, force_load?})
552555
:elixir_compiler.file(file, &each_file(&1, &2, parent))
553556
end
554557

@@ -582,7 +585,7 @@ defmodule Kernel.ParallelCompiler do
582585
end
583586

584587
defp count_modules(result) do
585-
Enum.count(result, &match?({{:module, _}, binary} when is_binary(binary), &1))
588+
Enum.count(result, &match?({{:module, _}, {binary, _}} when is_binary(binary), &1))
586589
end
587590

588591
defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings}
@@ -649,19 +652,37 @@ defmodule Kernel.ParallelCompiler do
649652
state
650653
)
651654

652-
{:module_available, child, ref, file, module, binary} ->
653-
state.each_module.(file, module, binary)
655+
{{:module_loaded, module}, _ref, _type, _pid, _reason} ->
656+
result =
657+
Map.update!(result, {:module, module}, fn {binary, _loader} -> {binary, true} end)
658+
659+
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
654660

655-
# Release the module loader which is waiting for an ack
661+
{:module_available, child, ref, file, module, binary, loaded?} ->
662+
state.each_module.(file, module, binary)
656663
send(child, {ref, :ack})
657-
{available, result} = update_result(result, :module, module, binary)
664+
665+
{available, load_status} =
666+
case Map.get(result, {:module, module}) do
667+
[_ | _] = pids when loaded? ->
668+
{Enum.map(pids, &{&1, :found}), loaded?}
669+
670+
# When compiling files to disk, we only load the module
671+
# if other modules are waiting for it.
672+
[_ | _] = pids ->
673+
pid = load_module(module, binary, state.dest)
674+
{Enum.map(pids, &{&1, {:loading, pid}}), pid}
675+
676+
_ ->
677+
{[], loaded?}
678+
end
658679

659680
spawn_workers(
660681
available ++ queue,
661682
spawned,
662683
waiting,
663684
files,
664-
result,
685+
Map.put(result, {:module, module}, {binary, load_status}),
665686
warnings,
666687
errors,
667688
state
@@ -680,7 +701,9 @@ defmodule Kernel.ParallelCompiler do
680701

681702
{waiting, files, result} =
682703
if not is_list(available_or_pending) or on in defining do
683-
send(child_pid, {ref, :found})
704+
# If what we are waiting on was defined but not loaded, we do it now.
705+
{reply, result} = load_pending(kind, on, result, state)
706+
send(child_pid, {ref, reply})
684707
{waiting, files, result}
685708
else
686709
waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock})
@@ -774,6 +797,55 @@ defmodule Kernel.ParallelCompiler do
774797
{{:error, Enum.reverse(errors, fun.()), info}, state}
775798
end
776799

800+
defp load_pending(kind, module, result, state) do
801+
case result do
802+
%{{:module, ^module} => {binary, load_status}}
803+
when kind in [:module, :struct] and is_binary(binary) ->
804+
case load_status do
805+
true ->
806+
{:found, result}
807+
808+
false ->
809+
pid = load_module(module, binary, state.dest)
810+
result = Map.put(result, {:module, module}, {binary, pid})
811+
{{:loading, pid}, result}
812+
813+
pid when is_pid(pid) ->
814+
{{:loading, pid}, result}
815+
end
816+
817+
_ ->
818+
{:found, result}
819+
end
820+
end
821+
822+
# We load modules in a separate process to avoid blocking
823+
# the parallel compiler. We store the PID of this process and
824+
# all entries monitor it to know once the module is loaded.
825+
defp load_module(module, binary, dest) do
826+
{pid, _ref} =
827+
:erlang.spawn_opt(
828+
fn ->
829+
beam_location =
830+
case dest do
831+
nil ->
832+
[]
833+
834+
dest ->
835+
:filename.join(
836+
:elixir_utils.characters_to_list(dest),
837+
Atom.to_charlist(module) ++ ~c".beam"
838+
)
839+
end
840+
841+
:code.load_binary(module, beam_location, binary)
842+
end,
843+
monitor: [tag: {:module_loaded, module}]
844+
)
845+
846+
pid
847+
end
848+
777849
defp update_result(result, kind, module, value) do
778850
available =
779851
case Map.get(result, {kind, module}) do

0 commit comments

Comments
 (0)