We have a production app that’s on Phoenix 1.2.4 and Elixir 1.3.4 and we have been doing a lot of new development and have noticed a large slowdown in the time it takes to recompile the phoenix parts of the application (controllers, views.)
Using this blog post as a resource: http://milhouseonsoftware.com/2016/08/11/understanding-elixir-recompilation/ we believe we’ve tracked down the culprit to be the Phoenix Router and specifically importing the Router.Helpers in the Web.controller/0 and Web.view/0 that get use
d by controller and view files. The net effect is, when adding a route or changing a plug, or changing code referenced by a plug, this causes the Router to recompile, which causes the ripple through views and controllers.
What options, or what can should we look at, to clean up these relationships?
Thanks!
Very cool insight you have already. I particularly (sometimes) think that Myapp.Router.Helpers
generation is a overuse of elixir metaprogramming, and this slowness in compilation is one of the symptoms.
My suggestion is another module to solve it with simple functions.
# instead of
MyApp.Router.Helpers.user_path(conn, :update, user_id)
# something like
Phoenix.Router.Helpers.path(Myapp.Router, conn, :user, :update, user_id)
While I agree that the former feels “cleaner”, I can’t see a huge difference. Maybe performance, but I don’t think this is a huge factor for path string generation, is it?
Now about what you can do: implement a module that does what I’ve suggested for you! You can see how phoenix
do mount their route helper methods here. And if you make it, please open source it…
I would do it if this was a big problem for me, but I didn’t suffer with compilation slowness, I guess it’s because I have few controllers and views for now.
Uh oh, sorry. Digging a little bit more, I found out that for building the helper methods, pheonix uses a compile time only available module attribute.
This attribute is where they save the route definitions, and there’s no way to access it in runtime. Sorry…
The router itself changes rarely, so it’s not a big issue. What is a problem are plugs that router calls? Since init/1
is called at compile-time this means a compile-time dependency on all the plugs. And if they use structs from code - it’s an easy recipe for huge recompiles on every small change.
Maybe router’s pipeline should be somehow isolated from the routes? I think this should solve most problems.
Hmm, here’s some code that does suggest the plugs being related.
We use a LayoutPlug
in the Router
pipelines to set a site-specific layout template based on some conn/config.
Here’s an example excerpt from our router:
pipeline :general_store do
plug :browser
plug Store.LayoutPlug
end
scope "/my-page", Store do
pipe_through :general_store
get "/", ContentController, :page
end
A simplified LayoutPlug looks like:
def call(conn, _opts) do
layout = determine_layout(conn)
conn
|> Phoenix.Controller.put_layout({Store.LayoutView, layout})
end
Because Store.LayoutView
is referenced in the plug, any changes I make to the app.html.ex
template requires the plug -> router recompilation.
If I comment out the plug Store.LayoutPlug
and move the put_layout
into the ContentController.page/2
action, the recompilation behavior goes away. Unfortunately, it’s important for us to plug the router, since all downstream controllers depend on this.
I’m not sure how to separate the pipelines from routes as you suggest.
Oh, I meant to do this in the Phoenix itself.
1 Like
So, plug
within the router is susceptible to the recompilation dependencies biting us, while plug
within a controller does not have these issues-- I thought that since controllers are essentially plugs with some syntactic sugar, it must be that Phoenix is doing something special here?
Trying to determine what our options are-- thank you so much for all your help (and for Ecto!)