ruby_memcheck 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/README.md +104 -36
- data/lib/ruby_memcheck/configuration.rb +43 -39
- data/lib/ruby_memcheck/frame.rb +15 -3
- data/lib/ruby_memcheck/rspec/rake_task.rb +48 -0
- data/lib/ruby_memcheck/stack.rb +29 -1
- data/lib/ruby_memcheck/suppression.rb +23 -0
- data/lib/ruby_memcheck/test_task.rb +3 -29
- data/lib/ruby_memcheck/test_task_reporter.rb +50 -0
- data/lib/ruby_memcheck/valgrind_error.rb +13 -6
- data/lib/ruby_memcheck/version.rb +1 -1
- data/lib/ruby_memcheck.rb +2 -1
- data/ruby_memcheck.gemspec +2 -0
- metadata +38 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 751718a23aee213973e93656dba653e4e253fd3f83618007c39e809827273ac5
|
4
|
+
data.tar.gz: 55d6196fb48e0c18fabb5c87536c2c4114d7a1c1a2b04128ec2c707389934de0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 861d0639b3d06d124f4020cb372472954018ad113ba4864b9367be6c6d167a338297d18c7c4adbcae767ba4a30e8925402bd7faa5a97841a513914ce41e10997
|
7
|
+
data.tar.gz: e0dc1e236947746baef93281f00ec8129f651b7e560e56d2dffc5c188f14e5b32b39adc204ecea2da51c1e9ed341f1c00ff55b4b82521bb2b7b54085d5bfddfc
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -9,11 +9,14 @@ This gem provides a sane way to use Valgrind's memcheck on your native extension
|
|
9
9
|
1. [How does it work?](#how-does-it-work)
|
10
10
|
1. [Limitations](#limitations)
|
11
11
|
1. [Installation](#installation)
|
12
|
-
1. [
|
12
|
+
1. [Setup](#setup)
|
13
|
+
1. [Configuration](#configuration)
|
14
|
+
1. [Suppression files](#suppression-files)
|
15
|
+
1. [License](#license)
|
13
16
|
|
14
17
|
## What is this gem?
|
15
18
|
|
16
|
-
Valgrind's memcheck is a great tool to find and debug memory issues (e.g. memory leak, use-after-free, etc.). However, it doesn't work well on Ruby because Ruby does not free all of the memory it allocates during shutdown. This results in Valgrind reporting thousands (or more) false
|
19
|
+
Valgrind's memcheck is a great tool to find and debug memory issues (e.g. memory leak, use-after-free, etc.). However, it doesn't work well on Ruby because Ruby does not free all of the memory it allocates during shutdown. This results in Valgrind reporting thousands (or more) false positives, making it very difficult for Valgrind to actually be useful. This gem solves the problem by using heuristics to filter out false positives.
|
17
20
|
|
18
21
|
### Who should use this gem?
|
19
22
|
|
@@ -21,11 +24,11 @@ Only gems with native extensions can use this gem. If your gem is written in pla
|
|
21
24
|
|
22
25
|
### How does it work?
|
23
26
|
|
24
|
-
This gem runs Valgrind with the `--xml` option to generate
|
27
|
+
This gem runs Valgrind with the `--xml` option to generate an XML of all the errors. It will then parse the XML and use various heuristics based on the type of the error and the stack trace to filter out errors that are false positives.
|
25
28
|
|
26
29
|
### Limitations
|
27
30
|
|
28
|
-
Because of the aggressive heuristics used to filter out false
|
31
|
+
Because of the aggressive heuristics used to filter out false positives, there are various limitations of what this gem can detect.
|
29
32
|
|
30
33
|
1. This gem is only expected to work on Linux.
|
31
34
|
1. It will not find memory leaks in Ruby. It filters out everything in Ruby.
|
@@ -38,15 +41,13 @@ Because of the aggressive heuristics used to filter out false-positives, there a
|
|
38
41
|
|
39
42
|
## Installation
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
gem "ruby_memcheck"
|
44
|
+
```
|
45
|
+
gem install ruby_memcheck
|
45
46
|
```
|
46
47
|
|
47
|
-
##
|
48
|
+
## Setup
|
48
49
|
|
49
|
-
The easiest way to use this gem is to use it on your test suite using rake.
|
50
|
+
The easiest way to use this gem is to use it on your test suite (minitest or RSpec) using rake.
|
50
51
|
|
51
52
|
0. Install Valgrind.
|
52
53
|
1. In your Rakefile, require this gem.
|
@@ -54,43 +55,110 @@ The easiest way to use this gem is to use it on your test suite using rake.
|
|
54
55
|
```ruby
|
55
56
|
require "ruby_memcheck"
|
56
57
|
```
|
58
|
+
|
59
|
+
- **For RSpec:** If you're using RSpec, also add the following require.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require "ruby_memcheck/rspec/rake_task"
|
63
|
+
```
|
64
|
+
|
57
65
|
1. Configure the gem by calling `RubyMemcheck.config`. You must pass it your binary name. This is the same value you passed into `create_makefile` in your `extconf.rb` file. Make sure this value is correct or it will filter out almost everything as a false-positive!
|
58
66
|
|
59
67
|
```ruby
|
60
68
|
RubyMemcheck.config(binary_name: "your_binary_name")
|
61
69
|
```
|
62
|
-
1.
|
63
|
-
|
70
|
+
1. Setup the test task for your test framework.
|
71
|
+
- **minitest**
|
72
|
+
|
73
|
+
Locate your test task(s) in your Rakefile. You can identify it with a call to `Rake::TestTask.new`.
|
64
74
|
|
65
|
-
|
75
|
+
Create a namespace under the test task and create a `RubyMemcheck::TestTask` with the same configuration.
|
66
76
|
|
67
|
-
|
68
|
-
Rake::TestTask.new(test: :compile) do |t|
|
69
|
-
t.libs << "test"
|
70
|
-
t.test_files = FileList["test/unit/**/*_test.rb"]
|
71
|
-
end
|
72
|
-
```
|
77
|
+
For example, if your Rakefile looked like this before:
|
73
78
|
|
74
|
-
|
79
|
+
```ruby
|
80
|
+
Rake::TestTask.new(test: :compile) do |t|
|
81
|
+
t.libs << "test"
|
82
|
+
t.test_files = FileList["test/unit/**/*_test.rb"]
|
83
|
+
end
|
84
|
+
```
|
75
85
|
|
76
|
-
|
77
|
-
test_config = lambda do |t|
|
78
|
-
t.libs << "test"
|
79
|
-
t.test_files = FileList["test/**/*_test.rb"]
|
80
|
-
end
|
81
|
-
Rake::TestTask.new(test: :compile, &test_config)
|
82
|
-
namespace :test do
|
83
|
-
RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
|
84
|
-
end
|
85
|
-
```
|
86
|
-
1. In your `test_helper.rb`/`spec_helper.rb` (or whatever file sets up your test suite), add this line:
|
86
|
+
You can change it to look like this:
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
```ruby
|
89
|
+
test_config = lambda do |t|
|
90
|
+
t.libs << "test"
|
91
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
92
|
+
end
|
93
|
+
Rake::TestTask.new(test: :compile, &test_config)
|
94
|
+
namespace :test do
|
95
|
+
RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
- **RSpec**
|
100
|
+
|
101
|
+
Locate your rake task(s) in your Rakefile. You can identify it with a call to `RSpec::Core::RakeTask.new`.
|
102
|
+
|
103
|
+
Create a namespace under the test task and create a `RubyMemcheck::RSpec::RakeTask` with the same configuration.
|
104
|
+
|
105
|
+
For example, if your Rakefile looked like this before:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
|
109
|
+
```
|
110
|
+
|
111
|
+
You can change it to look like this:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
|
115
|
+
namespace :spec do
|
116
|
+
RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
1. You're ready to run your test suite with Valgrind using `rake test:valgrind` or `rake spec:valgrind`! Note that this will take a while to run because Valgrind will make Ruby significantly slower.
|
121
|
+
1. (Optional) If you find false positives in the output, you can create Valgrind suppression files. See the [`Suppression files`](#suppression-files) section for more details.
|
122
|
+
|
123
|
+
## Configuration
|
124
|
+
|
125
|
+
When you run `RubyMemcheck.config`, you are creating a default `RubyMemcheck::Configuration`. By default, the Rake tasks for minitest and RSpec will use this configuration. You can also manually pass in a `Configuration` object as the first argument to the constructor of `RubyMemcheck::TestTask` or `RubyMemcheck::RSpec::RakeTask` to use a different `Configuration` object rather than the default one.
|
126
|
+
|
127
|
+
`RubyMemcheck::Configuration` accepts a variety of keyword arguments. Here are all the arguments:
|
128
|
+
|
129
|
+
- `binary_name`: Required. The binary name of your native extension gem. This is the same value you passed into `create_makefile` in your `extconf.rb` file.
|
130
|
+
- `ruby`: Optional. The command to run to invoke Ruby. Defaults to the Ruby that is currently being used.
|
131
|
+
- `valgrind`: Optional. The command to run to invoke Valgrind. Defaults to the string `"valgrind"`.
|
132
|
+
- `valgrind_options`: Optional. Array of options to pass into Valgrind. This is only present as an escape hatch, so avoid using it. This may be deprecated or removed in future versions.
|
133
|
+
- `valgrind_suppressions_dir`: Optional. The string path of the directory that stores suppression files for Valgrind. See the [`Suppression files`](#suppression-files) section for more details. Defaults to `suppressions`.
|
134
|
+
- `valgrind_generate_suppressions`: Optional. Whether suppressions should also be outputted along with the errors. the [`Suppression files`](#suppression-files) section for more details. Defaults to `false`.
|
135
|
+
- `skipped_ruby_functions`: Optional. Ruby functions that are ignored because they are considered a call back into Ruby. This is only present as an escape hatch, so avoid using it. If you find another Ruby function that is a false positive because it calls back into Ruby, please send a patch into this repo. Otherwise, use a Valgrind suppression file.
|
136
|
+
- `valgrind_xml_dir`: Optional. The directory to store temporary XML files for Valgrind. It defaults to a temporary directory. This is present for development debugging, so you shouldn't have to use it.
|
137
|
+
- `output_io`: Optional. The `IO` object to output Valgrind errors to. Defaults to standard error.
|
138
|
+
|
139
|
+
## Suppression files
|
140
|
+
|
141
|
+
If you find false positives in the output, you can create suppression files in a `suppressions` directory in the root directory of your gem. In this directory, you can create [Valgrind suppression files](https://wiki.wxwidgets.org/Valgrind_Suppression_File_Howto).
|
142
|
+
|
143
|
+
The most basic suppression file is `your_binary_name_ruby.supp`. If you want some suppressions for only specific versions of Ruby, you can add the Ruby version to the filename. For example, `your_binary_name_ruby-3.supp` will suppress for any Rubies with a major version of 3 (e.g. 3.0.0, 3.1.1, etc.), while suppression file `your_binary_name_ruby-3.1.supp` will only be used for Ruby with a major and minor version of 3.1 (e.g. 3.1.0, 3.1.1, etc.).
|
144
|
+
|
145
|
+
## Success stories
|
146
|
+
|
147
|
+
Let's celebrate wins from this gem! If this gem was useful for you, please share your story below too!
|
91
148
|
|
92
|
-
|
93
|
-
|
149
|
+
- [`liquid-c`](https://github.com/Shopify/liquid-c):
|
150
|
+
- Found 2 memory leaks: [#157](https://github.com/Shopify/liquid-c/pull/157), [#161](https://github.com/Shopify/liquid-c/pull/161)
|
151
|
+
- Running on CI: [#162](https://github.com/Shopify/liquid-c/pull/162)
|
152
|
+
- [`nokogiri`](https://github.com/sparklemotion/nokogiri):
|
153
|
+
- Found 5 memory leaks: [4 in #2345](https://github.com/sparklemotion/nokogiri/pull/2345), [#2347](https://github.com/sparklemotion/nokogiri/pull/2347)
|
154
|
+
- CI is WIP: [#2344](https://github.com/sparklemotion/nokogiri/pull/2344)
|
155
|
+
- [`rotoscope`](https://github.com/Shopify/rotoscope):
|
156
|
+
- Found a [memory leak in Ruby TracePoint](https://bugs.ruby-lang.org/issues/18264)
|
157
|
+
- Running on CI: [#89](https://github.com/Shopify/rotoscope/pull/89)
|
158
|
+
- [`protobuf`](https://github.com/protocolbuffers/protobuf):
|
159
|
+
- Found 1 memory leak: [#9150](https://github.com/protocolbuffers/protobuf/pull/9150)
|
160
|
+
- [`gRPC`](https://github.com/grpc/grpc):
|
161
|
+
- Found 1 memory leak: [#27900](https://github.com/grpc/grpc/pull/27900)
|
94
162
|
|
95
163
|
## License
|
96
164
|
|
@@ -6,87 +6,91 @@ module RubyMemcheck
|
|
6
6
|
DEFAULT_VALGRIND_OPTIONS = [
|
7
7
|
"--num-callers=50",
|
8
8
|
"--error-limit=no",
|
9
|
+
"--trace-children=yes",
|
9
10
|
"--undef-value-errors=no",
|
10
11
|
"--leak-check=full",
|
11
12
|
"--show-leak-kinds=definite",
|
12
13
|
].freeze
|
14
|
+
DEFAULT_VALGRIND_SUPPRESSIONS_DIR = "suppressions"
|
13
15
|
DEFAULT_SKIPPED_RUBY_FUNCTIONS = [
|
16
|
+
/\Aeval_string_with_cref\z/,
|
17
|
+
/\Arb_add_method_cfunc\z/,
|
14
18
|
/\Arb_check_funcall/,
|
19
|
+
/\Arb_class_boot\z/, # Called for all the different ways to create a Class
|
15
20
|
/\Arb_enc_raise\z/,
|
21
|
+
/\Arb_exc_raise\z/,
|
22
|
+
/\Arb_extend_object\z/,
|
16
23
|
/\Arb_funcall/,
|
24
|
+
/\Arb_intern/,
|
17
25
|
/\Arb_ivar_set\z/,
|
26
|
+
/\Arb_module_new\z/,
|
18
27
|
/\Arb_raise\z/,
|
19
28
|
/\Arb_rescue/,
|
20
29
|
/\Arb_respond_to\z/,
|
30
|
+
/\Arb_thread_create\z/, # Threads are relased to a cache, so they may be reported as a leak
|
21
31
|
/\Arb_yield/,
|
22
32
|
].freeze
|
23
33
|
|
24
|
-
attr_reader :binary_name, :ruby, :valgrind_options, :
|
25
|
-
:skipped_ruby_functions, :
|
34
|
+
attr_reader :binary_name, :ruby, :valgrind, :valgrind_options, :valgrind_suppressions_dir,
|
35
|
+
:valgrind_generate_suppressions, :skipped_ruby_functions, :valgrind_xml_dir, :output_io
|
36
|
+
alias_method :valgrind_generate_suppressions?, :valgrind_generate_suppressions
|
26
37
|
|
27
38
|
def initialize(
|
28
39
|
binary_name:,
|
29
40
|
ruby: FileUtils::RUBY,
|
30
41
|
valgrind: DEFAULT_VALGRIND,
|
31
42
|
valgrind_options: DEFAULT_VALGRIND_OPTIONS,
|
43
|
+
valgrind_suppressions_dir: DEFAULT_VALGRIND_SUPPRESSIONS_DIR,
|
44
|
+
valgrind_generate_suppressions: false,
|
32
45
|
skipped_ruby_functions: DEFAULT_SKIPPED_RUBY_FUNCTIONS,
|
33
|
-
|
46
|
+
valgrind_xml_dir: Dir.mktmpdir,
|
34
47
|
output_io: $stderr
|
35
48
|
)
|
36
49
|
@binary_name = binary_name
|
37
50
|
@ruby = ruby
|
38
51
|
@valgrind = valgrind
|
39
52
|
@valgrind_options = valgrind_options
|
53
|
+
@valgrind_suppressions_dir = File.expand_path(valgrind_suppressions_dir)
|
54
|
+
@valgrind_generate_suppressions = valgrind_generate_suppressions
|
40
55
|
@skipped_ruby_functions = skipped_ruby_functions
|
41
56
|
@output_io = output_io
|
42
57
|
|
43
|
-
if
|
44
|
-
|
58
|
+
if valgrind_xml_dir
|
59
|
+
valgrind_xml_dir = File.expand_path(valgrind_xml_dir)
|
60
|
+
FileUtils.mkdir_p(valgrind_xml_dir)
|
61
|
+
@valgrind_xml_dir = valgrind_xml_dir
|
45
62
|
@valgrind_options += [
|
46
63
|
"--xml=yes",
|
47
|
-
|
64
|
+
# %p will be replaced with the PID
|
65
|
+
# This prevents forking and shelling out from generating a corrupted XML
|
66
|
+
# See --log-file from https://valgrind.org/docs/manual/manual-core.html
|
67
|
+
"--xml-file=#{File.join(valgrind_xml_dir, "%p.out")}",
|
48
68
|
]
|
49
69
|
end
|
50
70
|
end
|
51
71
|
|
52
72
|
def command(*args)
|
53
|
-
|
73
|
+
[
|
74
|
+
valgrind,
|
75
|
+
valgrind_options,
|
76
|
+
get_valgrind_suppression_files(valgrind_suppressions_dir).map { |f| "--suppressions=#{f}" },
|
77
|
+
valgrind_generate_suppressions ? "--gen-suppressions=all" : "",
|
78
|
+
ruby,
|
79
|
+
args,
|
80
|
+
].flatten.join(" ")
|
54
81
|
end
|
55
82
|
|
56
|
-
|
57
|
-
in_binary = false
|
83
|
+
private
|
58
84
|
|
59
|
-
|
60
|
-
|
85
|
+
def get_valgrind_suppression_files(dir)
|
86
|
+
full_ruby_version = "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
|
87
|
+
versions = [full_ruby_version]
|
88
|
+
(0..3).reverse_each { |i| versions << full_ruby_version.split(".")[0, i].join(".") }
|
89
|
+
versions << RUBY_ENGINE
|
61
90
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
return true if skipped_ruby_functions.any? { |r| r.match?(fn) }
|
66
|
-
end
|
67
|
-
elsif frame_in_binary?(frame) # in binary
|
68
|
-
in_binary = true
|
69
|
-
|
70
|
-
# Skip the Init function
|
71
|
-
return true if fn == "Init_#{binary_name}"
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
!in_binary
|
76
|
-
end
|
77
|
-
|
78
|
-
def frame_in_ruby?(frame)
|
79
|
-
frame.obj == ruby ||
|
80
|
-
# Hack to fix Ruby built with --enabled-shared
|
81
|
-
File.basename(frame.obj) == "libruby.so.#{RUBY_VERSION}"
|
82
|
-
end
|
83
|
-
|
84
|
-
def frame_in_binary?(frame)
|
85
|
-
if frame.obj
|
86
|
-
File.basename(frame.obj, ".*") == binary_name
|
87
|
-
else
|
88
|
-
false
|
89
|
-
end
|
91
|
+
versions.map do |version|
|
92
|
+
Dir[File.join(dir, "#{binary_name}_#{version}.supp")]
|
93
|
+
end.flatten
|
90
94
|
end
|
91
95
|
end
|
92
96
|
end
|
data/lib/ruby_memcheck/frame.rb
CHANGED
@@ -2,17 +2,29 @@
|
|
2
2
|
|
3
3
|
module RubyMemcheck
|
4
4
|
class Frame
|
5
|
-
attr_reader :fn, :obj, :file, :line
|
6
|
-
alias_method :in_binary?, :in_binary
|
5
|
+
attr_reader :configuration, :fn, :obj, :file, :line
|
7
6
|
|
8
7
|
def initialize(configuration, frame_xml)
|
8
|
+
@configuration = configuration
|
9
9
|
@fn = frame_xml.at_xpath("fn")&.content
|
10
10
|
@obj = frame_xml.at_xpath("obj")&.content
|
11
11
|
# file and line may not be available
|
12
12
|
@file = frame_xml.at_xpath("file")&.content
|
13
13
|
@line = frame_xml.at_xpath("line")&.content
|
14
|
+
end
|
15
|
+
|
16
|
+
def in_ruby?
|
17
|
+
obj == configuration.ruby ||
|
18
|
+
# Hack to fix Ruby built with --enabled-shared
|
19
|
+
File.basename(obj) == "libruby.so.#{RUBY_VERSION}"
|
20
|
+
end
|
14
21
|
|
15
|
-
|
22
|
+
def in_binary?
|
23
|
+
if obj
|
24
|
+
File.basename(obj, ".*") == configuration.binary_name
|
25
|
+
else
|
26
|
+
false
|
27
|
+
end
|
16
28
|
end
|
17
29
|
|
18
30
|
def to_s
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
module RubyMemcheck
|
6
|
+
module RSpec
|
7
|
+
class RakeTask < ::RSpec::Core::RakeTask
|
8
|
+
include TestTaskReporter
|
9
|
+
|
10
|
+
attr_reader :configuration
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@configuration =
|
14
|
+
if !args.empty? && args[0].is_a?(Configuration)
|
15
|
+
args.shift
|
16
|
+
else
|
17
|
+
RubyMemcheck.default_configuration
|
18
|
+
end
|
19
|
+
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_task(verbose)
|
24
|
+
error = nil
|
25
|
+
|
26
|
+
begin
|
27
|
+
# RSpec::Core::RakeTask#run_task calls Kernel.exit on failure
|
28
|
+
super
|
29
|
+
rescue SystemExit => e
|
30
|
+
error = e
|
31
|
+
end
|
32
|
+
|
33
|
+
report_valgrind_errors
|
34
|
+
|
35
|
+
raise error if error
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def spec_command
|
41
|
+
# First part of command is Ruby
|
42
|
+
args = super.split(" ")[1..]
|
43
|
+
|
44
|
+
configuration.command(args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/ruby_memcheck/stack.rb
CHANGED
@@ -2,10 +2,38 @@
|
|
2
2
|
|
3
3
|
module RubyMemcheck
|
4
4
|
class Stack
|
5
|
-
attr_reader :frames
|
5
|
+
attr_reader :configuration, :frames
|
6
6
|
|
7
7
|
def initialize(configuration, stack_xml)
|
8
|
+
@configuration = configuration
|
8
9
|
@frames = stack_xml.xpath("frame").map { |frame| Frame.new(configuration, frame) }
|
9
10
|
end
|
11
|
+
|
12
|
+
def skip?
|
13
|
+
in_binary = false
|
14
|
+
|
15
|
+
frames.each do |frame|
|
16
|
+
fn = frame.fn
|
17
|
+
|
18
|
+
if frame.in_ruby?
|
19
|
+
# If a stack from from the binary was encountered first, then this
|
20
|
+
# memory leak did not occur from Ruby
|
21
|
+
unless in_binary
|
22
|
+
# Skip this stack because it was called from Ruby
|
23
|
+
return true if configuration.skipped_ruby_functions.any? { |r| r.match?(fn) }
|
24
|
+
end
|
25
|
+
elsif frame.in_binary?
|
26
|
+
in_binary = true
|
27
|
+
|
28
|
+
# Skip the Init function because it is only ever called once, so
|
29
|
+
# leaks in it cannot cause memory bloat
|
30
|
+
return true if fn == "Init_#{configuration.binary_name}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Skip if the stack was never in the binary because it is very likely
|
35
|
+
# not a leak in the native gem
|
36
|
+
!in_binary
|
37
|
+
end
|
10
38
|
end
|
11
39
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyMemcheck
|
4
|
+
class Suppression
|
5
|
+
attr_reader :root
|
6
|
+
|
7
|
+
def initialize(configuration, suppression_node)
|
8
|
+
@root = suppression_node
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
str = StringIO.new
|
13
|
+
str << "{\n"
|
14
|
+
str << " #{root.at_xpath("sname").content}\n"
|
15
|
+
str << " #{root.at_xpath("skind").content}\n"
|
16
|
+
root.xpath("./sframe/fun | ./sframe/obj").each do |frame|
|
17
|
+
str << " #{frame.name}:#{frame.content}\n"
|
18
|
+
end
|
19
|
+
str << "}\n"
|
20
|
+
str.string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
module RubyMemcheck
|
4
4
|
class TestTask < Rake::TestTask
|
5
|
-
|
5
|
+
include TestTaskReporter
|
6
6
|
|
7
|
-
attr_reader :configuration
|
7
|
+
attr_reader :configuration
|
8
8
|
|
9
9
|
def initialize(*args)
|
10
10
|
@configuration =
|
@@ -20,36 +20,10 @@ module RubyMemcheck
|
|
20
20
|
def ruby(*args, **options, &block)
|
21
21
|
command = configuration.command(args)
|
22
22
|
sh(command, **options) do |ok, res|
|
23
|
-
|
24
|
-
parse_valgrind_output
|
25
|
-
unless errors.empty?
|
26
|
-
output_valgrind_errors
|
27
|
-
raise VALGRIND_REPORT_MSG
|
28
|
-
end
|
29
|
-
end
|
23
|
+
report_valgrind_errors
|
30
24
|
|
31
25
|
yield ok, res if block_given?
|
32
26
|
end
|
33
27
|
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def parse_valgrind_output
|
38
|
-
@errors = []
|
39
|
-
|
40
|
-
xml = Nokogiri::XML(configuration.valgrind_xml_file.read)
|
41
|
-
xml.xpath("/valgrindoutput/error").each do |error_xml|
|
42
|
-
error = ValgrindError.new(configuration, error_xml)
|
43
|
-
next if error.skip?
|
44
|
-
@errors << error
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def output_valgrind_errors
|
49
|
-
@errors.each do |error|
|
50
|
-
configuration.output_io.puts error
|
51
|
-
configuration.output_io.puts
|
52
|
-
end
|
53
|
-
end
|
54
28
|
end
|
55
29
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyMemcheck
|
4
|
+
module TestTaskReporter
|
5
|
+
VALGRIND_REPORT_MSG = "Valgrind reported errors (e.g. memory leak or use-after-free)"
|
6
|
+
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def report_valgrind_errors
|
12
|
+
if configuration.valgrind_xml_dir
|
13
|
+
parse_valgrind_output
|
14
|
+
remove_valgrind_xml_files
|
15
|
+
|
16
|
+
unless errors.empty?
|
17
|
+
output_valgrind_errors
|
18
|
+
raise VALGRIND_REPORT_MSG
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_valgrind_output
|
24
|
+
require "nokogiri"
|
25
|
+
|
26
|
+
@errors = []
|
27
|
+
|
28
|
+
Dir[File.join(configuration.valgrind_xml_dir, "*")].each do |file|
|
29
|
+
Nokogiri::XML::Reader(File.open(file)).each do |node|
|
30
|
+
next unless node.name == "error" && node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
|
31
|
+
error_xml = Nokogiri::XML::Document.parse(node.outer_xml).root
|
32
|
+
error = ValgrindError.new(configuration, error_xml)
|
33
|
+
next if error.skip?
|
34
|
+
@errors << error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_valgrind_errors
|
40
|
+
@errors.each do |error|
|
41
|
+
configuration.output_io.puts error
|
42
|
+
configuration.output_io.puts
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_valgrind_xml_files
|
47
|
+
FileUtils.rm_rf(configuration.valgrind_xml_dir)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
module RubyMemcheck
|
4
4
|
class ValgrindError
|
5
|
-
|
5
|
+
SUPPRESSION_NOT_CONFIGURED_ERROR_MSG =
|
6
|
+
"Please enable suppressions by configuring with valgrind_generate_suppressions set to true"
|
7
|
+
|
8
|
+
attr_reader :kind, :msg, :stack, :suppression
|
6
9
|
|
7
10
|
def initialize(configuration, error)
|
8
11
|
@kind = error.at_xpath("kind").content
|
@@ -14,14 +17,17 @@ module RubyMemcheck
|
|
14
17
|
end
|
15
18
|
@stack = Stack.new(configuration, error.at_xpath("stack"))
|
16
19
|
@configuration = configuration
|
20
|
+
|
21
|
+
suppression_node = error.at_xpath("suppression")
|
22
|
+
if configuration.valgrind_generate_suppressions?
|
23
|
+
@suppression = Suppression.new(configuration, suppression_node)
|
24
|
+
elsif suppression_node
|
25
|
+
raise SUPPRESSION_NOT_CONFIGURED_ERROR_MSG
|
26
|
+
end
|
17
27
|
end
|
18
28
|
|
19
29
|
def skip?
|
20
|
-
|
21
|
-
@configuration.skip_stack?(stack)
|
22
|
-
else
|
23
|
-
false
|
24
|
-
end
|
30
|
+
should_filter? && stack.skip?
|
25
31
|
end
|
26
32
|
|
27
33
|
def to_s
|
@@ -34,6 +40,7 @@ module RubyMemcheck
|
|
34
40
|
" #{frame}\n"
|
35
41
|
end
|
36
42
|
end
|
43
|
+
str << suppression.to_s if suppression
|
37
44
|
str.string
|
38
45
|
end
|
39
46
|
|
data/lib/ruby_memcheck.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "tempfile"
|
4
|
-
require "nokogiri"
|
5
4
|
require "rake/testtask"
|
6
5
|
|
7
6
|
require "ruby_memcheck/configuration"
|
8
7
|
require "ruby_memcheck/frame"
|
9
8
|
require "ruby_memcheck/stack"
|
9
|
+
require "ruby_memcheck/test_task_reporter"
|
10
10
|
require "ruby_memcheck/test_task"
|
11
11
|
require "ruby_memcheck/valgrind_error"
|
12
|
+
require "ruby_memcheck/suppression"
|
12
13
|
require "ruby_memcheck/version"
|
13
14
|
|
14
15
|
module RubyMemcheck
|
data/ruby_memcheck.gemspec
CHANGED
@@ -27,8 +27,10 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency("nokogiri")
|
28
28
|
|
29
29
|
spec.add_development_dependency("minitest", "~> 5.0")
|
30
|
+
spec.add_development_dependency("minitest-parallel_fork", "~> 1.2")
|
30
31
|
spec.add_development_dependency("rake", "~> 13.0")
|
31
32
|
spec.add_development_dependency("rake-compiler", "~> 1.1")
|
33
|
+
spec.add_development_dependency("rspec-core")
|
32
34
|
spec.add_development_dependency("rubocop", "~> 1.22")
|
33
35
|
spec.add_development_dependency("rubocop-shopify", "~> 2.3")
|
34
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_memcheck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Zhu
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest-parallel_fork
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.2'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rake
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +80,20 @@ dependencies:
|
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '1.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-core
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: rubocop
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,7 +122,7 @@ dependencies:
|
|
94
122
|
- - "~>"
|
95
123
|
- !ruby/object:Gem::Version
|
96
124
|
version: '2.3'
|
97
|
-
description:
|
125
|
+
description:
|
98
126
|
email:
|
99
127
|
- peter@peterzhu.ca
|
100
128
|
executables: []
|
@@ -112,8 +140,11 @@ files:
|
|
112
140
|
- lib/ruby_memcheck.rb
|
113
141
|
- lib/ruby_memcheck/configuration.rb
|
114
142
|
- lib/ruby_memcheck/frame.rb
|
143
|
+
- lib/ruby_memcheck/rspec/rake_task.rb
|
115
144
|
- lib/ruby_memcheck/stack.rb
|
145
|
+
- lib/ruby_memcheck/suppression.rb
|
116
146
|
- lib/ruby_memcheck/test_task.rb
|
147
|
+
- lib/ruby_memcheck/test_task_reporter.rb
|
117
148
|
- lib/ruby_memcheck/valgrind_error.rb
|
118
149
|
- lib/ruby_memcheck/version.rb
|
119
150
|
- ruby_memcheck.gemspec
|
@@ -122,7 +153,7 @@ licenses:
|
|
122
153
|
- MIT
|
123
154
|
metadata:
|
124
155
|
homepage_uri: https://github.com/peterzhu2118/ruby_memcheck
|
125
|
-
post_install_message:
|
156
|
+
post_install_message:
|
126
157
|
rdoc_options: []
|
127
158
|
require_paths:
|
128
159
|
- lib
|
@@ -137,8 +168,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
168
|
- !ruby/object:Gem::Version
|
138
169
|
version: '0'
|
139
170
|
requirements: []
|
140
|
-
rubygems_version: 3.2.
|
141
|
-
signing_key:
|
171
|
+
rubygems_version: 3.2.22
|
172
|
+
signing_key:
|
142
173
|
specification_version: 4
|
143
174
|
summary: Use Valgrind memcheck without going crazy
|
144
175
|
test_files: []
|