Why do releases use so much more memory than `mix run`?

A question was asked here whether releases use less memory, so checked in a project I’m working on and I was surprised to see almost twice the memory use for the same code. To make sure it wasn’t just my project, I tried with a plain Phoenix app and only the minimal steps to get releases started. This is what I did if you want to reproduce:

mix phx.new hello --no-webpack --no-html --no-ecto
# added {:distillery, "~> 2.0"} to mix.exs
cd hello
mix deps.get
MIX_ENV=prod mix release

Then to compare the memory use I relied on :erlang.memory/0. First using plain mix run in iex.

MIX_ENV=prod iex -S mix run

iex(1)> :erlang.memory 
[
  total: 31407808,
  processes: 7082192,
  processes_used: 7063440,
  system: 24325616,
  atom: 512625,
  atom_used: 504423,
  binary: 97440,
  code: 10298466,
  ets: 929712
]

and then starting the release in the equivalent console

_build/prod/rel/hello/bin/hello console

iex(hello@127.0.0.1)1> :erlang.memory 
[
  total: 55362432,
  processes: 14649704,
  processes_used: 14363384,
  system: 40712728,
  atom: 984241,
  atom_used: 961617,
  binary: 174464,
  code: 21349495,
  ets: 2029176
]

Total reported memory use went from 31MB to 55MB. And you see increases across the board. In my personal project the increase was even more significant, going from 45MB to 90MB.

One thing I guess could be related is that I believe releases preload all code while mix will load modules on demand. But does that really account for the whole difference?

I also want to add a disclaimer: I’m still using releases and I don’t find the memory used to be unreasonable. This is just curiosity!

7 Likes

I think the code loading part will be the major difference - more code means more atoms, more binaries (from literals in those modules) and more ets usage (since information which modules are loaded is held in an ets table by the code_server process). Additionally there might be some more system-level services running like SASL or the release handlers, which could account for the extra process size. Have you tried comparing the outputs of Process.list() on both systems? That could shed some more light on it. Similarly for ets tables you could investigate with :ets.info.

9 Likes

Check to see which applications have been loaded, there may be more in the release. Also check which applications have been started. IIRC correctly the release will start all applications at start up time.

8 Likes

I’ve had similar numbers here, @jola, using your steps.

Following @rvirding suggestion, I’ve ran this code in each:

Application.started_applications() |> Enum.map(&elem(&1, 0)) |> Enum.sort()

# release
# $ _build/prod/rel/hello/bin/hello console
[:artificery, :asn1, :compiler, :cowboy, :cowlib, :crypto, :distillery, :eex,
 :elixir, :gettext, :hello, :iex, :jason, :kernel, :logger, :mime, :mix,
 :phoenix, :phoenix_pubsub, :plug, :plug_cowboy, :plug_crypto, :public_key,
 :ranch, :runtime_tools, :sasl, :ssl, :stdlib]

# mix
# $ MIX_ENV=prod iex -S mix run
[:artificery, :asn1, :compiler, :cowboy, :cowlib, :crypto, :distillery, :eex,
 :elixir, :gettext, :hello, :hex, :iex, :inets, :jason, :kernel, :logger, :mime,
 :mix, :phoenix, :phoenix_pubsub, :plug, :plug_cowboy, :plug_crypto,
 :public_key, :ranch, :runtime_tools, :ssl, :stdlib]
  • release had extra: sasl
  • mix run had extra: hex, inets

I guess sasl is the likely culprit?

UPDATE: sasl start not relevant to memory. I’ve checked also the loaded applications, and it’s the same as started applications. That’s intriguing :+1:t3:

# :erlang.memory in mix option after start
[
  total: 34951112,
  processes: 13154512,
  processes_used: 13153312,
  system: 21796600,
  atom: 512625,
  atom_used: 505434,
  binary: 206016,
  code: 10368934,
  ets: 942080
]

# :erlang.memory in mix option, after starting sasl
[
  total: 35274176,
  processes: 13233528,
  processes_used: 13230984,
  system: 22040648,
  atom: 520817,
  atom_used: 517966,
  binary: 205984,
  code: 10547076,
  ets: 957688
]

# :erlang.memory in release option
[
  total: 60294456,
  processes: 21970552,
  processes_used: 21847048,
  system: 38323904,
  atom: 992433,
  atom_used: 971447,
  binary: 441208,
  code: 21443760,
  ets: 1992328
]
3 Likes

Your release is likely starting in embedded mode while mix run is interactive mode. Embedded mode will load all beam modules on start up instead of only when they are used.

See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy

7 Likes

Indeed, using :code.all_loaded(), I saw the release has 663 modules loaded in it that were not loaded in mix run, while the contrary is 25.

2 Likes

I tried comparing named processes between the app started through mix and the release and came up with this

# iex(4)> release -- mix
[:alarm_handler, :auth, :erl_epmd, :net_kernel, :net_sup, :release_handler,
 :sasl_safe_sup, :sasl_sup]
# iex(5)> mix -- release
[Hex.Registry.Server, Hex.Server, Hex.State, Hex.Supervisor, Hex.UpdateChecker,
 :hex_fetcher, :httpc_handler_sup, :httpc_hex, :httpc_manager,
:httpc_profile_sup, :httpc_sup, :httpd_sup, :inets_sup]

which seems to show the same differences that @rodrigues found comparing the running applications. Put the output and the code I ran here.

I wonder what the memory use would be when running with mix if you force-load all the modules.

1 Like

passing --preload-modules to mix run seems to do the job, actually it loads 133 modules more than release, while the modules below are loaded by release but not loaded in mix run, even with preload:

[:sasl_report, :systools_rc, :alarm_handler, :release_handler_1,
 :format_lib_supp, :misc_supp, :systools_make, :sasl, :sasl_report_tty_h,
 :sasl_report_file_h, :erlsrv, :systools_lib, :release_handler, :systools, :rb,
 :rb_format_supp, :systools_relup]

It does increase memory significantly, but release still has more (gist with output).

6 Likes