Ruby compatibility and experience • Standard Ruby libs, frameworks, work fl ow all are the same • JVM for Ruby • Bring the best of the JVM to the Ruby world • Bring the challenges of Ruby to the JVM
runtime • Much more interesting than former life as JavaEE architect • Sandbox for utilizing new JVM features • Easy excuse to pester all of you for features, performance • Interesting solutions to dif fi cult problems • Immediately applicable to real-world users • Help Ruby scale, grow, gain access to larger enterprises
had • JRuby drove early stages of invokedynamic, method handles • Demands for native FFI, lightweight threads, startup/warmup • Brutal test case • If you're not testing against JRuby you should be • If we're not utilizing your work, show us how
My fi rst contribution • 2006: JRuby runs Rails, team joins Sun Micro • 2008: First JVM bytecode JIT, early production use, fi rst JVMLS • 2015: New IR compiler, interpreter, new JVM JIT • 2024: Leap forward to Java 17 or 21
stuck at 2.7 • #6 JavaScript (Rhino, Nashorn): no recent development, old spec • #8 Visual Basic: Promising efforts late 2000s, none active now • #13 PHP (Quercus): long ago abandoned • #15 Ruby (JRuby): active development, ongoing prod usage • Groovy, Clojure, others: semi-dynamic, active niche use
• Ongoing native JIT effort, reducing C exts, weird parallelism • Rubinius: meta-circular VM from scratch, abandoned >10 years • Truf fl eRuby: Development has slowed, no major production users • JRuby: Silently powering large users in the 1% • Next gen JVM usage, optimizations right around the corner
complete LALR parser • Bison grammar, ported to Jay for JRuby • Recent momentum on hand-written recursive-descent parser • Likely future for us to maintain feature parity
work, 99% accurate • Error-tolerant, returns error AST nodes and attempts to continue • Weak detection of *invalid* syntax • Native library bound with JNI or Panama • Ship major platforms • WASM on Chicory on JVM for others (mostly to bootstrap)
be parsed, compiled, loaded at boot • Work fl ow does not make room for a compile phase • Eventual goal is Ruby as JVM bytecode • But also have reasonable startup time
90% of code is never used • Compile method at call time? Early methods called once • Compile at threshold! Save JIT for valuable targets • Unique challenges similar to JVM itself • Evolving representation of a body of code • Interpreter and bytecode frames on JVM stack
at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.interpretFrameScope(org.jruby.dist/Interpreter.java:177) at org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(org.jruby.dist/Interpreter.java:148) at org.jruby.internal.runtime.methods.InterpretedIRMethod.call(org.jruby.dist/InterpretedIRMethod.java:130) at org.jruby.runtime.callsite.CachingCallSite.call(org.jruby.dist/CachingCallSite.java:193) at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.interpretFrameScope(org.jruby.dist/Interpreter.java:177) at org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(org.jruby.dist/Interpreter.java:148) at org.jruby.internal.runtime.methods.InterpretedIRMethod.call(org.jruby.dist/InterpretedIRMethod.java:130) at org.jruby.runtime.callsite.CachingCallSite.call(org.jruby.dist/CachingCallSite.java:193) at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.INTERPRET_BLOCK(org.jruby.dist/Interpreter.java:123)
Update line, walk JVM trace to fi nd interpreter frames • Splice into modi fi ed stack trace • JIT backtrace encoded in method name • Look for marker on stack, unpack backtrace element • Include relevant Java frames from JRuby
at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.interpretFrameScope(org.jruby.dist/Interpreter.java:177) at org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(org.jruby.dist/Interpreter.java:148) at org.jruby.internal.runtime.methods.InterpretedIRMethod.call(org.jruby.dist/InterpretedIRMethod.java:130) at org.jruby.runtime.callsite.CachingCallSite.call(org.jruby.dist/CachingCallSite.java:193) at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.interpretFrameScope(org.jruby.dist/Interpreter.java:177) at org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(org.jruby.dist/Interpreter.java:148) at org.jruby.internal.runtime.methods.InterpretedIRMethod.call(org.jruby.dist/InterpretedIRMethod.java:130) at org.jruby.runtime.callsite.CachingCallSite.call(org.jruby.dist/CachingCallSite.java:193) at org.jruby.ir.interpreter.InterpreterEngine.processCall(org.jruby.dist/InterpreterEngine.java:350) at org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(org.jruby.dist/StartupInterpreterEngine.java:64) at org.jruby.ir.interpreter.Interpreter.INTERPRET_BLOCK(org.jruby.dist/Interpreter.java:123)
at command line using jruby blah.rb • ❤: Ruby marker (formerly $RUBY$) • def: Method de fi nition marker (formerly $method$ • foo: Method name (encoded for JVMS as needed) • #1: Unique body within compilation unit
at command line using jruby blah.rb • ❤: Ruby marker • {}: Method de fi nition marker • \=\^main\_: Main body marker (encoded for JVMS) • #0: Unique body within compilation unit
java.lang.invoke.LambdaForm$MH/0x00000070015ab000.invoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.Invokers$Holder.linkToCallSite(java.base@21/Invokers$Holder) at blah .️ ❤ def foo #1(blah.rb:2) at java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(java.base@21/DirectMethodHandle$Holder) at java.lang.invoke.LambdaForm$MH/0x00000070015ab000.invoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.Invokers$Holder.linkToCallSite(java.base@21/Invokers$Holder) at blah .️ ❤ {} \=\^main\_ #0(blah.rb:9)
of loads, stores, and invokedynamic • Controlled use of MH chains, keep the graph compact • Default execution uses minimal indy • Startup hit is nontrivial • Really want to be all indy all the time
eRuby optimizes much more aggressively • Method splitting and specialization, deep inlining, PEA • Uses many times more memory and warms up much slower • Support for C extensions cripples optimizer on real-world apps • JRuby balances optimization and usability
enough time • High overhead early on • Easy to trip up and then inlining falls apart • Nightmare for tooling: thread dumps, pro fi le splitting and pollution • Hard to fi gure out why things fail to optimize • Synchronicity with hidden classes or code models?
CRuby • Parser, compiler, interpeters, core: all start cold • Signi fi cant amount of Ruby code loaded at every boot • Interpreter heats up faster than precompiled Ruby • Thousands of classes at boot • Thousands more as methods JIT, indy sites stabilize
Restore In Userspace • Snapshot a process and exit • Clone that and jump back in for new processes • Primarily a Linux technology (work ongoing) • Cumbersome to deal with external resources (open IO, native stuff)
contains encoded characters • Encoding de fi nes structure, manipulation of those characters • Necessitates a custom Regexp implementation • Inef fi cient to roundtrip through String/char[] • java.util.regex falls apart easily (deep alternations over fl ow, etc)
• Designed based on CRuby encoding subsystem • Direct byte[] to byte[] transcoding of most encodings • joni: fl exible bytecoded regex engine • Works directly against byte[] • Pluggable encoding and dialect support
channels self-destruct on interrupt • Only Sockets, Pipes provide selection • Files, fi fos, stdio, process IO must be native in JRuby • Channel abstraction atop native IO via FFI • Can't safely access fi le descriptor from Channel
to my fi rst and only JEP: 191 • Precursor to Panama • Minimal FFI stub, utility libs for binding, common POSIX functions, native IO, unix sockets • Jorn Vernee (Oracle) did a large rework of JNR atop Panama
IO • Better Ruby FFI to help eliminate C extensions • Code generation for us and for Ruby • jextract to generate our JNR wrapper APIs • jextract as a library to generate Ruby FFI compatible with CRuby
support Ruby database wrappers • Java Native Interface (JNI) currently limits throughput • Proof-of concept Panama-based version working now • 2x performance for most operations
• Separate call stacks • Explicit scheduling, direct hand-off • Growing use in CRuby to simulate parallelism • Hide blocking operations behind coroutines
• Blocking IO goes back to scheduler and picks a ready Fiber • Single thread handling 1000s of sessions without IO reactor • Hand-off must be fast for throughput server = TCPServer.new(...) loop { socket = server.accept Fiber.new { handle_request(socket) } } def handle_request(socket) request = socket.read result = do_something_with(data) socket.write(result) end
fi ber • Limited scaling, poor performance characteristics • Extensive contortions to tidy up abandoned fi bers • Java 21+: virtual thread per fi ber • Practically no other changes in JRuby were required • Already avoiding monitors for interruptability
need • Clearly makes many fi bers possible • Hand-off performance isn't that bad • But it solves a bigger problem than we need • We are implementing coroutines on vthreads on coroutines • Ruby folks implementing a scheduler...that we run on a scheduler
dynamic languages • InvokeDynamic works well if we can massage bytecode enough • CDS, CRaC, Leyden will probably make startup time "good enough" • Panama will do wonders for native integration, C ext alternatives • Loom is like 90% solution for fi bers • Every other talk here brings new potential to JRuby
Yard, Red Hat • Today: it's just me • Sponsors on GitHub (https://github.com/sponsors/headius) • Commercial support (https://headius.com/support) • Seeking OSS, research grants for new work
The dream of a multi-language JVM • JRuby has proven it can be done • We want to go on proving it • How do we lure the others back? • What's missing? What stops them?