One of the places where TruffleRuby is still noticeably slower than CRuby is in finding and loading a lot of files. Do Ruby apps really do this?
Let’s take a look at one of our internal Rails applications that we call the Vault,
our knowledge site where we keep documentation and team information, and track project status.
Its dependencies are fairly up to date, running on Rails 7 with a standard architecture.
It has 33,493 lines of code according to
rails stats, so we might call this a medium to large application.
In our Vault app we have
*.rb files in
We also have
gem declarations in the Gemfile
which, including transitive dependencies, gives us a total of
384 external gems.
Eager loading the app results in a
It also has a
which is quite a lot of directories to search
as it looks for each of those six thousand files.
This is because directories are added to the
$LOAD_PATH for each ruby gem used.
If a gem uses
require instead of
every entry in
$LOAD_PATH will be checked for each file.
If you require
M files with a
$LOAD_PATH size of
N, it’s an
O(M * N) operation.
In this case that’s
513 * 6658 which is
There is an old GitHub issue with some great research about speeding up YAML handling in TruffleRuby.
Does a production runtime load that many YAML files? Booting Vault loads a total of 430 YAML files, 423 of which are i18n files from external gems.
Finding and parsing a lot of files can be slow in any environment as this primarily involves system calls and IO operations which require expensive context switching.
Bootsnap was created to improve the speed of these operations for CRuby. Since TruffleRuby is capable of loading Ruby C-extensions could it gain any performance from what Bootsnap does? With a small amount of effort it seems the answer is “yes”.
Bootsnap thus far has only been written to support CRuby. Expanding that to support TruffleRuby was fairly simple.
Bootsnap has a
LoadPathCache which optimizes the finding and loading of Ruby files.
Using TruffleRuby to boot the app under
there are around 7 thousand
open calls for
and 3.2 million
A significant number of these
stat calls result in
because it’s searching for each
.rb file in every directory in
LoadPathCache enabled reduces the number of
stat calls to 2.1 million,
a 35% reduction.
To get this working on TruffleRuby required a few small tweaks, mostly handling
slightly incompatible internals that most application code wouldn’t be concerned with
(like a path prefix on builtin features
or a test expecting enumerator to be written in C when in TruffleRuby it’s
actually written in Ruby).
We also ran into an esoteric issue with super calls resolving incorrectly with singleton classes which has been fixed and is available in TruffleRuby
While native extensions are fairly common in Ruby, there really isn’t an API for extensions. Instead, extension authors link against CRuby’s internals (sometimes linking against functions they really shouldn’t). Since TruffleRuby is an entirely different VM, to run native extensions the VM has to treat a subset of CRuby’s internal functions as if they were an extension API. The last piece of the puzzle was rounding out TruffleRuby’s native extension support by implementing another C function.
Bootsnap also supports compilation caching for Ruby, YAML, and JSON files.
RubyVM constant does not exist under TruffleRuby
which means the
Bootsnap::CompileCache::ISeq can’t work
but we can simply guard against that and only load the code when it’s available.
With a few more
if supported? checks strewn about the codebase
the YAML and JSON compile cache tests were able to pass on TruffleRuby.
With the benchmark in the previously linked GitHub issue about YAML performance
Bootsnap::CompileCache::YAML improved the speed on my machine by 50%.
Eager loading the app with a native build of the latest TruffleRuby commit on a development cloud instance was taking as long as 134 seconds. By simply enabling Bootsnap it finishes in under 122 seconds (consistently about 10% faster). With another small PR to TruffleRuby we can remove several thousand more of those system calls and shave another 2 seconds off the start up time.
The Bootsnap changes for TruffleRuby have been merged and will be included in the next release. We’re excited for this and more improvements to come as we investigate further optimization opportunities.
While Bootsnap was designed for CRuby and is largely implemented as a C extension, thanks to TruffleRuby’s compatibility with native extensions, we were able to extend Bootsnap to TruffleRuby with minimal changes.
Many thanks to Kevin Menard who reviewed this post several times and provided a lot of excellent feedback. I had the opportunity to work with Kevin this summer on a variety of projects to make it easier for applications to adopt TruffleRuby and it was a wonderful experience.
Thanks also to Jean Boussier who reviewed many of my pull requests and encouraged many improvements.