Skip to content

Commit 972a900

Browse files
josevalimv0idpwn
authored andcommitted
Add MIX_OS_DEPS_COMPILE_PARTITION_COUNT for concurrent deps compilation (elixir-lang#14340)
1 parent c32f725 commit 972a900

File tree

10 files changed

+443
-103
lines changed

10 files changed

+443
-103
lines changed

lib/mix/lib/mix.ex

+16-10
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,11 @@ defmodule Mix do
355355
* `MIX_OS_CONCURRENCY_LOCK` - when set to `0` or `false`, disables mix compilation locking.
356356
While not recommended, this may be necessary in cases where hard links or TCP sockets are
357357
not available. When opting for this behaviour, make sure to not start concurrent compilations
358-
of the same project.
358+
of the same project
359+
360+
* `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` - when set to a number greater than 1, it enables
361+
compilation of dependencies over multiple operating system processes. See `mix help deps.compile`
362+
for more information
359363
360364
* `MIX_PATH` - appends extra code paths
361365
@@ -414,7 +418,7 @@ defmodule Mix do
414418
415419
"""
416420

417-
@mix_install_project __MODULE__.InstallProject
421+
@mix_install_project Mix.InstallProject
418422
@mix_install_app :mix_install
419423
@mix_install_app_string Atom.to_string(@mix_install_app)
420424

@@ -900,9 +904,7 @@ defmodule Mix do
900904

901905
case Mix.State.get(:installed) do
902906
nil ->
903-
Application.put_all_env(config, persistent: true)
904907
System.put_env(system_env)
905-
906908
install_project_dir = install_project_dir(id)
907909

908910
if Keyword.fetch!(opts, :verbose) do
@@ -919,10 +921,14 @@ defmodule Mix do
919921
config_path: config_path
920922
]
921923

922-
config = install_project_config(dynamic_config)
924+
:ok =
925+
Mix.ProjectStack.push(
926+
@mix_install_project,
927+
[compile_config: config] ++ install_project_config(dynamic_config),
928+
"nofile"
929+
)
923930

924931
started_apps = Application.started_applications()
925-
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
926932
build_dir = Path.join(install_project_dir, "_build")
927933
external_lockfile = expand_path(opts[:lockfile], deps, :lockfile, "mix.lock")
928934

@@ -939,9 +945,9 @@ defmodule Mix do
939945
File.mkdir_p!(install_project_dir)
940946

941947
File.cd!(install_project_dir, fn ->
942-
if config_path do
943-
Mix.Task.rerun("loadconfig")
944-
end
948+
# This steps need to be mirror in mix deps.partition
949+
Application.put_all_env(config, persistent: true)
950+
Mix.Task.rerun("loadconfig")
945951

946952
cond do
947953
external_lockfile ->
@@ -1074,7 +1080,7 @@ defmodule Mix do
10741080

10751081
defp install_project_config(dynamic_config) do
10761082
[
1077-
version: "0.1.0",
1083+
version: "1.0.0",
10781084
build_embedded: false,
10791085
build_per_environment: true,
10801086
build_path: "_build",

lib/mix/lib/mix/tasks/deps.compile.ex

+85-61
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ defmodule Mix.Tasks.Deps.Compile do
3131
recompiled without propagating those changes upstream. To ensure
3232
`b` is included in the compilation step, pass `--include-children`.
3333
34+
## Compiling dependencies across multiple OS processes
35+
36+
If you set the environment variable `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
37+
to a number greater than 1, Mix will start multiple operating system
38+
processes to compile your dependencies concurrently.
39+
40+
While Mix and Rebar compile all files within a given project in parallel,
41+
enabling this environment variable can still yield useful gains in several
42+
cases, such as when compiling dependencies with native code, dependencies
43+
that must download assets, or dependencies where the compilation time is not
44+
evenly distributed (for example, one file takes much longer to compile than
45+
all others).
46+
47+
While most configuration in Mix is done via command line flags, this particular
48+
environment variable exists because the best number will vary per machine
49+
(and often per project too). The environment variable also makes it more accessible
50+
to enable concurrent compilation in CI and also during `Mix.install/2` commands.
51+
3452
## Command line options
3553
3654
* `--force` - force compilation of deps
@@ -53,7 +71,6 @@ defmodule Mix.Tasks.Deps.Compile do
5371
end
5472

5573
Mix.Project.get!()
56-
5774
config = Mix.Project.config()
5875

5976
Mix.Project.with_build_lock(config, fn ->
@@ -71,86 +88,82 @@ defmodule Mix.Tasks.Deps.Compile do
7188

7289
@doc false
7390
def compile(deps, options \\ []) do
74-
shell = Mix.shell()
75-
config = Mix.Project.deps_config()
7691
Mix.Task.run("deps.precompile")
92+
force? = Keyword.get(options, :force, false)
7793

78-
compiled =
94+
deps =
7995
deps
8096
|> reject_umbrella_children(options)
8197
|> reject_local_deps(options)
82-
|> Enum.map(fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
83-
check_unavailable!(app, scm, status)
84-
maybe_clean(dep, options)
8598

86-
compiled? =
87-
cond do
88-
not is_nil(opts[:compile]) ->
89-
do_compile(dep, config)
99+
count = System.get_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT", "0") |> String.to_integer()
90100

91-
Mix.Dep.mix?(dep) ->
92-
do_mix(dep, config)
101+
compiled? =
102+
if count > 1 and length(deps) > 1 do
103+
Mix.shell().info("mix deps.compile running across #{count} OS processes")
104+
Mix.Tasks.Deps.Partition.server(deps, count, force?)
105+
else
106+
config = Mix.Project.deps_config()
107+
true in Enum.map(deps, &compile_single(&1, force?, config))
108+
end
93109

94-
Mix.Dep.make?(dep) ->
95-
do_make(dep, config)
110+
if compiled?, do: Mix.Task.run("will_recompile"), else: :ok
111+
end
96112

97-
dep.manager == :rebar3 ->
98-
do_rebar3(dep, config)
113+
@doc false
114+
def compile_single(%Mix.Dep{} = dep, force?, config) do
115+
%{app: app, status: status, opts: opts, scm: scm} = dep
116+
check_unavailable!(app, scm, status)
99117

100-
true ->
101-
shell.error(
102-
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
103-
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
104-
)
118+
# If a dependency was marked as fetched or with an out of date lock
119+
# or missing the app file, we always compile it from scratch.
120+
if force? or Mix.Dep.compilable?(dep) do
121+
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
122+
end
105123

106-
false
107-
end
124+
compiled? =
125+
cond do
126+
not is_nil(opts[:compile]) ->
127+
do_compile(dep, config)
108128

109-
if compiled? do
110-
build_path = Mix.Project.build_path(config)
129+
Mix.Dep.mix?(dep) ->
130+
do_mix(dep, config)
111131

112-
lazy_message = fn ->
113-
info = %{
114-
app: dep.app,
115-
scm: dep.scm,
116-
manager: dep.manager,
117-
os_pid: System.pid()
118-
}
132+
Mix.Dep.make?(dep) ->
133+
do_make(dep, config)
119134

120-
{:dep_compiled, info}
121-
end
135+
dep.manager == :rebar3 ->
136+
do_rebar3(dep, config)
122137

123-
Mix.Sync.PubSub.broadcast(build_path, lazy_message)
124-
end
138+
true ->
139+
Mix.shell().error(
140+
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
141+
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
142+
)
125143

126-
# We should touch fetchable dependencies even if they
127-
# did not compile otherwise they will always be marked
128-
# as stale, even when there is nothing to do.
129-
fetchable? = touch_fetchable(scm, opts[:build])
144+
false
145+
end
130146

131-
compiled? and fetchable?
147+
if compiled? do
148+
config
149+
|> Mix.Project.build_path()
150+
|> Mix.Sync.PubSub.broadcast(fn ->
151+
info = %{
152+
app: dep.app,
153+
scm: dep.scm,
154+
manager: dep.manager,
155+
os_pid: System.pid()
156+
}
157+
158+
{:dep_compiled, info}
132159
end)
133-
134-
if true in compiled, do: Mix.Task.run("will_recompile"), else: :ok
135-
end
136-
137-
defp maybe_clean(dep, opts) do
138-
# If a dependency was marked as fetched or with an out of date lock
139-
# or missing the app file, we always compile it from scratch.
140-
if Keyword.get(opts, :force, false) or Mix.Dep.compilable?(dep) do
141-
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
142160
end
143-
end
144161

145-
defp touch_fetchable(scm, path) do
146-
if scm.fetchable?() do
147-
path = Path.join(path, ".mix")
148-
File.mkdir_p!(path)
149-
File.touch!(Path.join(path, "compile.fetch"))
150-
true
151-
else
152-
false
153-
end
162+
# We should touch fetchable dependencies even if they
163+
# did not compile otherwise they will always be marked
164+
# as stale, even when there is nothing to do.
165+
fetchable? = touch_fetchable(scm, opts[:build])
166+
compiled? and fetchable?
154167
end
155168

156169
defp check_unavailable!(app, scm, {:unavailable, path}) do
@@ -172,6 +185,17 @@ defmodule Mix.Tasks.Deps.Compile do
172185
:ok
173186
end
174187

188+
defp touch_fetchable(scm, path) do
189+
if scm.fetchable?() do
190+
path = Path.join(path, ".mix")
191+
File.mkdir_p!(path)
192+
File.touch!(Path.join(path, "compile.fetch"))
193+
true
194+
else
195+
false
196+
end
197+
end
198+
175199
defp do_mix(dep, _config) do
176200
Mix.Dep.in_dependency(dep, fn _ ->
177201
config = Mix.Project.config()

lib/mix/lib/mix/tasks/deps.loadpaths.ex

+1-10
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,7 @@ defmodule Mix.Tasks.Deps.Loadpaths do
134134
defp partition([dep | deps], not_ok, compile) do
135135
cond do
136136
Mix.Dep.compilable?(dep) or (Mix.Dep.ok?(dep) and local?(dep)) ->
137-
if from_umbrella?(dep) do
138-
partition(deps, not_ok, compile)
139-
else
140-
partition(deps, not_ok, [dep | compile])
141-
end
137+
partition(deps, not_ok, [dep | compile])
142138

143139
Mix.Dep.ok?(dep) ->
144140
partition(deps, not_ok, compile)
@@ -158,11 +154,6 @@ defmodule Mix.Tasks.Deps.Loadpaths do
158154
|> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache())
159155
end
160156

161-
# Those are compiled by umbrella.
162-
defp from_umbrella?(dep) do
163-
dep.opts[:from_umbrella]
164-
end
165-
166157
# Every local dependency (i.e. that are not fetchable)
167158
# are automatically recompiled if they are ok.
168159
defp local?(dep) do

0 commit comments

Comments
 (0)