Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jruby/jruby
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 32f213b20741
Choose a base ref
...
head repository: jruby/jruby
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 903728b770b1
Choose a head ref
  • 2 commits
  • 1 file changed
  • 2 contributors

Commits on Apr 30, 2017

  1. Fix wrong method lookup cache used when class is prepended

    While benchmarking a @Talkdesk JRuby application during our recent
    hackaton, I noticed quite a lot of thread contention in a workload where
    there should be no shared data.
    
    Stack traces showed that there was contention while performing writes to
    the method lookup cache:
    
    ```
    java.lang.Thread.State: BLOCKED (on object monitor)
         at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
         - waiting to lock <0x00000000e1565058> (a java.util.concurrent.ConcurrentHashMap$Node)
         at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
         at org.jruby.RubyModule.addToCache(RubyModule.java:1467)
         at org.jruby.RubyModule.searchWithCache(RubyModule.java:1315)
         at org.jruby.RubyModule.searchWithCache(RubyModule.java:1269)
         at org.jruby.RubyModule.searchMethod(RubyModule.java:1258)
         at org.jruby.RubyModule.respondsToMethod(RubyModule.java:1829)
    ```
    
    I ended up instrumenting the JRuby code and noticed that this kept
    happening (on this workload) for calls to `Array.respond_to?(:to_ary)`
    which happened quite often and never seemed to be cached.
    
    But why were threads repeatedly failing to lookup `#to_ary`, saving it
    on the cache, and then not finding it the next time around?
    
    Every instance of the `RubyModule` class keeps a method lookup cache in
    the `myCachedMethods` and normally reads and writes to it. But
    additionally, every `RubyModule` instance keeps a reference called
    `methodLocation` to the object where it keeps its methods.
    
    Usually, `methodLocation` is set to `this` (e.g. the `RubyModule` stores
    its own methods), but in some cases, e.g. when a class is prepended,
    `methodLocation` changes, and points elsewhere.
    
    The problem here is that when looking up on the `myCachedMethods`, the
    `cacheHit` method always checks the local `myCachedMethods` reference,
    while the `addToCache` writes to the cache inside the `methodLocation`
    reference. This means that normally the method cache works fine as
    `this.myCachedMethods` and `this.methodLocation.myCachedMethods` are the
    same, but when `methodLocation` changes, the cache reads will never find
    the correct cached method.
    
    As a fix, I updated the `cacheHit` method to also read from
    `this.methodLocation.myCachedMethods`.
    
    Included below are my test cases and results from executing them.
    
    Thanks go to @Talkdesk for letting me work on making JRuby kick even
    more ass.
    
    Single-threaded performance impact test-case:
    
    ```ruby
    require 'benchmark/ips'
    
    module Foo; end
    
    mode = ARGV.first
    
    if mode == 'prepend'
      Array.prepend(Foo)
    elsif mode == 'no-prepend'
      # do nothing
    else
      fail 'no mode supplied'
    end
    
    Benchmark.ips do |benchmark|
      benchmark.time = 15
      benchmark.warmup = 15
    
      benchmark.report(mode) { [1, 2, 3].respond_to?(:to_ary) }
    
      benchmark.compare!
    end
    ```
    
    Before fix:
    
    ```
    $ jruby singlethreaded-bug-bench.rb no-prepend
    Warming up --------------------------------------
              no-prepend   265.802k i/100ms
    Calculating -------------------------------------
              no-prepend     14.619M (± 5.1%) i/s -    218.489M in
    14.993450s
    
    $ jruby singlethreaded-bug-bench.rb prepend
    Warming up --------------------------------------
                 prepend   222.863k i/100ms
    Calculating -------------------------------------
                 prepend      6.879M (± 3.1%) i/s -    103.186M in
    15.017267s
    ```
    
    After fix:
    
    ```
    $ jruby singlethreaded-bug-bench.rb no-prepend
    Warming up --------------------------------------
              no-prepend   272.227k i/100ms
    Calculating -------------------------------------
              no-prepend     14.229M (± 4.1%) i/s -    212.882M in
    14.990837s
    
    $ jruby singlethreaded-bug-bench.rb prepend
    Warming up --------------------------------------
                 prepend   271.436k i/100ms
    Calculating -------------------------------------
                 prepend     14.095M (± 4.2%) i/s -    210.906M in
    14.996117s
    ```
    
    Multi-threaded contention test case:
    
    ```ruby
    module Foo; end
    
    mode = ARGV.first
    
    if mode == 'prepend'
      Array.prepend(Foo)
    elsif mode == 'no-prepend'
      # do nothing
    else
      fail 'no mode supplied'
    end
    
    Integer(ENV['THREADS'] || 16).times do
      Thread.new { [1, 2, 3].respond_to?(:to_ary) while true }
    end
    
    sleep
    ```
    
    (See VisualVM screenshots attached to PR for results).
    Ivo Anjo authored and ivoanjo committed Apr 30, 2017
    Copy the full SHA
    8420b3e View commit details

Commits on May 3, 2017

  1. Merge pull request #4581 from ivoanjo/fix-wrong-method-lookup-cache

    Fix wrong method lookup cache used when class is prepended
    headius authored May 3, 2017
    Copy the full SHA
    903728b View commit details
Showing with 1 addition and 1 deletion.
  1. +1 −1 core/src/main/java/org/jruby/RubyModule.java
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/RubyModule.java
Original file line number Diff line number Diff line change
@@ -1340,7 +1340,7 @@ private final Map<String, CacheEntry> getCachedMethodsForWrite() {
}

private CacheEntry cacheHit(String name) {
CacheEntry cacheEntry = getCachedMethods().get(name);
CacheEntry cacheEntry = methodLocation.getCachedMethods().get(name);

if (cacheEntry != null) {
if (cacheEntry.token == getGeneration()) {