Talks

JRuby 10: Ruby 3.3 on the Modern JVM

RubyKaigi 2024

00:00:01.800 Okay, we'll get started right away. I've got a lot of content today. I'm going to talk about JRuby 10, give you a little bit of introduction, and then discuss where we're going with development. I'm Charles Nutter, but you can call me Charlie. I've been working on JRuby for the past 20 years. Here's some contact information for me if you need to reach me. I mentioned that we're seeking new additional sponsorship for JRuby; I'll talk a little bit more about that later on.
00:00:30.080 First of all, a little introduction to JRuby itself. JRuby, of course, is Ruby running on top of the Java Virtual Machine (JVM). We focus on making it a Ruby implementation first; we want it to feel like Ruby. We want everything that Rubyists are used to to work correctly. However, we gain many benefits from running on top of the JVM ecosystem. Generally, we expect that pure Ruby code should just work, meaning anything that's pure Ruby should function exactly the same in JRuby as it does in standard CRuby. We do have a different extension API: our extensions are written in Java or other JVM languages. While we don't support forking, we do support parallel threads, allowing you to manage all of your concurrency and parallel scaling with just one process. We have thousands of production users all over the world running JRuby; millions or billions of requests are processed daily through JRuby, and we’ve had production users for over 17 years now—the only alternative Ruby to have this kind of deployment.
00:01:44.119 I mentioned the advantages of being on JRuby or being part of the JVM. We get world-class garbage collector support for free and native Just-In-Time (JIT) compilers at no extra cost. Being part of the JVM ecosystem allows Ruby to inline with all of JRuby's native code. All the extensions we write optimize together, and we have access to all of the monitoring and profiling tools available for the JVM, allowing you to profile Ruby code just like you would with any other JVM language. You can deploy anywhere there's a JVM, which basically means anywhere. All platforms have some form of JVM, and JRuby runs smoothly on almost all of them. We also benefit from a huge team of people constantly improving the JVM, so we don’t have to invest a significant amount of effort into that ourselves.
00:02:47.519 Our current release is JRuby 9.4, and we are currently on version 9.4.7, which is our stable release. We will support this version until probably 2026. JRuby 10 is set to be released this year, and we usually provide support for another full calendar year after that. The new version will be compatible with Ruby 3.1, a significant upgrade from JRuby 9.3, which was only compatible with Ruby 2.7. JRuby 10 will support as far back as Java 8, but this will likely be the last release supporting those old versions of Java.
00:03:21.080 There are many installation methods for JRuby; I don’t need to explain all of them since you’ve all used them before—RVM, Ruby Build, and Ruby Install, for example—all have support for JRuby. You can also find specific packages for different operating systems or simply download a tarball and run it. As I mentioned earlier, there are thousands of users worldwide, processing billions of requests through JRuby.
00:03:46.400 Some example users include Camy, a tool used by teachers for giving homework assignments and receiving completed assignments back from students. All of this involves processing PDFs, and millions of those PDFs are generated and processed daily using JRuby. Daytec, based in Norway, builds point-of-sale systems using JRuby on a custom version of Android. For instance, point-of-sale terminals at the Oslo Airport run a JRuby application, which is one of the most intimidating uses of JRuby I know of. Additionally, Google has a business analytics platform called Looker that leverages JRuby. A company called Kinetic Data, based near my hometown in Minneapolis, uses JRuby to build a business portal system based on the Remedy system, allowing users to automate scripting.
00:04:43.400 We are eager to know who is using JRuby. Sometimes we find out only when something breaks, so it would be great to know when it works well for you. Now, let's dive into how JRuby handles compiling Ruby code. We start with Ruby code input, which we compile into our Ruby intermediate representation. We interpret this for a while, similar to the bytecode interpreter in CRuby. Eventually, we identify hot methods and compile those into JVM bytecode. The JVM then continues to run this code, re-optimizing and eventually converting it into native code. By compiling to JVM bytecode in the right way, we achieve a native JIT for Ruby.
00:05:31.280 It’s quite interesting to see the composition of different pieces within Ruby implementations. This is where CRuby stands in terms of native code execution. JRuby, similarly, primarily relies on Java, but we also utilize some C code and various native libraries. We use a lot of standard library code written in Ruby that CRuby utilizes. Because we are on the JVM, the combination of JRuby code and the Java code we write allows all of our extensions to inline and optimize together effectively. For instance, we compared performance between JRuby's implementation of the OJ (the fast JSON library) and its C counterpart. We did a line-by-line port to JRuby, and the performance difference was notable, yielding significant performance boosts.
00:06:06.360 We also perform significantly better when scaling applications because you can run a single JRuby application for your entire site, accommodating tens, hundreds, or even thousands of concurrent users. This capability means we can leverage available resources, using all cores in the system, enabling us to handle more requests per second based on memory usage. The Java platform also provides numerous toolkits for graphics, GUIs, and, of course, games like Minecraft, which is written in Java and runs on the JVM. This leads to the availability of plugins that have been written in Ruby to enhance gameplay.
00:07:11.600 Let’s discuss JRuby 10, which is the exciting part of this talk. This upcoming version is a big leap forward, the biggest since JRuby 9000 several years ago. I want to highlight that this release will feature Ruby 3.3 on the modern JVM. We've decided that since we're set to release later this year, we will also include Ruby 3.4 compatibility, possibly before CRuby releases 3.4 compatibility. We are currently up to date and tracking changes as they are made in the CRuby repository.
00:07:30.760 We are integrating the Prism parser, which will support all language features, and we'll also be performing optimizations—I'll discuss those in detail later. Now is a great opportunity to contribute to this project. For JRuby 10, we considered branding it as JRuby X to capture attention, but that branding has been somewhat tainted, so we're sticking with just JRuby 10.
00:08:12.000 Regarding Ruby 3.4 support, we are on track to be Ruby 3.4 compatible when we release later this year. Currently, the language specs and core specs are about 90% compliant, and we’re working through the CRuby test suite, which comes with additional tests. As I mentioned, when we see pull requests merged into CRuby, we’re implementing the same changes in JRuby. We recently landed changes to support frozen strings throughout the system, and we are tracking everything going into CRuby. Generally, we aim to support the same bundled and default gems; in cases where there are C extensions, we often have a JRuby-specific version or utilize a Pure Ruby version that can perform equally as well.
00:09:11.960 I’ve mentioned we will likely release right after Ruby 3.4. We’ll assess our progress when CRuby runs its preview of version 3.4—if we’re ready, we might release then and allow users to test it out. About the Ruby parser, we are integrating the new Prism parser, which is a state-based parser. We'll also maintain our Java port of the standard Ruby C parser for one more major release cycle, but we believe that the future is with Prism. We are committed to supporting that library to eliminate the need to re-port the parser for every new release.
00:09:49.600 Currently, we will ship native builds of Prism for most major platforms. In case a native library is not shipped with JRuby, we will run Prism as a Wasm build on top of a Wasm runtime within the JVM. This configuration actually works and passes all of the required tests and language specs. While it may not be especially fast, it will be sufficient to facilitate bootstrapping on that platform. You can also activate Prism in JRuby 9.4 if you'd like to try it out. The fact that we've managed to catch up on compatibility over the past few years means we can now focus on numerous optimizations that had been postponed. We've always prioritized compatibility first, working on user issues, assisting users in deploying JRuby into production, and keeping up with Ruby's new features.
00:11:11.120 Now that we’re in a good place with compatibility, we can engage in more interesting optimization projects. For example, several core methods in Ruby need to read state from the caller, such as the underscore underscore method and the block_given? method. These methods require access to the called method name and need to determine if they were invoked with a block. These are standard methods, but retrieving this information from the JVM can be challenging. We handle this by storing the information on the heap, which we clean up afterward, but it incurs optimization costs. Let's explore a benchmark of invoking the underscore underscore method call between CRuby and JRuby 9.4.
00:12:09.840 In this benchmark, CRuby achieves 2.5 million iterations of method calls, while JRuby performs slightly better by default, even with some overhead present. Meanwhile, the JIT in CRuby is functioning quite efficiently. If we analyze what's happening in the actual JRuby bytecode, we see that we load a variable representing the method name onto the stack as it's called, managing frames cleverly to get necessary information. However, pushing and popping this frame for every call incurs substantial overhead.
00:12:56.199 The key to JRuby's optimizations is a JVM feature known as 'invoke dynamic.' This feature allows us to define new instructions for the JVM. When the JVM encounters an invoke dynamic call, it uses a callback to JRuby, where we specify how to optimize that code. Thereafter, the JVM treats it as though it's a standard piece of static Java code, resulting in superior speed. An overwhelming majority of JRuby optimizations leverage invoke dynamic, and if you look at the bytecode generated, you’ll see it almost entirely consists of invoke dynamic calls to our special instructions.
00:13:26.360 By employing our invoke dynamic strategies, we can optimize method calling. Instead of calling the method directly in JRuby, we leverage a special frame name call site that is aware of the frame that has been invoked. As soon as we access the method, we can decide whether to treat it as a regular method call, depending on whether it's the expected method. This strategy enables faster calls without pushing the frame, which reduces overhead significantly. Returning to our benchmark, we now observe that JRuby's performance surpasses CRuby's in a variety of scenarios, confirming the benefits of our optimizations regarding method call efficiency.
00:14:06.480 Another optimization aspect we improved recently is string interpolation. In JRuby, excessive bytecode generation was previously an issue whenever strings contained dynamic and static elements. For simple cases of string interpolation with a few dynamic parts, we ended up generating a considerable amount of bytecode, creating repetitive code for processing the static and dynamic elements separately. This situation would lead to a lot of overhead and unnecessary bytecode usage.
00:14:48.560 However, by utilizing invoke dynamic, we can now optimize string interpolation significantly. Instead of generating much repetitive code with static and dynamic elements, we directly push the dynamic values and create a dynamic string instruction that embeds those static pieces into one instruction. This strategy allows the JVM to optimize the way it constructs the resulting string based on the inserted dynamic and static pieces, thereby resulting in less bytecode for the JVM to manage. With this refinement, we see drastically improved performance for string interpolation when compared to earlier JRuby versions and CRuby.
00:15:45.160 We've identified more optimization opportunities with JRuby 10. There are other variables, such as regex matches and the last line read from I/O, that we can optimize for better access and performance. We can also pass some frequently-read state variables onto the stack, eliminating the need to revert to the heap. In addition, we're making progress with inlining methods and blocks all the way back to the caller, ensuring every version optimizes properly. We're also working on smarter object shapes; JRuby was the first to introduce specialization, where objects with fewer instance variables would occupy less memory.
00:16:16.920 Currently, if new instance variables are introduced at runtime, we have no means of evolving that shape. However, we plan to implement this evolution process in JRuby 10. Additionally, there are many types of calls we do not currently optimize, such as 'super' calls, which involve several steps for method resolution. These too will be inlined and optimized in JRuby, and this will improve performance significantly.
00:16:58.960 We will also focus on utilizing all the advanced features of the modern JVM. I had mentioned earlier that we will set a minimum support for Java 7 or possibly Java 21. One of the features arriving with Java 21 is improved fiber support. Project Loom is an open JDK initiative aiming to introduce true fibers into the JVM, which is essential as JRuby has traditionally had to work with native threads for fiber implementations. Most systems can't handle thousands or tens of thousands of native threads well, but with Java's progression towards virtual threads, JRuby can now upscale effectively.
00:17:38.440 With Loom's introduction of virtual threads that resemble native threads, the changes needed in JRuby are minimal, but the benefits are enormous. We will be able to scale up to hundreds of thousands of fibers without any hassle, which offers significant performance improvements. One area we're investigating is improving the performance of calling native libraries from Ruby via FFI. Project Panama, which has now entered production with Java 22, equips the JVM with a built-in native foreign function interface, which enables efficient calling of native code. Additionally, this project includes a foreign memory API to optimize the management of native memory blocks.
00:18:26.120 Ultimately, our aim is to speed up FFI calls to match the performance of Ruby C extensions. We've recognized that C extensions have been a bottleneck for Ruby performance over the last 20 years. If we can migrate more code into FFI or pure Ruby, it would yield better performance across all Ruby implementations, including CRuby.
00:19:03.440 A classic challenge for JRuby has been its startup and warm-up time. We have noticed that the JRuby compiler pipeline presents a long journey from Ruby code on the left to optimized native code on the right. Each Ruby method must traverse this pipeline to reach full performance. Compounding this, all the Java code has to do the same, which can be quite challenging.
00:19:35.400 There are new projects emerging, including Project Crack—which has a rather unfortunate name—aiming for coordinated restoration and checkpointing. The idea is that you could warm up a JRuby instance, optimize all code, and then save an image of it to facilitate almost immediate restarts. This advancement could make us competitive with CRuby's startup performance. We are also looking into Project Lader, which intends to introduce ahead-of-time compilation to OpenJDK while maintaining JRuby's dynamic features. Some JVM implementations already allow JIT servers to share compiled code across runs, thereby removing the need to recompute everything.
00:20:24.240 For instance, let’s analyze a simple Ruby command executed in different implementations. With CRuby 3.2, we see a very fast startup time of about 53 milliseconds. In contrast, JRuby 9.4, without any specific flags, achieves a slower startup due to the optimizations required—usually several seconds. This startup time corresponds to the top three complaints JRuby receives from new users transitioning from CRuby.
00:21:32.560 To address this, we added a '–-dev' flag that disables most optimizations, resulting in a more rapid startup time. Although speed isn't at the optimum level, it still achieves satisfactory performance and results in a notable difference on local systems. Moreover, Project Crack can significantly speed up the process. Once we get startup times down below one second, we can expect fewer complaints about JRuby.
00:22:10.560 Now, concerning the future of JRuby after version 10: currently, JRuby 9.4 is stable, and you can run anything compatible with Ruby 3.1. We support Rails 7.1; I believe PostgreSQL requires a little extra work, but Rails 7 operates robustly. Our next major release, JRuby 10, is planned for this year. We could begin disseminating preview releases shortly, alongside the JVM enhancements and optimizations we are working on.
00:22:51.120 I'm excited to share a major announcement: JRuby is embarking on a new path. For the past 12 years, we’ve been sponsored by Red Hat, and we sincerely thank them for their invaluable resources. They’ve helped us become an essential part of the Ruby ecosystem, supporting users in production settings. However, we are moving away from Red Hat at this juncture. This decision has nothing to do with the JRuby project; rather, it pertains to Red Hat's resource allocations. I intend to continue working on JRuby, so we are now seeking sponsors or patrons to help sustain this project.
00:23:39.120 Additionally, we will be offering commercial support for JRuby in production environments. We are still working on defining the specifics of this initiative, but there will be a JRuby support company ready to assist you in deploying JRuby in production through targeted bug fixes and optimization. I have several ideas for new optimizations we could implement if we secure the necessary funding. If you're interested in learning more about this, please reach out to us at [email protected]. We aim to roll out these changes within the next month or so, marking a significant step to maintain JRuby as a viable project.
00:24:25.120 Thank you very much for your attention. Please don't hesitate to contact us if you have further questions or need support for JRuby in your production environments or to explore any collaboration opportunities. We are actively seeking to integrate JRuby into the Ruby ecosystem, and with the right funding and partnerships, we can continue to enhance performance and usability across all implementations.