« Back to Index

Concurrency: JRuby vs MRI

View original Gist on GitHub

1. test.rb

require "thread"

module ConcurrencyTest
  def self.run!
    mutex       = Mutex.new
    limit       = 1200
    shared_data = 0

    # Create 1200 Threads
    # Each Thread will attempt to increment the shared data/memory
    # Each Thread continues until the shared data/memory is equal to the defined limit

    (1..limit).map do
      Thread.new do
        i = 0

        while i < limit
          i += 1
          mutex.synchronize { shared_data += 1 }
        end
      end
    end.each { |t| t.join }

    raise shared_data.to_s if shared_data != limit * limit

    shared_data.to_s
  end
end

2. test results.bash

# These results were based on running the script from the shell

# System Ruby: 2.0.0p481
time ruby test.rb
ruby test.rb  16.11s user 237.43s system 206% cpu 2:02.63 total

# MRI: 2.1.3p242
time ruby test.rb
ruby test.rb  15.90s user 263.46s system 194% cpu 2:23.70 total

# JRuby 1.7.12
time ruby test.rb
ruby test.rb  15.93s user 14.35s system 280% cpu 10.778 total

# UPDATE: I modified test.rb to be wrapped in a module for the purposes of testing within a web application

3. config.ru

require_relative "../test"

run -> env do
  [200, {"Content-Type" => "text/html"}, [ConcurrencyTest::run!]]
end

4. Web Result.md

JRuby 1.7.12 (Puma):
2.0790 seconds

MRI 2.1.3p242 (Unicorn):
30.9189 seconds

Note: these are using the default settings for both Puma and Unicorn

Update

MRI 2.1.3p242 (Unicorn - when configured with 8 processors**):
12.8673 seconds

**see the following Unicorn config which is executed using
bundle exec unicorn -c unicorn_config.rb

5. Unicorn Config.rb

# Unicorn forks multiple OS processes within each dyno to allow an app to
# support multiple concurrent requests without requiring them to be thread-safe
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 8)

# To ensure your application’s requests do not tie up your app with zombie processes when the server timeouts early
timeout 30

# Preloading your application reduces the startup time of individual Unicorn
# worker_processes and allows you to manage the external connections of each
# individual worker using the before_fork and after_fork calls
preload_app true

before_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
    Process.kill 'QUIT', Process.pid
  end
end

after_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
  end
end