Slowness in dev only, seems to be related to Phoenix CodeReloader and Elixir 1.19

Today I upgraded to Elixir 1.19.5 (latest stable) and one thing lead to another, I eventually upgraded most of the dependencies of my app. I got everything working in a couple of hours, but then I noticed a slowness in handling HTTP requests. My controller finishes in a few ms but the browser waits for a couple hundred ms before getting the first byte, and this is on LAN. Further troubleshooting isolate this slowness to:

plug Phoenix.CodeReloader

Does anyone know anything that may have caused this slowness in Phoenix CodeReloader? I noticed that compiling in general for Elixir 1.19 is slower than say, 1,15. Are they related? As a developer, I am willing to spend couple more minutes to compile my app (the type checking and extra warnings are useful) but I am hesitate to spend 200ms for every request served in the dev environment though.

You can try running the server with MIX_DEBUG=1 to get an estimate of where the slowdown is or try one of the Elixir v1.20 release candidates to see if it has been addressed. Compilation has generally gotten faster since v1.15, so it may be something in particular at hand.

1 Like

Please double check that slowness by comparing older and latest Elixir versions (of the same version of application) - it should be the same/similar.
I mean that I experience that slowness too on older versions of Elixir - this is probably because our app has a lot of cycle dependencies and change of one file entails recompilation of another 200 files completely unnecessary.

quick fix for it - set locally code_reloader to false in config/dev
``config :your_app, BetStackWeb.Endpoint,code_reloader: false,```

I will try. Anyway, code was not changed during the slowness so nothing was really recompiled; I guess a null build has gotten slower than before.

Running with MIX_DEBUG=1 shows for every http request:

-> Running mix compile.elixir --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.elixir in 274ms
-> Running mix compile.app --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.app in 1ms

So a null run of mix compile.elixir is slow. By the way, I was under the impression that code reload shall be triggered by inotify, but in my case nothing was changed, but mix compile.elixir is still ran, for every request. Seems excessive?

running the same with elixir1.15.6, I have:

-> Running mix compile.elixir --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.elixir in 7ms
-> Running mix compile.app --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.app in 2ms

So a null build of mix compile.elixir has got a lot slower. I wanted to try 1.20rc but then I have to upgrade my erlang version, which is a lot more complicated.

This performance regression happened between 1.19.0-rc.0 and 1.19.0-rc.1. All packages version pinned down; nothing else was changing. The change log of 1.19.0-rc.1 shows quite a few changes related to mix compile, does anything ring a bell? I am a bit lost on how to debug further though.

My suggestion would be to change Mix.Compilers.Elixir to add some timestamps so you can see the code that slows down. FWIW, I just checked both Tidewave and Livebook and here is what I got:

-> Running mix compile.phoenix_live_view --purge-consolidation-path-if-stale /Users/jose/Work/ts/_build/dev/lib/ts/consolidated --no-all-warnings (inside TS.MixProject)
<- Ran mix compile.phoenix_live_view in 0ms
-> Running mix compile.elixir --purge-consolidation-path-if-stale /Users/jose/Work/ts/_build/dev/lib/ts/consolidated --no-all-warnings (inside TS.MixProject)
<- Ran mix compile.elixir in 7ms
-> Running mix compile.app --purge-consolidation-path-if-stale /Users/jose/Work/ts/_build/dev/lib/ts/consolidated --no-all-warnings (inside TS.MixProject)
<- Ran mix compile.app in 0ms

Also, do you have path dependencies? We fixed recent regressions on the Elixir compiler where some path deps would be marked as stale, leading to additional checks. So don’t forget to try the latest RC.

Yes, I have path dependencies, for both elixir packages and erlang packages. Maybe that’s the culprit? Anyway, I update to OTP28 and Elixir 1.20.0-rc.4, the result seems to be somewhat better:

-> Running mix compile.elixir --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.elixir in 163ms
-> Running mix compile.app --purge-consolidation-path-if-stale /home/derek/projects/roastidious/_build/dev/lib/roastidious/consolidated --no-all-warnings (inside Roastidious.MixProject)
<- Ran mix compile.app in 0ms

I will try to use git dependency instead of path dependency and see what happens.

By the way, got rid of path dependencies and replace them with git dependencies does not help.

See if you can reproduce it on a brand new Phoenix app but same deps in mix.exs. That will tell you if it is an app issue or deps issue.

I kind of sense that the compiler was going through all my source files that still have outstanding warnings. Is what the CodeReloader doing equivalent to run:

 Mix.Task.rerun("compile.elixir", ...)

from the iex prompt? It returns quite a few warnings consistently; most of them are warnings from expanded macros defined in 3rd party libraries called from inside my files. The same can be observed in running mix compile repeatedly. I’d assume I will see warning only once, because subsequent compilation should be noop; however, I am seeing all warnings again and again. Does the compiler rely on mtime only, or does it somehow cache warnings from the previous invocation and revisit those files?

Turns out all previous hypothesis are wrong. It is related to the locking. If I set

MIX_OS_CONCURRENCY_LOCK=0

then everything went back to fast.

PS: It is the fsync. If I just comment out :file.sync call from Mix.Sync.Lock then everything is fast.

BTW, My dev machine is a Linux container in a Linux desktop PC with a rotary disk.

2 Likes

I filed an issue on elixir’s github page with all my findings:

Let’s continue the discussion there.