Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JRuby: The Road to Ruby 2.6 and Rails 6

headius
April 20, 2019

JRuby: The Road to Ruby 2.6 and Rails 6

Over the past six months, the JRuby team has been working on filling in edges: getting refinements working well, porting C extensions, and getting well-known applications running. At the same time, we always try to push forward on performance and general compatibility. In this talk, we'll cover recent work to support Rails 6 and Ruby 2.6. We'll show off a major Rails application running on JRuby. And we'll show you how you can help us keep JRuby moving forward.

headius

April 20, 2019
Tweet

More Decks by headius

Other Decks in Programming

Transcript

  1. JRuby: The Road to Ruby and Rails 6 Charles Oliver

    Nutter (@headius) Thomas Enebo (@tom_enebo)
  2. #FreeOlaBini • Ola Bini, former JRuby contributor, is being held

    without cause by Ecuadorian authorities in connection with Julian Assange • His detention is illegal and should worry all of us • Please tell your friends about Ola and support efforts to help force the Ecuadorian authorities to release him
  3. What is JRuby • It's just Ruby! • Ruby 2.5

    compatible, if something's broken tell us • Supports pure-Ruby gems, many extensions • We want to be a Ruby first! • It's a JVM language • Full access to the power of the JVM platform!
  4. Roadmap • 9.2.7.0 last week, 9.2.8.0 in May • 9.1.x

    is EOL • How to handle 2.6? 9.1.17.0 ... 9.2.0.0 2.5.3+ 2.3.x 2.6? 9.2.7.0 master jruby-9_1 9.1.1.18.0 EOL ruby-2.6
  5. Ruby 2.6? • Should we wait for 2.7? • How

    important is 2.6.x • We went from 2.3.x -> 2.5.x • Less maintenance for us
  6. Refinements! • Partially implemented in 9.2.0.0 • Refinements in modules,

    prepends, with super: broken • Different Method lookup logic in JRuby • Nearly 100% in 9.2.7.0! • Rework of method lookup and super calls (fixed super bugs too!) • 93 tests, 470 assertions, 4 failures, 2 errors, 0 skips
  7. Load and Require • JRuby's load/require logic was from Ruby

    1.8 times • Path expansion differences • No $LOADED_FEATURES cache • Subtle bugs in autoload (breaking Zeitwerk) • Rework is in progress hopefully for 9.2.8
  8. Enumerators and Fibers • More and more use in Ruby

    apps • See async, falcon for an extreme case • No fibers on JVM, have to use native threads • JRuby 9.2.8 will merge logic, fix Enumerator#next bugs • OpenJDK's Project Loom: native fibers for JVM! • We'll start supporting this after RailsConf
  9. Developer Experience • Startup time is critical to the Ruby

    experience • Fast iteration on command line tools • Automatic reload or quick reboot of apps • Primary concern when switching to away from CRuby • CRuby is super fast to start up!
  10. Why Is This Hard? • CRuby: Mostly native code at

    startup • Parser, iseq compiler, iseq interpreter, core classes, extensions • JRuby: Mostly interpreted bytecode at startup • Everything in JRuby starts "cold" • Repeated JRuby startup in same JVM gets much faster
  11. total execution time (lower is better) 0 0.45 0.9 1.35

    1.8 -e 1 CRuby JRuby (JDK8) JRuby (10th iter)
  12. total execution time (lower is better) 0 1.25 2.5 3.75

    5 gem --version gem list (~350 gems) CRuby JRuby (JDK8) JRuby (10th iter)
  13. Running in Same JVM total execution time (lower is better)

    0 1.25 2.5 3.75 5 gem list (~350 gems)
  14. Disabling JRuby JIT total execution time (lower is better) 0

    1.15 2.3 3.45 4.6 gem list (~350 gems) JRuby JRuby --dev
  15. Java 11 total execution time (lower is better) 0 1.75

    3.5 5.25 7 gem list (~350 gems) JRuby --dev --dev + CDS
  16. OpenJ9 JDK 8 total execution time (lower is better) 0

    2 4 6 8 gem list (~350 gems) JRuby --dev --dev + quick
  17. Best Times Compared total execution time (lower is better) 0

    1 2 3 4 gem list (~350 gems) CRuby JRuby --dev JDK8 JRuby --dev JDK11 CDS JRuby --dev J9 quick
  18. Ahead-of-time Compile? • Recent interest in precompiling JVM apps to

    native • TruffleRuby uses GraalVM's Substrate VM • "The SVM version of TruffleRuby has better startup performance and lower memory footprint than JRuby, Rubinius and TruffleRuby on the JVM, and similar or better startup performance than MRI." • Precompiled TruffleRuby core + Java dependencies • Ruby and C extensions must be optimized from scratch
  19. total execution time (lower is better) 0 0.75 1.5 2.25

    3 -v -e 1 JRuby --dev TruffleRuby SVM TruffleRuby JVM
  20. total execution time (lower is better) 0 5.5 11 16.5

    22 gem --version gem list (~350 gems) JRuby --dev TruffleRuby SVM TruffleRuby JVM
  21. Startup Future • Continue reducing complexity at boot • Future

    improvements to JVM JIT caching, class sharing, etc • Precompiling Ruby code to JVM • JRuby on SVM: fully precompiled JRuby+app • Fully-native precompiled Ruby apps! • Lots of work to do!
  22. Why JRuby on Rails? • "Are there JRuby users running

    Rails applications?" • Oh yes! And at large scale! • JRuby has run Rails since 2006 • We believe it is the best way to scale large Rails apps today • Excellent GC • Fully parallel threading
  23. Scaling Rails • Classic problem on MRI • No concurrent

    threads, so we need processes • Processes duplicate runtime state and waste resources • JRuby is the answer! • Multi-threaded single process runs your entire site
  24. Rails Performance • Simple scaffolded "blog" application • EC2 t2.xlarge,

    Ubuntu 18.04 • CRuby 2.6.2, JRuby 9.2.6, TruffleRuby RC15 • wrk driver with keepalive disabled due to Puma bug
  25. Optimizing for Rails • Most important metric for Ruby performance

    • Very difficult framework to optimize • See also k0kubun's JIT talk • JRuby typically ran a bit slower than CRuby • Until recently!
  26. Caveats • Keep-alive improves numbers • This bench is pretty

    small, so it shows up • JRuby takes about 60s of driving to fully warm up • TruffleRuby numbers are extremely erratic and never settle down • Idle server was using 2GB, 150% CPU
  27. Resident Memory • CRuby • 4 processes: ~400MB • 10

    processes: ~1GB • JRuby • Default settings: ~1.3GB • With 250MB heap cap: ~580MB (no significant perf hit)
  28. rubygems.org benchmark • Production mode • force_https = false (localhost

    testing) • Use puma (JRuby does not support unicorn) • Same machine: • rails, postgresql, elasticsearch, toxiproxy, redis, memcached • client software: wrk, ab
  29. rubygems.org benchmark • http://localhost:9292/profiles/enebo • Gem push is not working?

    • Benchmark mystery! • wrk never would show improvements with multiple threads • ab keep alive problems (due to bug #1565 in Puma)
  30. Requests/second 0 75 150 225 300 Iteration (30s) 1 2

    3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 JRuby puma single 2.6.2 puma clustered 2.6.2 puma single wrk -t 1 -d 30s http://localhost:9292/profiles/enebo Supposedly Keep-Alive???
  31. Requests/second 0 100 200 300 400 Iteration (30s) 1 2

    3 4 5 6 7 8 9 10 11 12 13 JRuby puma single 2.6.2 puma clustered 2.6.2 puma single wrk -H “Connection: Close” -t 1 -d 30s http://localhost:9292/profiles/enebo Disable Keep-Alive
  32. Requests/second 0 100 200 300 400 Concurrent threads 1 2

    4 6 8 10 12 14 JRuby puma single 2.6.2 puma clustered 2.6.2 puma single ab -c {n} -t 30 http://localhost:9292/profiles/enebo
  33. Use-Case: Discourse • Big well-known Rails application • “A platform

    for community discussion” • >500 gems • 250,000 lines of Ruby • JRuby is not currently supported
  34. JRuby-lint See how ready your Ruby code is to run

    on JRuby % gem install jruby-lint % cd my-app % jruby-lint STEP 1
  35. Strategy: Replace with Pure Ruby def ruby_xor!(x, y) i =

    0 max = (x.length < y.length ? x.length : y.length) while i < max x.setbyte(i, x.getbyte(i) ^ y.getbyte(i)) i += 1 end end ruby 3.702M (± 5.8%) i/s - 18.431M in 4.995973s xorcist 11.015M (± 8.1%) i/s - 54.710M in 5.007484s Unless the performance matters…
  36. Strategy: FFI - Foreign Function Interface • Bind to DLL/shared

    library • Call functions & interact with structs/pointers • Pure-Ruby syntax! • Portable across all Ruby implementations
  37. https://github.com/chuckremes/ffi-rzmq-core/blob/master/lib/ffi-rzmq-core/libzmq.rb module LibZMQ extend FFI::Library # ... ffi_lib(ZMQ_LIB_PATHS + %w{libzmq})

    attach_function :zmq_strerror, [:int], :pointer, :blocking => true # ... end Load FFI methods Load specific DLL Attach function Nam e of function Input param eters Return value puts "Error: #{LibZMQ.zmq_strerror(errno)}" Call it just like Ruby!
  38. Strategy: Script Java Library • JRuby allows call Java classes

    with Ruby syntax • Zillions of existing Java libraries • Usually minimal glue code
  39. mini_racer port • mini_racer is minimal bindings to V8 •

    JRuby will script Java library J2V8 • Native bindings to V8 from Java • Someone wrote the C so we don’t have to!
  40. mini_racer porting snippets require 'mini_racer/jruby/j2v8_linux_x86_64-4.8.0.jar' java_import com.eclipsesource.v8.V8 @v8 ||= V8::createV8Runtime

    JSToRuby(@v8.execute_script(src, file, 0)) Load Java Library Make Java V8 class accessible Call method on that class Tough to tell we are calling Java here
  41. Strategy: Native Java Extension • Write the extension in Java

    • Using our extension APIs • Generally fastest
  42. Porting oj • Popular JSON gem • 19810 lines of

    C • 7 parsers/dumpers (object, strict, compat, null, custom, rails, wab) https://github.com/ohler55/oj
  43. JRuby APIs @JRubyModule(name = "Oj") public class RubyOj extends RubyModule

    { // ... @JRubyMethod(module = true, required = 1, rest = true) public static IRubyObject strict_load(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) { OjLibrary oj = resolveOj(self); Options options = oj.default_options.dup(context); ParserSource source = processArgs(context, args, options); return new StrictParse(source, context, options).parse(oj, true, block); } } Define Oj module Define Oj.strict_load
  44. Porting Gems Conclusion • Pain when replacement does not already

    exist • Many strategies to move past the pain • Once someone does it we all get it • Yay for Open Source
  45. Rails 5.2.3 actioncable: some results…hard pg dep for other fails

    actionmailer: 204 runs, 457 assertions, 0 failures, 0 errors actionpack: 3174 runs, 15884 assertions, 1 failures, 0 errors actionview: 1993 runs, 4398 assertions, 2 failures, 4 errors activejob: 180 runs, 415 assertions, 0 failures, 0 errors activemodel: 810 runs, 2265 assertions, 0 failures, 0 errors activestorage: 144 runs, 372 assertions, 0 failures, 0 errors activesupport: 4212 runs, 763229 assertions, 10 failures, 1 errors railties: uses fork() [issue #35900] 99.998% passing!
  46. ActiveRecordJDBC 52.2 • Historic dates • PG-specific tests in AR

    test suite (PRs needed yet) • Miscellaneous… sqlite3: 19 excludes, 5327 runs, 14961 assertions postgresql: 53 excludes, 5915 runs, 16626 assertions mysql2: 33 excludes, 5491 runs, 15527 assertions
  47. Rails 6.0.0.beta3 actioncable: 203 runs, 921 assertions, 0 failures, 10

    errors actionmailbox: 79 runs, 205 assertions, 0 failures, 2 errors actionmailer: 219 runs, 503 assertions, 0 failures, 0 errors actionpack: 3245 runs, 15995 assertions, 1 failures, 0 errors actiontext: 49 runs, 88 assertions, 5 failures, 0 errors actionview: 2037 runs, 4484 assertions, 2 failures, 3 errors activejob: 292 runs, 648 assertions, 0 failures, 0 errors activemodel: 842 runs, 2330 assertions, 0 failures, 0 errors activestorage:222 runs, 683 assertions, 0 failures, 0 errors activesupport: 4338 runs, 13858 assertions, 12 failures, 1 errors 99.921% passing!
  48. ARJDBC (master - 6.x) • Same excludes as 52.x •

    New excludes as we work on new 6.0.0.beta • Rails 6 apps basically work fine! sqlite3: 20 excludes, 6415 runs, 16749 assertions postgresql: 89 excludes, 6962 runs, 18289 assertions mysql2: 66 excludes, 6535 runs, 17210 assertions