Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Q: Super slow Socket IO ? #3253

Closed
boazsegev opened this issue Aug 14, 2015 · 1 comment
Closed

Q: Super slow Socket IO ? #3253

boazsegev opened this issue Aug 14, 2015 · 1 comment

Comments

@boazsegev
Copy link

After adjusting the GReactor gem to comply with JRuby (mainly avoiding the freeze string issue), I discovered that the JRuby implementation for the same code is SUPER slow.

The difference was between 36,895 Req/sec and 6554 Req/sec...

MRI was 600% faster?! This must be an issue either with the GReactor gem or with the JRuby implementation....

I wrote the most simple server emulation I could think of (attached), leveraging on JRuby's specialty - many threads. The results matched the expected results.

JRuby: 72,045 req/sec
MRI: 28,267 req/sec

I wrote a quick and dirty reactor loop to test if the issue was related to anything in the core ruby implementation that I was using.

MRI gave me 30K Req/sec...
JRuby gave me a plenty of errors I couldn't decipher forced my hand into using blocking calls (socket.gets instead of socket.recv_nonblock) and finally gave me ~23K Req/sec...

What am I missing?

I thought JRuby was supposed to comply with MRI 2.2 behavior?

Code for GReactor testing

require 'greactor'
require 'stringio'


class MiniServer < GReactor::Protocol
    def on_connect
     @con ||= Time.now
     conn_inactivity_timeout = 5
     @headers = {}
    end

    def on_message data
        data = StringIO.new data
        l = nil
        headers = @headers
        while (l = data.gets)
            unless l.match /^[\r]?\n/
                if l.include? ':'
                    l = l.strip.downcase.split(':', 2)
                    headers[l[0]] = l[1]
                else
                    headers[:method], headers[:query], headers[:version] = l.strip.split(/[\s]+/, 3)
                    headers[:request_start] = Time.now
                end
                next
            end
            if headers['connection'].to_s.match(/keep/i) || headers[:version].match(/1\.1/)
                send "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\nKeep-Alive: 5\r\n\r\nhello world\n"
            else
                send "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: close\r\n\r\nhello world\n"
                close
            end
            headers.clear
        end
        data.string.clear
    end
end

GR.on_shutdown { GR.instance_exec { puts "We're done: #{@stop}" } }
GR.start(1) { GR.listen timeout: 5, handler: MiniServer, port: 3000 }

Code for a thread-per-request HTTP emulation

require 'socket'
require 'stringio'

server = TCPServer.new(3000)
trap('INT'){ server.close rescue true }
trap('TERM'){ server.close rescue true }

until server.closed?
    Thread.new(server.accept) do |client|
        begin
            headers = {}
            while (l = client.gets)
                unless headers[:method]
                        headers[:method], headers[:query], headers[:version] = l.strip.split(/[\s]+/, 3)
                        headers[:request_start] = Time.now
                        unless headers[:method].to_s.match /get|post|trace|delete|head/i
                            client.close rescue true
                            return false
                        end
                        next    
                end
                unless l.match /^[\r]?\n/
                    if l.include? ':'
                        l = l.strip.downcase.split(':', 2)
                        headers[l[0]] = l[1]
                    else
                    end
                    next
                end
                if headers['connection'].to_s.match(/keep/i) || headers[:version].to_s.match(/1\.1/)
                    client.write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\nKeep-Alive: 5\r\n\r\nhello world\n"
                else
                    client.write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: close\r\n\r\nhello world\n"
                    client.close
                end
                headers.clear
            end
        rescue => e
            # puts e.message
        ensure
            client.close rescue true
        end
    end
end

Code for the worst reactor ever

Please notice that this code uses blocking calls, although that was done after JRuby failed to run while using recv_nonblock:

require 'socket'

class JobManager
    def initialize
        @run = false
        @workers = []
        @queue = []
        @locker = Mutex.new
        @ios = {}
        @io_lock = Mutex.new
        @review_lock = Mutex.new
    end
    def start
        @run = true
        10.times { @workers << ( Thread.new {( work || review ) while @run } ) }
        true
    end
    def stop
        @run = false
        @ios.each {|io, h| io.close rescue true}
        @workers.each {|t| t.join rescue true }
        @workers.clear
        @ios.clear
        @queue.clear
        true
    end

    def listen io, handler = nil, &block
        handler ||= block
        @io_lock.synchronize {@ios[io] = handler}
    end

    def clear io
        io.close unless io.closed?
        @io_lock.synchronize {@ios.delete io}
    end

    def queue *args, &block
        @locker.synchronize {@queue << [args, block]} if block
    end
    def work
        job = @locker.synchronize { @queue.shift }
        return false unless job
        begin
            job[1].call *job[0]
        rescue Errno::EWOULDBLOCK => e

        rescue => e
            puts e.message
            puts e.backtrace
        end
    end
    def review
        @review_lock.synchronize do
            return if @queue.any?
            begin
                io_array = @io_lock.synchronize { @ios.keys }
                io_r = ::IO.select(io_array, nil, io_array, 0.2)
                if io_r
                    io_r[0].each {|io| queue io, &@ios[io] }
                    io_r[2].each { |io| @ios.delete io }
                end
            rescue => e
                @io_lock.synchronize { @ios.keys } .each {|io| clear io if io.closed? || ( (io.stat && false) rescue true )}
            end
            true
        end
    end
end


reactor = JobManager.new
reactor.start
trap('INT'){ reactor.stop ; raise 'stop' }
trap('TERM'){ reactor.stop ; raise 'stop' }

reactor.listen(TCPServer.new(3000)) do |io|
    begin
        reactor.listen(io.accept_nonblock) do |client|
            begin
                headers = {}
                while (l = client.gets)
                    unless headers[:method]
                            headers[:method], headers[:query], headers[:version] = l.strip.split(/[\s]+/, 3)
                            headers[:request_start] = Time.now
                            unless headers[:method].to_s.match /get|post|trace|delete|head/i
                                reactor.clear client
                            end
                            next    
                    end
                    unless l.to_s.match /^[\r]?\n/
                        if l.include? ':'
                            l = l.strip.downcase.split(':', 2)
                            headers[l[0]] = l[1]
                        else
                        end
                        next
                    end
                    if headers['connection'].to_s.match(/keep/i) || headers[:version].to_s.match(/1\.1/)
                        client.write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\nKeep-Alive: 5\r\n\r\nhello world\n"
                    else
                        client.write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: close\r\n\r\nhello world\n"
                        client.close
                    end
                    headers.clear
                end
            rescue Errno::EWOULDBLOCK => e
            end
        end
    rescue Errno::EWOULDBLOCK => e
    end
end

sleep rescue true
@boazsegev
Copy link
Author

Duplicate: The issue was discovered as a Mutex.lock slowness.

Opened a designated issue, issue #3255

@enebo enebo added this to the Invalid or Duplicate milestone Aug 20, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants