Reading Settings
16px
blog cover

How to customize YJIT in the Rails app

Software Engineer
Software Engineer
Ruby on Rails
Ruby on Rails
In this post, we'll dive into what YJIT is, how to enable it in your Rails app, and how to monitor and configure it for optimal performance.

What is YJIT?

YJIT is a lightweight Just-In-Time (JIT) compiler introduced in Ruby to speed up method execution by compiling Ruby code into machine code on the fly. This approach can significantly boost performance for Ruby applications, including those built with Rails.

YJIT may not be suitable for certain applications. It currently only supports macOS, Linux and BSD on x86-64 and arm64/aarch64 CPUs. YJIT will use more memory than the Ruby interpreter because the JIT compiler needs to generate machine code in memory and maintain additional state information.
That's why we have to customize it to take advantage of the speed and avoid memory overflow.

Enabling YJIT in Your Rails Application

To leverage YJIT in your Rails app, follow these steps:

1. Install Ruby 3.3.x: Ensure you're running Ruby 3.3.x or later, as YJIT's performance improvements are most effective than 3.2
Make sure you install Rust because Ruby requires it to compile YJIT.

2. Enable YJIT: You can enable YJIT by setting an environment variable or using command-line options:

RUBY_YJIT_ENABLE=1

Or start your Rails server with:
RUBYOPT="--yjit" rails s

Enable YJIT at Runtime: For better boot performance, you can disable YJIT at startup and enable it later in your application:
RubyVM::YJIT.enable
From Rails 7.2 with Ruby 3.3+, YJIT is enabled by default. This approach prevents YJIT from compiling initialization code, which typically doesn't benefit from JIT compilation.

Monitoring YJIT's Performance

To gain insights into YJIT's performance in your Rails app, integrate a custom middleware to log runtime statistics. Here's a sample logger you can use:

class YJITLogger
  def initialize(app)
    @app = app
  end

  def call(env)
    # Sample approximately 5% of requests
    if rand < 0.05 && defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
      stats = RubyVM::YJIT.runtime_stats
      Rails.logger.info "YJIT - code region size: #{stats[:code_region_size]}"
      Rails.logger.info "YJIT - ratio in YJIT: #{stats[:ratio_in_yjit]}"
    end
    @app.call(env)
  end
end

Rails.application.config.middleware.use YJITLogger

This middleware logs key YJIT statistics, such as the size of the code region used by JIT-compiled code and the ratio of instructions executed by YJIT versus the Ruby interpreter.

Configuring YJIT for Better Performance

YJIT comes with several configuration options that can affect its performance. Here are some tips to optimize YJIT for your Rails app:

1. Adjust Executable Memory Size: The default memory size for YJIT is 48 MiB (you can read more in this document), but increasing it can improve performance. For instance, setting it to 64 - 96 MiB might benefit applications with larger codebases:
RUBYOPT="--yjit-exec-mem-size=64"
Use the RubyVM::YJIT.runtime_stats[:code_region_size] metric to ensure your code region size stays below this limit.

2. Monitor Ratio in YJIT: Aim for a high ratio_in_yjit value, ideally above 98%, indicating that most of your code is being executed by YJIT.

3. Use jemalloc for Memory Management: jemalloc is a memory allocator that can improve memory usage patterns and reduce fragmentation in Ruby applications. It's often used in production environments to enhance performance and stability. By using jemalloc, you can help your application run with higher --yjit-exec-mem-size setting

Conclusion

By enabling and configuring YJIT, you can achieve substantial performance improvements in your Rails application. DON'T FORGET to monitor key metrics (response time, memory, ...) and adjust configurations. There is not one fit solution for all projects.


References:
Created at Dec 4, 2024 09:29:15 Coordinated Universal Time

Related blogs