Skip to content

Commit c06075a

Browse files
authored
perf: reduce memory allocations for pipelining (#123)
1 parent 56e254b commit c06075a

File tree

7 files changed

+133
-18
lines changed

7 files changed

+133
-18
lines changed

.github/workflows/test.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,32 @@ jobs:
338338
done
339339
- name: Stop containers
340340
run: docker compose -f $DOCKER_COMPOSE_FILE down
341+
memory-profile:
342+
name: Memory Profile
343+
timeout-minutes: 15
344+
strategy:
345+
fail-fast: false
346+
runs-on: ubuntu-latest
347+
env:
348+
REDIS_VERSION: '7.0.1'
349+
DOCKER_COMPOSE_FILE: 'compose.yaml'
350+
steps:
351+
- name: Check out code
352+
uses: actions/checkout@v3
353+
- name: Set up Ruby
354+
uses: ruby/setup-ruby@v1
355+
with:
356+
ruby-version: '3.1'
357+
bundler-cache: true
358+
- name: Pull Docker images
359+
run: docker pull redis:$REDIS_VERSION
360+
- name: Run containers
361+
run: docker compose -f $DOCKER_COMPOSE_FILE up -d
362+
- name: Wait for Redis cluster to be ready
363+
run: bundle exec rake wait
364+
- name: Print containers
365+
run: docker compose -f $DOCKER_COMPOSE_FILE ps
366+
- name: Run memory profiler
367+
run: bundle exec rake prof
368+
- name: Stop containers
369+
run: docker compose -f $DOCKER_COMPOSE_FILE down

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
44
gemspec name: 'redis-cluster-client'
55

66
gem 'hiredis-client', '~> 0.6'
7+
gem 'memory_profiler'
78
gem 'minitest'
89
gem 'rake'
910
gem 'rubocop'

Rakefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ Rake::TestTask.new(:bench) do |t|
3434
t.test_files = ARGV.size > 1 ? ARGV[1..] : Dir['test/**/bench_*.rb']
3535
end
3636

37+
Rake::TestTask.new(:prof) do |t|
38+
t.libs << :lib
39+
t.libs << :test
40+
t.options = '-v'
41+
t.warning = false
42+
t.test_files = ARGV.size > 1 ? ARGV[1..] : Dir['test/**/prof_*.rb']
43+
end
44+
3745
desc 'Wait for cluster to be ready'
3846
task :wait do
3947
$LOAD_PATH.unshift(File.expand_path('test', __dir__))

lib/redis_client/cluster.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ def zscan(key, *args, **kwargs, &block)
7878
end
7979

8080
def pipelined
81-
pipeline = ::RedisClient::Cluster::Pipeline.new(@router, @command_builder)
81+
seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
82+
pipeline = ::RedisClient::Cluster::Pipeline.new(@router, @command_builder, seed: seed)
8283
yield pipeline
8384
return [] if pipeline.empty? == 0
8485

lib/redis_client/cluster/command.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ def pick_details(details)
7272
end
7373

7474
def dig_details(command, key)
75-
name = command&.flatten&.first.to_s.downcase
75+
name = command&.flatten&.first.to_s.downcase # OPTIMIZE: prevent allocation for string
7676
return if name.empty? || !@details.key?(name)
7777

7878
@details.fetch(name).fetch(key)
7979
end
8080

8181
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
82-
case command&.flatten&.first.to_s.downcase
82+
case command&.flatten&.first.to_s.downcase # OPTIMIZE: prevent allocation for string
8383
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
8484
when 'object' then 2
8585
when 'memory'

lib/redis_client/cluster/pipeline.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,54 +9,48 @@ class Pipeline
99
ReplySizeError = Class.new(::RedisClient::Error)
1010
MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
1111

12-
def initialize(router, command_builder)
12+
def initialize(router, command_builder, seed: Random.new_seed)
1313
@router = router
1414
@command_builder = command_builder
15-
@grouped = Hash.new([].freeze)
15+
@grouped = {}
1616
@size = 0
17-
@seed = Random.new_seed
17+
@seed = seed
1818
end
1919

2020
def call(*args, **kwargs, &block)
2121
command = @command_builder.generate(args, kwargs)
2222
node_key = @router.find_node_key(command, seed: @seed)
23-
@grouped[node_key] += [[@size, :call_v, command, block]]
24-
@size += 1
23+
add_line(node_key, [@size, :call_v, command, block])
2524
end
2625

2726
def call_v(args, &block)
2827
command = @command_builder.generate(args)
2928
node_key = @router.find_node_key(command, seed: @seed)
30-
@grouped[node_key] += [[@size, :call_v, command, block]]
31-
@size += 1
29+
add_line(node_key, [@size, :call_v, command, block])
3230
end
3331

3432
def call_once(*args, **kwargs, &block)
3533
command = @command_builder.generate(args, kwargs)
3634
node_key = @router.find_node_key(command, seed: @seed)
37-
@grouped[node_key] += [[@size, :call_once_v, command, block]]
38-
@size += 1
35+
add_line(node_key, [@size, :call_once_v, command, block])
3936
end
4037

4138
def call_once_v(args, &block)
4239
command = @command_builder.generate(args)
4340
node_key = @router.find_node_key(command, seed: @seed)
44-
@grouped[node_key] += [[@size, :call_once_v, command, block]]
45-
@size += 1
41+
add_line(node_key, [@size, :call_once_v, command, block])
4642
end
4743

4844
def blocking_call(timeout, *args, **kwargs, &block)
4945
command = @command_builder.generate(args, kwargs)
5046
node_key = @router.find_node_key(command, seed: @seed)
51-
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
52-
@size += 1
47+
add_line(node_key, [@size, :blocking_call_v, timeout, command, block])
5348
end
5449

5550
def blocking_call_v(timeout, args, &block)
5651
command = @command_builder.generate(args)
5752
node_key = @router.find_node_key(command, seed: @seed)
58-
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
59-
@size += 1
53+
add_line(node_key, [@size, :blocking_call_v, timeout, command, block])
6054
end
6155

6256
def empty?
@@ -92,6 +86,14 @@ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Met
9286

9387
raise ::RedisClient::Cluster::ErrorCollection, errors
9488
end
89+
90+
private
91+
92+
def add_line(node_key, line)
93+
@grouped[node_key] = [] unless @grouped.key?(node_key)
94+
@grouped[node_key] << line
95+
@size += 1
96+
end
9597
end
9698
end
9799
end

test/prof_mem.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
3+
require 'memory_profiler'
4+
require 'redis_cluster_client'
5+
require 'testing_constants'
6+
7+
module ProfMem
8+
module_function
9+
10+
ATTEMPT_COUNT = 1000
11+
12+
def run
13+
%w[primary_only scale_read_random scale_read_latency pooled].each do |cli_type|
14+
print "################################################################################\n"
15+
print "# #{cli_type}\n"
16+
print "################################################################################\n"
17+
print "\n"
18+
19+
profile do
20+
send("new_#{cli_type}_client".to_sym).pipelined do |pi|
21+
ATTEMPT_COUNT.times { |i| pi.call('SET', "key#{i}", i) }
22+
ATTEMPT_COUNT.times { |i| pi.call('GET', "key#{i}") }
23+
end
24+
end
25+
end
26+
end
27+
28+
def profile(&block)
29+
# https://github.com/SamSaffron/memory_profiler
30+
report = ::MemoryProfiler.report(top: 10, &block)
31+
report.pretty_print(color_output: true, normalize_paths: true)
32+
end
33+
34+
def new_primary_only_client
35+
::RedisClient.cluster(
36+
nodes: TEST_NODE_URIS,
37+
fixed_hostname: TEST_FIXED_HOSTNAME,
38+
**TEST_GENERIC_OPTIONS
39+
).new_client
40+
end
41+
42+
def new_scale_read_random_client
43+
::RedisClient.cluster(
44+
nodes: TEST_NODE_URIS,
45+
replica: true,
46+
replica_affinity: :random,
47+
fixed_hostname: TEST_FIXED_HOSTNAME,
48+
**TEST_GENERIC_OPTIONS
49+
).new_client
50+
end
51+
52+
def new_scale_read_latency_client
53+
::RedisClient.cluster(
54+
nodes: TEST_NODE_URIS,
55+
replica: true,
56+
replica_affinity: :latency,
57+
fixed_hostname: TEST_FIXED_HOSTNAME,
58+
**TEST_GENERIC_OPTIONS
59+
).new_client
60+
end
61+
62+
def new_pooled_client
63+
::RedisClient.cluster(
64+
nodes: TEST_NODE_URIS,
65+
fixed_hostname: TEST_FIXED_HOSTNAME,
66+
**TEST_GENERIC_OPTIONS
67+
).new_pool(
68+
timeout: TEST_TIMEOUT_SEC,
69+
size: 2
70+
)
71+
end
72+
end
73+
74+
ProfMem.run

0 commit comments

Comments
 (0)