Ruby YJIT at Underdog
Tags: ruby, rails, performance
Published: 2023-04-22 18:29:17

Here at Underdog, many of our projects are Rails services. So when [Ruby 3.2.0](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/) was released, we were eager to put it through its paces and see how much performance could be gained with this latest version! In this post, we'll discuss some critical changes in the release and how they affect performance. 

Per tradition, [Ruby 3.2.0](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/) was released this past Christmas. By all accounts, this is a step forward for the language, and with it came the following optimizations:

- [Variable width memory allocation](https://shopify.engineering/ruby-variable-width-allocation)
- Object shapes
- [Production-ready YJIT compilation](https://shopify.engineering/yjit-just-in-time-compiler-cruby)

Shopify has taken a leadership role in pushing Ruby forward — this release is the proof. They were running YJIT in production days before the release, and given that level of rigor, we were comfortable pushing 3.2.0 to our own production environment after some brief testing in our staging environment.

## How We Upgraded

We used Ruby’s official Debian-based images for our application containers when we decided we wanted to enable YJIT. Unfortunately, [YJIT support was not available in those images at the time](https://github.com/docker-library/ruby/pull/400#issuecomment-1369740381), so we opted to switch our container images to be based on Ruby’s Alpine-based images instead. We were using the Debian-based images for stability's sake. However, we had already run into issues with other out-of-date packages due to Debian's general philosophy on this topic, so switching to Alpine gave us additional benefits.

The workload here was small. All we had to do was change our Dockerfile, update some system-level dependency names between the two distros, and then add the `RUBY_YJIT_ENABLE` environment variable to our application containers’ Kubernetes manifests. Once we flipped that value to `1` via deployment, we were able to exec into a container and confirm YJIT was enabled with `ruby —-version`.

## Our Results

We had a few third-party dependencies we needed to update to support Ruby 3.2.0, but after those changes, things went smoothly. We had no issues or surprises over the weekend, and overall we saw roughly a 10% drop in response time:

![graph of comparisons of response times before and after YJIT upgrade](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pe02jqjc7gx9bcmtr7wf.png)

Our p50s saw about a 9% improvement, which grew to 12.5% at p98. We found this intriguing and consistent with how we expected this update to affect Rails. Our quickest APIs spend most of their time with database IO so there’s less “Ruby time” to optimize, whereas our worst-performing APIs are often doing more logic in Ruby or serializing larger objects. We expected to see the most significant improvement in these more Ruby-intensive requests, and backed this up by cherry picking an API we knew was Ruby intensive. 

Below is our Drafts API that serializes draft entries and picks. It is more data than we should be serializing, and running a little more logic than it should be, but it’s been below our SLOs so we have yet to optimize.


Before             |  After
:-------------------------:|:-------------------------:
![draft endpoint response times before](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3l9q6pzj56hsyrhsfil.png)  |  ![draft endpoint response times after](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kur9ufcig97atekixpf.png)

This improvement is staggering. In this comparison, you can see that the improvement was greatest for the worst-performing scenarios — which is amazing!

At Underdog, we also process stats and grade contests with background jobs via Sidekiq. We were curious if our job execution times improved as well… and absolutely, it did!


![graph of comparisons of execution times for sidekiq jobs before and after YJIT upgrade](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhjyuhsphb2fkqz90ez1.png)

The p90 looks a little strange, but other than that, we saw a significant improvement. This makes sense — we expected enhancements here as well because it’s common to have Ruby-heavy processes turned into background jobs.

## Conclusions

It is so important to keep projects up-to-date to take advantage of the newest releases, especially considering the free performance and how easy it was for Underdog to upgrade to 3.2.0. For some time, Ruby has been promising performance upgrades with mixed results through the lens of a Rails deployment, but they took a huge step forward with 3.2.0. The Ruby Core team hit it out of the park, and we’re lucky to have such a strong team behind the language.

**Want to learn more about our team here at Underdog? Check out our [culture and careers page here](https://underdogfantasy.com/careers?utm_source=DevTo&utm_medium=blog+&utm_campaign=UDLife) and see how we are changing the game!**