What's new in Phoenix development - February 2018

phoenix development

With the new year, the Phoenix team has been making steady progress towards a 1.4 release with some great new features. There’s still a few milestones yet to hit before release, but in master you’ll find HTTP2 support, faster development compile-times, new JSON encoding, and more. Let’s dive in and take a tour of the progress we’ve made over the last few months.

HTTP2 Support

Phoenix 1.4 will ship with HTTP2 support thanks to the release of Cowboy2 and the work of Phoenix core-team member Gary Rennie with his Plug and Phoenix integration. Phoenix will be released with Cowboy2 as opt-in, to allow more time for its development to settle. Future releases of Phoenix will ship with HTTP2 support by default once all the dust settles. For those that want HTTP2 today, opting into H2 support will be as easy as changing the :cowboy dependency to “~> 2.0” and specifying the handler in your endpoint configuration. HTTP2 brings server push capabilities and reduced latency. Check out Wikipedia’s overview of HTTP2 to learn more.

Faster development compilation

One of Phoenix’s core strengths is its speed, and as we like to remind folks, this goes beyond those microsecond response times you see in the server logs. Production speed is just one part of performance. Fast response times are great, but if your development workflow is tedious or your test suite is painfully slow, the production wins come at a great cost to productivity.

Fortunately Elixir and Phoenix optimize for the entire development process. Whether you’re running tests, developing your application, or serving requests to end-users, your application should be as fast as possible and use as many CPU cores and resources as we can take advantage of.

With this in mind, the Phoenix team is always looking where we can improve performance both in production as well as development. Some users with large applications were seeing increasingly long compile-times. This was tracked down to the Phoenix router causing large compile-time dependencies across the code-base. During development, some users were experiencing large recompiles of all these dependent modules in their application.

A deeper look at the problem

To understand why the Phoenix router causes compile-time dependencies, we have to dive deeper into the way Plug works and showoff a little bit of metaprogramming that’s happening under the hood.

Let’s say you define an AuthenticateUser plug which accepts options on how to lookup the user from the session.

defmodule MyAppWeb.AuthenticateUser do
  def init(opts), do: Enum.into(opts, %{session_key: "user_id"})
  def call(conn, %{session_key: key}) do
    case conn.session[key] do
      ...
    end
  end
end

To optimize the session_key lookup at runtime, we convert the keyword list passed to the plug into a map, as well as assign defaults to the options. By doing this coercion and defaulting in init, plug will perform this work at compile time. This allows us to skip this work at runtime. Every request will then be passed the already coerced options, which is a great way to optimize unnecessary runtime overhead.

The side-effect of this optimization is we must invoke AuthenticateUser.init/1 at compile-time to perform the work. This is where the compile-time dependencies begin to build and cascade. We can see why this happens by looking at the code that is generated underneath our plug call. When you plug a module in your Router, like so:

pipeline :browser do
  ...
  plug MyAppWeb.AuthenticateUser, session_key: "uid"
end

The following code is generated:

case AuthenticateUser.call(conn, %{session_key: "uid"}) do
  %Plug.Conn{halted: true} = conn ->
    nil
    conn
  %Plug.Conn{} = conn ->
    case ... do # further nested plug calls
  _ ->
    raise("expected AuthenticateUser.call/2 to return a Plug.Conn")
end

Notice how our case statement includes the final %{session_key: "uid"} options? This is because we called AuthenticateUser.init/1 at compile-time, and generated the code above to be run at runtime. While this is great for production, we want to avoid the compile-time call in development to gain faster refresh-driven-development times as we’re constantly recompiling the project in development.

Implementing the solution

To get the best of both worlds, the solution is easy. We can generate the compile-time optimized code in production and test environments, while invoking our init calls at runtime in development. This prunes our compile-time dependencies at the cost of small runtime work. For development environments, we’ll never notice the difference since the application is never under load.

To implement the fix, the Phoenix team introduced a new init_mode option to Plug.Builder.compile/3 which configures where the plug’s init/1 is called – :compile for compile-time (default), or :runtime. Phoenix supports this new configuration via the following mix config:

config :phoenix, :plug_init_mode, :runtime

With this in place, our generated AuthenticateUser code would look like this in dev:

case AuthenticateUser.call(conn, AuthenticateUser.init(session_key: "uid")) do
  %Plug.Conn{halted: true} = conn ->
    nil
    conn
  %Plug.Conn{} = conn ->
    case ... do # further nested plug calls
  _ ->
    raise("expected AuthenticateUser.call/2 to return a Plug.Conn")
end

Now every request to our application calls AuthenticateUser.init/1 with our plug options, since this is now a runtime call to coerce the final options. The end result is faster development compilation while maintaining production optimized code.

New Projects use Elixir’s new 1.5+ child_spec

Also coming in the next Phoenix release is the inclusion of the new Elixir 1.5+ streamlined child_spec’s.

Prior to Elixir 1.5, your Phoenix projects’ application.ex had code like this:

# lib/my_app/applicatin.ex
import Supervisor.Spec

children = [
  supervisor(MyApp.Repo, []),
  supervisor(MyApp.Web.Endpoint, []),
  worker(MyApp.Worker, [opts]),
]

Supervisor.start_link(children, strategy: :one_for_one)

New projects will have the following specification:

children = [
  Foo.Repo,
  FooWeb.Endpoint,
  {Foo.Worker, opts},
]

Supervisor.start_link(children, strategy: :one_for_one)

The new Elixir 1.5+ child_spec streamlines how child processes are started and supervised, by pushing the child specification down to the module, rather than relying on the developer to know if they need to start a worker or supervisor. This is great to not only prevent bugs, but to also allow your architecture to start simple and grow as needed. For example, you could start with a single worker process and later grow that worker into an entire supervision tree. Any caller’s using your simple worker in their supervision tree won’t require any code change once you level-up to a supervised tree internally. This is a huge win for maintenance and composability.

Explicit Router helper aliases

We have also removed the imports of the MyAppWeb.Router.Helpers from your web.ex in newly generated applications, instead favoring an explicit alias:

alias MyAppWeb.Router.Helpers, as: Routes

This will change your code in controllers and views to call the router functions off the new alias, so instead of:

redirect(conn, to: article_path(conn, :index))

We are promoting:

redirect(conn, to: Routes.article_path(conn, :index))

This makes it much less confusing for newcomers and seasoned developers alike when coming into a project. Now figuring out where router functions are defined or where to look up documentation in IEx or ExDoc is obvious when you come across the Routes alias. It also prevents confusing circular compiler errors when trying to import the Router helpers into a plug module that also is plugged from the router.

New default JSON encoder with Jason library

The next Phoenix release will also include Jason, the new JSON encoding library, written by Michał Muskała of the Elixir core-team. Jason is the fastest pure Elixir JSON encoder available, even beating c-based encoding libraries under certain scenarios. It is also maintained by an Elixir core-team member which makes it a natural choice for projects looking to get the best out of their applications. New applications will include the following mix configuration to support the Jason library:

config :phoenix, :json_library, Jason

The Phoenix team will be busy fine-tuning these new features ahead of our next release. We’ll see you next month with our latest updates!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box