Dependencies on Router and Router.Helpers causing slow recompilation in Phoenix app

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 used 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!)