Skip to content

Reduce memory usage in IEx autocomplete module listing#15140

Merged
josevalim merged 2 commits into
elixir-lang:mainfrom
dkuku:optimize-iex-autocomplete-memory
Mar 6, 2026
Merged

Reduce memory usage in IEx autocomplete module listing#15140
josevalim merged 2 commits into
elixir-lang:mainfrom
dkuku:optimize-iex-autocomplete-memory

Conversation

@dkuku

@dkuku dkuku commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Reduce memory usage in IEx autocomplete module listing. On memory-capped pods, tab completion could trigger OOM kills (exit 137) due to unnecessary allocations.

Two changes:

  • Use :erlang.loaded/0 instead of :code.all_loaded/0 and :lists.usort/1 instead of Enum.sort/1 |> Enum.dedup/1. We only need module names for autocomplete, not file paths. :code.all_loaded/0 copies the full beam file path for every loaded module, which is pure waste. :lists.usort/1 combines sort + dedup in a single native pass.

  • Use binary_part + :binary.match instead of String.split + Enum.at in match_elixir_modules. Avoids allocating intermediate lists of string fragments for every matching module.

Measurements on a production pod (~200 Elixir modules)

Memory per tab completion
Before 15,996 KB
After 1,089 KB

14.7x reduction — from 15.6 MB to 1.1 MB per autocomplete call.

Test plan

  • make test_iex — 279 tests, 0 failures
  • Verified correctness: old and new produce identical results
  • Measured on production pod with ~200 loaded Elixir modules

🤖 Generated with Claude Code

dkuku and others added 2 commits March 6, 2026 13:31
Use :erlang.loaded/0 instead of :code.all_loaded/0 since we only need
module names, not file paths. The latter copies the full path for every
loaded module, which on production systems with many dependencies
caused significant memory spikes (measured 2.8MB -> 172KB on a real pod).

Also replace Enum.sort/1 |> Enum.dedup/1 with :lists.usort/1 to
eliminate one intermediate list allocation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace String.split/Enum.at/length with binary_part and :binary.match
to extract submodule names. This avoids allocating intermediate lists
of string fragments for every matching module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sabiwara

sabiwara commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

NIce!
I noticed a difference between the two, don't sure if it is an issue or not:

iex> (:erlang.loaded()  |> :lists.usort()) -- (:code.all_loaded() |> Enum.map(&elem(&1, 0)) |> :lists.usort())
[:elixir_compiler_0, :elixir_compiler_1, :elixir_compiler_2, :elixir_compiler_3]

@dkuku

dkuku commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

I only checked it like that:

iex(add_ons_all@add-ons-5f6694dff9-2sz5f)17> :erlang.loaded() |> length()
6538
iex(add_ons_all@add-ons-5f6694dff9-2sz5f)18> :code.all_loaded() |> length()
6538

But still on the app I'm testing I'm getting an empty list. This is some older elixir version and a release running on kubernetes where this bothers me the most :D


 (:erlang.loaded()  |> :lists.usort()) -- (:code.all_loaded() |> Enum.map(&elem(&1, 0)) |> :lists.usort())
[]

@josevalim

Copy link
Copy Markdown
Member

I think the compiler modules will be fine because are likely not looking up under :elixir_. If it is a problem we can filter them out later!

@josevalim josevalim merged commit 7d3863f into elixir-lang:main Mar 6, 2026
15 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

parts = String.split(mod, "."),
depth <= length(parts),
name = Enum.at(parts, depth - 1),
rest = binary_part(mod, prefix_size, byte_size(mod) - prefix_size),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, there may be a bug here. is it guaranteeed that byte_size(mod) - prefix_size is greater than 0?

@sabiwara

sabiwara commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

I think the compiler modules will be fine because are likely not looking up under :elixir_. If it is a problem we can filter them out later!

Yes I totally agree, my point was more about the fact these are not returning exactly the same thing, so I wondered if there was a risk of having a wonky behavior.

But worst case scenario, we can always check for :erlang.module_loaded to avoid old code.

@josevalim

Copy link
Copy Markdown
Member

Oh, I see and good call! We can use module_loaded in the future if we run into something, yeah!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants