RubyKaigi 2017

Busting Performance Bottlenecks: Improving Boot Time by 60%

RubyKaigi2017
http://rubykaigi.org/2017/presentations/jules2689.html

Lengthy application boot times cause developers to quickly lose context and view applications in a negative light, which in turn costs organizations a lot of money and productivity. We found that there were a few areas that impacted boot time: compiling Ruby bytecode, serializing configurations, looking up files and constants, autoloading files, and booting Bundler. This talk focuses on our strategies and solutions which improved our boot time by 60%. Attendees will leave with knowledge of ways to find and mitigate their own startup performance bottlenecks.

RubyKaigi 2017

00:00:00.120 The next speaker is Julian Nadeau. He's coming from Shopify, where he is a production engineer. Our talk today is titled 'Busting Performance Bottlenecks: Improving Boot Time by 60%.' I can understand the struggles of working with lengthy application boot times, as they often cause developers to lose context and view applications negatively.
00:00:16.770 Let me tell you a little about Shopify. Shopify is a Canadian company founded to sell snowboards online back in 2004 by our CEO and Founder, Toby. At the time, the software available for e-commerce was lacking, and it could cost around one hundred thousand dollars just to set up a payment gateway. Therefore, Toby utilized Ruby, particularly Ruby on Rails, starting with version 0.18 to create the Shopify application. Today, Shopify powers over five hundred thousand merchants across the globe, with many merchants situated in Japan, thanks to our recently opened Tokyo office. We now boast a multi-million line code base running on Rails 5.1 with about a thousand engineers at work. This topic is especially dear to us because enhancing boot time has significant implications for our time and money.
00:00:42.000 So, what is application boot time? We're going to start by discussing that. We'll touch on why it matters, how it impacts you, and how it affects your organization. Specifically, we'll talk about how it impacted our Shopify application. Following that, we’ll discuss the areas of impact and how we managed to mitigate those issues, particularly with the Boot Snap gem. Some of you may be familiar with Boot Snap as it’s a gem we've recently released, and I also worked on performance enhancements within Bundler 1.15. Most of this presentation will center around application boot time and, more importantly, the straightforward but powerful techniques we utilized to identify inefficiencies in our own code. My goal is to arm you with insights so you can return and address similar problems in your own codebase. Additionally, I've prepared a website and GitHub repository containing demo information; I encourage you to reference those resources throughout the session or afterward.
00:01:42.000 Let's define application boot time simply as the duration it takes for your application to start after the start command has been issued. For instance, when I run 'bin/rails server' in a Rails application, it may take 8 seconds for the application to fully initialize. Therefore, an 8-second boot time may not seem excessive, but it becomes significant when you consider this is the boot time every time you run tests. Most of us write tests, leading to running numerous commands such as 'bin/rails test' multiple times each day. The total time quickly accumulates.
00:03:04.319 Why does this matter? A developer's experience with your application hinges on how easy it is to work with it. For Shopify, our product involves an e-commerce platform, but the developer experience is also a critical product. I'm part of a production engineering team that runs and maintains the product affecting our developer experience. Hence, we must provide our developers with the same quality experience we deliver to our merchants. Lengthy application boot times can hinder this experience, causing frustration and a negative perception. Negative interactions lead to decreased productivity and trust among developers, which ultimately results in wasted time and diminished resources, impacting our bottom line. At Shopify, we faced application boot times for tests exceeding 25 seconds, alongside server times. With approximately a thousand developers executing countless commands daily, we were potentially losing tens of thousands of dollars each day due to inefficiency.
00:05:02.080 With these challenges in mind, let’s explore the areas of impact we identified within our application before diving into how you can detect these inefficiencies yourself. The main areas include compiling, serialization, and constant lookup. Compiling in Ruby is done dynamically, but as the size of applications grow, more code needs to be compiled, particularly with required statements at the top of each file. This results in increased boot time relative to your application's size. Fortunately, Ruby 2.3 introduced a few instruction sequence methods, which we'll cover later.
00:06:01.270 Serialization is another area where we observed inefficiencies. In our case, we utilized YAML for our test fixtures and configurations, but it caused significant serialization time during the boot process. We addressed this issue by compiling and serializing the contents of our MO files using Ruby's Marshal and MessagePack. I'll elaborate on this process shortly. Another significant bottleneck we observed was in constant lookup. It's vital to note that looking up a constant can become highly expensive, with O(n²) complexity as you traverse the load path for each required item. As your application and load path grow, this inefficiency compounds, affecting overall performance. However, we can alleviate this by caching the first iteration of the lookup to transform O(n²) complexity into O(1) for cache hits or misses, which I will discuss now.
00:08:17.110 Let’s discuss how caching operates within Boot Snap. First, is everyone familiar with Boot Snap? All right, since most of you are acquainted with it, I’ll start by explaining the basic caching protocol we deploy. We conduct stability checks determining if a file or constant is stable or volatile, meaning it may change or not. In essence, we treat Ruby installs and gems as stable since they typically don’t change in the file system. While Ruby core contributors might modify Ruby often, this is less of a consideration for most applications. Once we identify a file as stable, we cache it in a designated folder using Ruby iseq (instruction sequence), which we do similarly with other files, but with an additional cache key. In the stable cache, we retain it indefinitely, while in the volatile cache, we keep it for merely 30 seconds. This strategy allows us to cache the application during boot while ensuring we identify instances where compilation had caused delays.
00:14:21.860 We've noted issues with compilation and as a solution, began compiling bytecode. Since Ruby 1.9, Ruby has translated source into bytecode, improving execution speed dramatically. Following Ruby 2.3’s introduction of the instruction sequence (ISEQ), we effectively bypassed a costly compilation step that recurs every time we load a file. If we continually require a file, it undergoes repeated compilation unless we cache it after the first iteration. Utilizing Boot Snap enables us to cache it in its ISEC form, preventing unnecessary recompilation and significantly speeding up our process. The result? Drastic performance improvements.
00:19:34.500 Another critical area we optimized was the serialization of test fixtures and configuration files. We found that using YAML for loading files is rather slow, even when leveraging fast C extensions. When we compared the performance, we discovered that MessagePack or marshaling provided substantially faster serialization than the YAML approach. Given that YAML is inherently dynamic, it can create overhead due to merging and importing processes. Implementing caching under volatility checks enabled us to enhance boot time effectively. Additionally, we redefined the Kernel.require and load methods to prioritize using the cache first. This way, we eliminated the dual penalty of constant lookups and expensive compilation, streamlining file access and reducing system calls significantly.
00:23:02.700 The Boot Snap library is simple to integrate into your Rails application. Just add it to your Gemfile, ensuring you set `require: false` and include it right after the Bundler setup. Doing this can yield a significant boost in boot time. For instance, Discourse, a popular Rails application, experienced a boot time reduction from 6 seconds to 3 seconds, while one of our smaller internal applications decreased from 3.6 seconds to 1.8 seconds. The core Shopify platform, a large codebase, improved its boot time from approximately 25 seconds to just over 6.5 seconds. The impact of these changes is substantial, and we've received positive feedback from others who witnessed similar gains in their projects, reducing long boot times by up to 60 seconds.
00:26:12.980 Let's shift our focus to Bundler; a tool almost every Ruby developer employs for managing dependencies. Surprisingly, it contributed about a second to our boot time. While not an excessive duration, it mattered when we aimed to reduce our boot time to under 6 seconds. We identified several optimizations released in Bundler 1.15, resulting in a 60% decrease in boot time. Post-implementation, we watched the duration drop to about 300 milliseconds. Upgrading to Bundler 1.15 has proven reliable and useful across Shopify and the wider community. Our success with these updates encourages continual improvement and maintenance efforts to ensure performance remains optimal.
00:31:10.780 Another critical improvement revolved around gem specification validation and how Gemfiles handle dependencies. Since Git gems don’t actually compose native code, they behave like path-based gems. Bundler clones repositories and builds gems from scratch, yet validating gem specifications can be slow. Implementing validation only during the initial load optimizes the process. Moreover, for serializing Gemfiles, we transformed the array of dependencies into a hashed structure, reducing lookup complexity from O(n²) to a more efficient O(1). This shift not only improved efficiency but also slashed the time spent on these operations.
00:36:00.000 In summary, Boot Snap significantly bolstered our performance by implementing effective caching, optimization of constant lookups, and serialization techniques. The enhancements we made to Bundler further optimized our Ruby applications. Encouragingly, in collaboration with the Ruby community, we hope to enhance boot performance within the ecosystem as a whole. I invite you all to explore these methods and share in the open-source community effort, contributing to a faster, more efficient Ruby experience. Thank you for your time, and I'm open to any questions you might have.