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!