Hi I'm John Hawthorn.
I'm a Rails committer and I work on the Ruby Architecture team at GitHub.
This talk is about Ruby templates. Here is an example.
We have a file named "_hello.html.erb", with the contents Buenos dias, and then we an ERB tag with with ruby code inside, which will interpolate a name.
We have another file named "home.html.erb". This renders the original file
Most of the techniques in this talk would apply to all Ruby code, but I'm interested in templates.
Among the many improvements in Rails 6.0 were several performance optimizations in ActionView.
We now allocate fewer template objects, templates in development mode should now be as fast as production, and template resolution (which is how we map template names to files) is now much faster.
Another performance improvement we've wanted to be able to make for a long time is being able to load the templates at boot.
So why do we want this...
Unicorn, puma, and similar web servers for MRI Ruby, spawn workers using forking. After fully loading the application they make a fork: a full copy of the process.
This is relatively fast, we only need to load the application once, and usually the workers can share most of the memory from the parent process.
This also means we want to boot the application fully and load as much as possible in the parent process.
With the current system where we lazy load views, we have to load them once for each worker.
We end up repeating work, and worst of all that work is done while a user is waiting for a response.
If we could instead load views in the parent we wouldn't need to repeat work in the worker and we could use less memory.
In general, for production, we always want to load as much as possible in the parent process.
This way we do no extra work, use no extra memory, and avoid wasting time when serving user requests.
So why has it been hard for us to introduce this change...
The big challenge we've always had is when looking at this template, we don't know if "name" is a local variable passed in, or a method.
Unlike a normal ruby method, name is never declared. It's actually pretty unusual to reference a variable which is never declared, you can't write a method that works this way.
We only know what locals are defined when we look at the keys of the hash passed to render.
I want to share two ways I've tried to solve this.
The first is to scan all views, to find all the render calls, and to find what keys the view is being passed for locals.
I was surprised to learn that Rails already does this! Kind of.
We have a feature called "Russian Doll Caching" which invalidates cache keys when a template's content changes. To do this it has a dependency tracker which determines which other templates a view might render.
Unfortunately it does it with this very large regular expression. Regular expressions aren't great for parsing code, it does work well for caching, but for our needs it isn't able to parse out the locals hash, and I think it would be too challenging to do so.
We need a different solution.
When we run Ruby we normally only interact with it as a file, like we would with our text editor, or a process as we would with a console session or running web server.
To run code Ruby actually goes through a series of steps, which are usually hidden from us.
When we run Ruby code we're actually going through a series of steps, which are usually hidden from us.
Most of the time when writing ruby we're only looking at the first and last of these steps. The first is where I open my editor and the last is where I have pry.
We don't want to run this code. We're probably not able to.
This isn't the first time we've had a parser available in Ruby and it's not the only option.
I've tried all of these and they're all good. I really like the Ruby 2.6 parser because it's built-in, and we can expect it to keep up with any new ruby syntaxes.
One problem is that it's only in Ruby 2.6+ and it's marked experimental.
It's likely you already use some sort of static analysis, like RuboCop in your projects.
What we get from the parser is an abstract syntax tree.
So we know we can parse ruby, what about templates?
There are many templating languages and they all compile into ruby. Let's take a look
Ruby templates _are_ ruby.
This is most apparent when looking at a template language like jbuilder
When you use jbuilder inside Rails, the template handler really just takes the template and adds one line of setup, and one line at the end to make sure we return the output.
To "compile" jbuilder, we add a little bit of code around the handler to initialize the json local variable, and to get the output at the end of the template.
ERB templates are ruby code. I think we recognize that from looking at them that they have ruby in them, but when we run them Rails will first turn them into Ruby.
ERB templates are ruby code. I think we recognize that from looking at them that they have ruby in them, but when we run them Rails will first turn them into Ruby.
We can do this manually with ERB.new(src).src, this is essentially what Rails action_view will do internally.
It's not pretty ruby, but it is plain Ruby.
And this is what it would look like as Rails finally renders the template.
It's compiled into a method and this is what Rails will call. The local variable name is assigned here, we need to compile a different method depending on while locals are passed in.
So our plan is to take our template language, compile it into ruby, and then parse it into an AST.
Because we can compile ERB to ruby and then parse that, we should be able to use any static analysis tool with ruby views. The Brakeman security scanner does this already, and there are tools which will let you use rubocop this way.
We're still looking for render calls. Let's try it out...
...
Let's look at just that render call to keep it simple.
We have our source code as a string. We pass it to the parser, and we get this node object out.
This object is a lisp-like definition, so I can be helpful to look at it as a tree.
To parse the render we need to check every aspect of the call.
We check that it's a function, that the function name is render, that it has arguments, and that the first argument is a string.
Then after that we can get the name of the template we're rendering.
Around here I gain more appreciation for the large regular expression solution.
This is kind of like writing a method_missing, but a little more detailed. This code isn't hard, it's just tedious. I find it necessary to write a lot of tests.
So we can parse the basic render call, same as the regex did, now we need to pick up on the keys of the hash.
So we can parse the basic render call, same as the regex did, now we need to pick up on the keys of the hash.
We need to make sure the keys are literals: either strings or symbols. If they're dynamic or use a variable we just have to skip this render. That's fine.
Ruby is a very dynamic language, any time we are doing static analysis we need to accept that it might not work.
This technique is now a gem: actionview_precompiler. If we ran it on the example we'd been using it it does find the render of hello with name.
For the top level "home" template it assumes it gets rendered with no arguments.
That's great for our small example. I wanted to verify this technique on a Real application.
I used exercism.io, a very cool code practice tool.
It's a real application with a few hundred views.
This is the tree of all the views in exercism and how they render each other.
To test this I booted the web server and then measured the very first request it served. This is without precompilation.
This is a flamegraph which shows where the application spent time in the first request. The horizontal x axis is time. I've highlighted where the view compilation occurs. There are 8 views which are compiled and rendered in this request.
The only modification made to exercism.io is to add one initializer, which runs Actionview::Precompiler on boot.
And this is with eager loaded views.
It was able to eager load all but one template render, which was inside a helper.
Profiling isn't benchmarking, so I re-ran the test 100 times with and without precompilation and found again that we are went from an average 100ms to 60ms.
So this worked great. I'm hoping someday soon we can add this to Rails, and we can give every Rails app this benefit.
For today it's a gem actionview_precompiler, it's available on my github.
So we've seen the benefits from parsing. We can analyze code and use those results at runtime.
We can do more by actually rewriting the code we've parsed.
Both of the existing popular parsing gems allow modifying the AST and then have a matching library to allow converting it back to ruby.
Unfortunately the new ruby parser doesn't support converting back to Ruby. Existing tools had ruby2ruby and unparser.
Each node comes with a start and end line and column.
So that's I think the the good solution, from what I learned implementing that I also came up with a strange solution.
I learned a lot about parsing from the previous work and began to wonder if there was another solution.
The fundamental problem seemed to be that we needed to compile assignment of the keys passed in as locals.
It would be nice if there was another way to do this, but we can't do it dynamically. You can try with eval or binding.local_variable_set, it won't work.
The reason we can't do this is that name is compiled differently depending on if there has been an assignment before or not. This is done at parse time.
If we know it's a local it gets compiled to LVAR otherwise it gets compiled to VCALL. I think VCALL means variable call but I'm not sure.
If we can parse this code, maybe we can parse the code and then rewrite it so that it does support dynamically assigning locals.
We can tell these are parsed differently because
I think VCALL means variable-call.
Here's the idea: ...
Every time we assign to a local variable, we can instead write to a locals hash. Every time we read from a local, we can read from a locals hash. No big change here.
Here's the big one, we can change those ambiguous calls or variables accesses to check if the key is in the hash and otherwise call the method.
What this is is a ruby-to-ruby transpiler with dynamic locals
We take our original source code.
Compile the ERB into plain Ruby.
Then we can run dynamic locals translate to convert it to use the locals hash.
And finally we can put it inside our method.
We don't need to know what locals this template is rendered with.
There are a few problems with this...
The defined keyword can be used to check if something is a local or not. If we didn't change it it would no longer work.
We can use the same trick as before, return "local-variable" if it's in our fake locals hash, otherwise we can call the original.
binding has a similar problem. If a user calls binding.pry they probably want their locals to be available.
Any time we see `binding`, we can add our dynamic variables onto it and then start pry on that.
Somehow this actually works, and it passes Rails' test suite 100%.
I don't think that means this is going to production or should ship with rails. It's still very strange and probably has some issues, but I found this a very fun experiment.
These techniques don't have to be about views.
I hope this might make you think of some interesting ways you might use parsing or static analysis to find ways to use Ruby in completely new ways.