Skip to content

Commit

Permalink
fix: resolves the memory leaks in the ruby ffi layer (#46)
Browse files Browse the repository at this point in the history
* fix: resolves the memory leaks in the ruby ffi layer and adds a script to help hunt them down
  • Loading branch information
sighphyre authored Oct 5, 2023
1 parent c878e95 commit fe5b636
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 5 deletions.
5 changes: 4 additions & 1 deletion ruby-engine/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ source "https://rubygems.org"
# Gemfile

gem "ffi"
gem "fiddle"
gem "fiddle"
group :development do
gem "get_process_mem", "~> 0.2.7"
end
5 changes: 4 additions & 1 deletion ruby-engine/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
GEM
remote: https://rubygems.org/
specs:
ffi (1.15.5)
ffi (1.16.3)
fiddle (1.1.1)
get_process_mem (0.2.7)
ffi (~> 1.0)

PLATFORMS
x86_64-linux

DEPENDENCIES
ffi
fiddle
get_process_mem (~> 0.2.7)

BUNDLED WITH
2.4.13
7 changes: 6 additions & 1 deletion ruby-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ Then you can run the tests with:
rspec
```

There's also a `mem_check.rb` in the scripts folder. This is not a bullet proof test, but it can be helpful for detecting large leaks. This requires human interaction - you need to read the output and understand what it's telling you, so it's not run as part of the test suite.

```bash
ruby scripts/mem_check.rb
```

## Build

You can build the gem with:

```bash
gem build unleash-engine.gemspec

```

Then you can install the gem for local development with:
Expand Down
7 changes: 5 additions & 2 deletions ruby-engine/lib/unleash_engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class UnleashEngine
attach_function :engine_new, [], :pointer
attach_function :engine_free, [:pointer], :void

attach_function :engine_take_state, %i[pointer string], :string, :pointer
attach_function :engine_take_state, %i[pointer string], :pointer
attach_function :engine_check_enabled, %i[pointer string string], :pointer
attach_function :engine_check_variant, %i[pointer string string], :pointer
attach_function :engine_get_metrics, [:pointer], :pointer
Expand All @@ -58,7 +58,9 @@ def self.finalize(engine_state)
end

def take_state(toggles)
repsonse_ptr = UnleashEngine.engine_take_state(@engine_state, toggles)
response_ptr = UnleashEngine.engine_take_state(@engine_state, toggles)
take_toggles_response = JSON.parse(response_ptr.read_string, symbolize_names: true)
UnleashEngine.engine_free_response_message(response_ptr)
end

def get_variant(name, context)
Expand Down Expand Up @@ -97,6 +99,7 @@ def count_variant(toggle_name, variant_name)
def get_metrics
metrics_ptr = UnleashEngine.engine_get_metrics(@engine_state)
metrics = JSON.parse(metrics_ptr.read_string, symbolize_names: true)
UnleashEngine.engine_free_response_message(metrics_ptr)
metrics[:value]
end
end
51 changes: 51 additions & 0 deletions ruby-engine/scripts/mem_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require 'objspace'
require 'json'
require_relative '../lib/unleash_engine'
require 'get_process_mem'

unleash_engine = UnleashEngine.new

def run_memory_check(lambda, method_name_being_tested)
puts "#{method_name_being_tested} Warming up"

## This should give us a warmup baseline to allow the runtime to assgin whatever it needs
for i in 0..1000000
lambda.call
end
GC.start

## Now we poke the memory - only memory we need for this call should be assigned now
## everything else should be done in the previous warm up
pre_check_memory = GetProcessMem.new.mb
for i in 0..1000000
lambda.call
end

GC.start
post_check_memory = GetProcessMem.new.mb

puts "#{method_name_being_tested} Memory diff during enabled check: #{post_check_memory} #{pre_check_memory} MB"
diff = post_check_memory - pre_check_memory
if diff < 0.5
puts "#{method_name_being_tested} Memory diff is within half a MB, this is an expected range"
else
STDERR.puts "WARNING: LIKELY MEMORY LEAK DETECTED. THIS REQUIRES HUMAN ATTENTION"
STDERR.puts "#{method_name_being_tested} Memory diff is not within the expected half MB range, this likely indicates a leak within the engine"
end

puts "---"
end

suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
suite_data = JSON.parse(File.read(suite_path))
json_client_features = suite_data['state'].to_json

is_enabled = lambda { unleash_engine.enabled?('Feature.A', {}) }
get_variant = lambda { unleash_engine.get_variant('Feature.A', {}) }
get_metrics = lambda { unleash_engine.get_metrics() }
take_state = lambda { unleash_engine.take_state(json_client_features) }

run_memory_check(is_enabled, "IsEnabled:")
run_memory_check(get_variant, "GetVariant:")
run_memory_check(get_metrics, "GetMetrics:")
run_memory_check(take_state, "TakeState:")

0 comments on commit fe5b636

Please sign in to comment.