Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ Lint/UnderscorePrefixedVariableName:
Lint/EmptyBlock:
Enabled: false

Lint/DuplicateBranch:
Enabled: false

Lint/MissingSuper:
Enabled: false

Expand Down Expand Up @@ -63,6 +66,12 @@ Metrics/ParameterLists:
Metrics/PerceivedComplexity:
Enabled: false

Style/InfiniteLoop:
Enabled: false

Style/WhileUntilModifier:
Enabled: false

Style/Alias:
EnforcedStyle: prefer_alias_method

Expand Down
5 changes: 4 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,15 @@ namespace :hiredis do
end
end

benchmark_suites = %w(single pipelined)
benchmark_suites = %w(single pipelined drivers)
benchmark_modes = %i[ruby yjit hiredis]
namespace :benchmark do
benchmark_suites.each do |suite|
benchmark_modes.each do |mode|
next if suite == "drivers" && mode == :hiredis

name = "#{suite}_#{mode}"
desc name
task name do
output_path = "benchmark/#{name}.md"
sh "rm", "-f", output_path
Expand Down
43 changes: 43 additions & 0 deletions benchmark/drivers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative "setup"

ruby = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :ruby)
hiredis = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :hiredis)

ruby.call("SET", "key", "value")
ruby.call("SET", "large", "value" * 10_000)
ruby.call("LPUSH", "list", *5.times.to_a)
ruby.call("LPUSH", "large-list", *1000.times.to_a)
ruby.call("HMSET", "hash", *8.times.to_a)
ruby.call("HMSET", "large-hash", *1000.times.to_a)

benchmark("small string x 100") do |x|
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("GET", "key") } } }
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("GET", "key") } } }
end

benchmark("large string x 100") do |x|
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("GET", "large") } } }
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("GET", "large") } } }
end

benchmark("small list x 100") do |x|
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } }
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } }
end

benchmark("large list") do |x|
x.report("hiredis") { hiredis.call("LRANGE", "large-list", 0, -1) }
x.report("ruby") { ruby.call("LRANGE", "large-list", 0, -1) }
end

benchmark("small hash x 100") do |x|
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } }
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } }
end

benchmark("large hash") do |x|
x.report("hiredis") { ruby.call("HGETALL", "large-hash") }
x.report("ruby") { ruby.call("HGETALL", "large-hash") }
end
59 changes: 59 additions & 0 deletions benchmark/drivers_ruby.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]`

redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c`


### small string x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 4825.5 i/s
ruby: 2863.4 i/s - 1.69x slower

```

### large string x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 266.6 i/s
ruby: 198.1 i/s - 1.35x slower

```

### small list x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 2416.9 i/s
ruby: 1223.3 i/s - 1.98x slower

```

### large list

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 5351.6 i/s
ruby: 1718.0 i/s - 3.11x slower

```

### small hash x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 2854.3 i/s
ruby: 1294.4 i/s - 2.21x slower

```

### large hash

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
hiredis: 1580.6 i/s
ruby: 1634.7 i/s - same-ish: difference falls within error

```

59 changes: 59 additions & 0 deletions benchmark/drivers_yjit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]`

redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c`


### small string x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 6407.8 i/s
ruby: 5852.0 i/s - same-ish: difference falls within error

```

### large string x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 302.8 i/s
ruby: 337.3 i/s - same-ish: difference falls within error

```

### small list x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 4067.7 i/s
ruby: 2721.5 i/s - 1.49x slower

```

### large list

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 7138.7 i/s
ruby: 6605.4 i/s - same-ish: difference falls within error

```

### small hash x 100

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 4219.8 i/s
ruby: 3586.4 i/s - 1.18x slower

```

### large hash

```
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
hiredis: 5240.9 i/s
ruby: 5312.5 i/s - same-ish: difference falls within error

```

66 changes: 51 additions & 15 deletions lib/redis_client/ruby_connection/buffered_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ class BufferedIO

attr_accessor :read_timeout, :write_timeout

def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096, encoding: Encoding.default_external)
@io = io
@buffer = "".b
@encoding = encoding
@buffer = "".dup.force_encoding(@encoding)
@offset = 0
@chunk_size = chunk_size
@read_timeout = read_timeout
Expand Down Expand Up @@ -82,8 +83,10 @@ def write(string)
end

def getbyte
ensure_remaining(1)
byte = @buffer.getbyte(@offset)
unless byte = @buffer.getbyte(@offset)
ensure_remaining(1)
byte = @buffer.getbyte(@offset)
end
@offset += 1
byte
end
Expand All @@ -99,6 +102,29 @@ def gets_chomp
line
end

def gets_integer
int = 0
offset = @offset
while true
chr = @buffer.getbyte(offset)

if chr
if chr == 13 # "\r".ord
@offset = offset + 2
break
else
int = (int * 10) + chr - 48
end
offset += 1
else
ensure_line
return gets_integer
end
end

int
end

def read_chomp(bytes)
ensure_remaining(bytes + EOL_SIZE)
str = @buffer.byteslice(@offset, bytes)
Expand All @@ -108,16 +134,27 @@ def read_chomp(bytes)

private

def ensure_line
fill_buffer(false) if @offset >= @buffer.bytesize
until @buffer.index(EOL, @offset)
fill_buffer(false)
end
end

def ensure_remaining(bytes)
needed = bytes - (@buffer.bytesize - @offset)
if needed > 0
fill_buffer(true, needed)
end
end

RESET_BUFFER_ENCODING = RUBY_ENGINE == "truffleruby"
private_constant :RESET_BUFFER_ENCODING

def fill_buffer(strict, size = @chunk_size)
remaining = size
empty_buffer = @offset >= @buffer.bytesize
start = @offset - @buffer.bytesize
empty_buffer = start >= 0

loop do
bytes = if empty_buffer
Expand All @@ -126,15 +163,6 @@ def fill_buffer(strict, size = @chunk_size)
@io.read_nonblock([remaining, @chunk_size].max, exception: false)
end
case bytes
when String
if empty_buffer
@offset = 0
empty_buffer = false
else
@buffer << bytes
end
remaining -= bytes.bytesize
return if !strict || remaining <= 0
when :wait_readable
unless @io.to_io.wait_readable(@read_timeout)
raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
Expand All @@ -144,7 +172,15 @@ def fill_buffer(strict, size = @chunk_size)
when nil
raise EOFError
else
raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
if empty_buffer
@offset = start
empty_buffer = false
@buffer.force_encoding(@encoding) if RESET_BUFFER_ENCODING
else
@buffer << bytes.force_encoding(@encoding)
end
remaining -= bytes.bytesize
return if !strict || remaining <= 0
end
end
end
Expand Down
39 changes: 31 additions & 8 deletions lib/redis_client/ruby_connection/resp3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,39 @@ def dump_symbol(symbol, buffer)

def parse(io)
type = io.getbyte
method = PARSER_TYPES.fetch(type) do
if type == 35 # '#'.ord
parse_boolean(io)
elsif type == 36 # '$'.ord
parse_blob(io)
elsif type == 43 # '+'.ord
parse_string(io)
elsif type == 61 # '='.ord
parse_verbatim_string(io)
elsif type == 45 # '-'.ord
parse_error(io)
elsif type == 58 # ':'.ord
parse_integer(io)
elsif type == 40 # '('.ord
parse_integer(io)
elsif type == 44 # ','.ord
parse_double(io)
elsif type == 95 # '_'.ord
parse_null(io)
elsif type == 42 # '*'.ord
parse_array(io)
elsif type == 37 # '%'.ord
parse_map(io)
elsif type == 126 # '~'.ord
parse_set(io)
elsif type == 62 # '>'.ord
parse_array(io)
else
raise UnknownType, "Unknown sigil type: #{type.chr.inspect}"
end
send(method, io)
end

def parse_string(io)
str = io.gets_chomp
str.force_encoding(Encoding.default_external)
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
str.freeze
end
Expand All @@ -140,16 +164,16 @@ def parse_boolean(io)
end

def parse_array(io)
parse_sequence(io, parse_integer(io))
parse_sequence(io, io.gets_integer)
end

def parse_set(io)
parse_sequence(io, parse_integer(io))
parse_sequence(io, io.gets_integer)
end

def parse_map(io)
hash = {}
parse_integer(io).times do
io.gets_integer.times do
hash[parse(io)] = parse(io)
end
hash
Expand Down Expand Up @@ -192,11 +216,10 @@ def parse_null(io)
end

def parse_blob(io)
bytesize = parse_integer(io)
bytesize = io.gets_integer
return if bytesize < 0 # RESP2 nil type

str = io.read_chomp(bytesize)
str.force_encoding(Encoding.default_external)
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
str
end
Expand Down