memory_profiler 0.9.10 → 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/CHANGELOG.md +26 -0
- data/LICENSE.txt +1 -1
- data/README.md +44 -7
- data/bin/ruby-memory-profiler +8 -0
- data/lib/memory_profiler.rb +6 -5
- data/lib/memory_profiler/cli.rb +149 -0
- data/lib/memory_profiler/helpers.rb +38 -6
- data/lib/memory_profiler/monochrome.rb +3 -1
- data/lib/memory_profiler/polychrome.rb +4 -2
- data/lib/memory_profiler/reporter.rb +14 -11
- data/lib/memory_profiler/results.rb +153 -49
- data/lib/memory_profiler/stat.rb +2 -0
- data/lib/memory_profiler/stat_hash.rb +2 -0
- data/lib/memory_profiler/top_n.rb +23 -7
- data/lib/memory_profiler/version.rb +3 -1
- metadata +13 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dea36221189a4ea796d262870a9a09acf48fcfe3b6321814fc3e86cc1f4c37db
|
4
|
+
data.tar.gz: '059ab396fc3c0e928046dd3f5c0e6271a616a369f427ee8d134521bb934e8cd8'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69a830fb9ac126817342cc84e70f0796417d119ae337172a6b79a26d10e78a08146a4496915c384c7f993e8c514d43168e8e9d1d925acb856ca77092bdd1535e
|
7
|
+
data.tar.gz: 6de39e21847f551fac3b53107e5fa9d8cba5c6d00cfd39fb104432bc44f83008afefe9c46138ce4560e91880f245456b00f60718593198d735a379e4d93b8d71
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.0.0 - 02-12-2020
|
4
|
+
|
5
|
+
- Added new CLI `ruby-memory-profiler` which can be used to profile scripts @fatkodima
|
6
|
+
- Reduced memory usage when generating reports
|
7
|
+
- Some optimizations for Ruby 2.7
|
8
|
+
- Remove EOL Rubies: 2.3 and 2.4 are no longer supported (use an earlier version of the gem if needed)
|
9
|
+
|
10
|
+
## 0.9.14 - 28-06-2019
|
11
|
+
|
12
|
+
- Pass 'normalize_path: true' to pretty_print to have locations stripped
|
13
|
+
- Improve number formatting
|
14
|
+
|
15
|
+
## 0.9.13 - 22-03-2019
|
16
|
+
|
17
|
+
- remove support explicitly for all EOL rubies, 2.1 and 2.2
|
18
|
+
- frozen string literal comment @RST-J
|
19
|
+
- scale_bytes option @RST-J
|
20
|
+
|
21
|
+
## 0.9.12
|
22
|
+
- Correct bug under-reporting memory for large string allocation @sam
|
23
|
+
|
24
|
+
## 0.9.11
|
25
|
+
- Reduce memory needed for string allocation tracing @dgynn
|
26
|
+
- Use yield rather than block.call to reduce an allocation @dgynn
|
27
|
+
- Ensure string allocation locations sort consistently @dgynn
|
28
|
+
|
3
29
|
## 0.9.10
|
4
30
|
- Add better detection for stdlib "gems"
|
5
31
|
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
[![
|
1
|
+
[![CI](https://github.com/SamSaffron/memory_profiler/workflows/CI/badge.svg)](https://github.com/SamSaffron/memory_profiler/actions?query=workflow%3ACI)
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/memory_profiler.svg)](https://rubygems.org/gems/memory_profiler)
|
3
3
|
|
4
4
|
# MemoryProfiler
|
5
5
|
|
6
|
-
A memory profiler for Ruby
|
6
|
+
A memory profiler for Ruby
|
7
7
|
|
8
|
-
## Requirements
|
8
|
+
## Requirements
|
9
9
|
|
10
|
-
Ruby(MRI) Version 2.
|
10
|
+
Ruby(MRI) Version 2.5.0 and above.
|
11
11
|
|
12
12
|
## Installation
|
13
13
|
|
@@ -25,6 +25,25 @@ Or install it yourself as:
|
|
25
25
|
|
26
26
|
## Usage
|
27
27
|
|
28
|
+
There are two ways to use `memory_profiler`:
|
29
|
+
* command line
|
30
|
+
* convenience API
|
31
|
+
|
32
|
+
### Command Line
|
33
|
+
|
34
|
+
The easiest way to use memory_profiler is via the command line, which requires no modifications to your program. The basic usage is:
|
35
|
+
```
|
36
|
+
$ ruby-memory-profiler [options] <script.rb> [--] [script-options]
|
37
|
+
```
|
38
|
+
Where `script.rb` is the program you want to profile.
|
39
|
+
|
40
|
+
For a full list of options, execute the following command:
|
41
|
+
```
|
42
|
+
ruby-memory-profiler -h
|
43
|
+
```
|
44
|
+
|
45
|
+
### Convenience API
|
46
|
+
|
28
47
|
```ruby
|
29
48
|
require 'memory_profiler'
|
30
49
|
report = MemoryProfiler.report do
|
@@ -52,7 +71,9 @@ be the only time you can retrieve the report using this API.
|
|
52
71
|
|
53
72
|
## Options
|
54
73
|
|
55
|
-
|
74
|
+
### `report`
|
75
|
+
|
76
|
+
The `report` method can take a few options:
|
56
77
|
|
57
78
|
* `top`: maximum number of entries to display in a report (default is 50)
|
58
79
|
* `allow_files`: include only certain files from tracing - can be given as a String, Regexp, or array of Strings
|
@@ -84,7 +105,23 @@ allocated memory by file
|
|
84
105
|
. . .
|
85
106
|
```
|
86
107
|
|
87
|
-
|
108
|
+
### `pretty_print`
|
109
|
+
|
110
|
+
The `pretty_print` method can take a few options:
|
111
|
+
|
112
|
+
* `to_file`: a path to your log file - can be given a String
|
113
|
+
* `color_output`: a flag for whether to colorize output - can be given a Boolean
|
114
|
+
* `retained_strings`: how many retained strings to print - can be given an Integer
|
115
|
+
* `allocated_strings`: how many allocated strings to print - can be given a String
|
116
|
+
* `detailed_report`: should report include detailed information - can be given a Boolean
|
117
|
+
* `scale_bytes`: flag to convert byte units (e.g. 183200000 is reported as 183.2 MB, rounds with a precision of 2 decimal digits) - can be given a Boolean
|
118
|
+
* `normalize_paths`: flag to remove a gem's directory path from printed locations - can be given a Boolean
|
119
|
+
*Note: normalized path of a "location" from Ruby's stdlib will be prefixed with `ruby/lib/`. e.g.: `ruby/lib/set.rb`, `ruby/lib/pathname.rb`, etc.*
|
120
|
+
|
121
|
+
|
122
|
+
Check out `Results#pretty_print` for more details.
|
123
|
+
|
124
|
+
For example to report to file, use `pretty_print` method with `to_file` option and `path_to_your_log_file` string:
|
88
125
|
```
|
89
126
|
$ pry
|
90
127
|
pry> require 'memory_profiler'
|
@@ -364,7 +401,7 @@ The report breaks down 2 key concepts.
|
|
364
401
|
|
365
402
|
**Allocated**: All object allocation and memory allocation during code block.
|
366
403
|
|
367
|
-
As a general rule "retained" will always be smaller than or equal to allocated.
|
404
|
+
As a general rule "retained" will always be smaller than or equal to allocated.
|
368
405
|
|
369
406
|
Memory profiler will tell you aggregate costs of the above, for example requiring the mime-types gem above results in approx 2MB of retained memory in 22K or so objects. The actual RSS cost will always be slightly higher as MRI heaps are not squashed to size and memory fragments. In future we may be able to calculate a rough long term GC cost of retained objects (for major GCs).
|
370
407
|
|
data/lib/memory_profiler.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "memory_profiler/version"
|
2
4
|
require "memory_profiler/helpers"
|
3
5
|
require "memory_profiler/polychrome"
|
@@ -7,13 +9,14 @@ require "memory_profiler/stat"
|
|
7
9
|
require "memory_profiler/stat_hash"
|
8
10
|
require "memory_profiler/results"
|
9
11
|
require "memory_profiler/reporter"
|
12
|
+
require "memory_profiler/cli"
|
10
13
|
|
11
14
|
module MemoryProfiler
|
12
|
-
def self.report(opts={}
|
13
|
-
Reporter.report(opts
|
15
|
+
def self.report(opts = {}, &block)
|
16
|
+
Reporter.report(opts, &block)
|
14
17
|
end
|
15
18
|
|
16
|
-
def self.start(opts={})
|
19
|
+
def self.start(opts = {})
|
17
20
|
unless Reporter.current_reporter
|
18
21
|
Reporter.current_reporter = Reporter.new(opts)
|
19
22
|
Reporter.current_reporter.start
|
@@ -26,5 +29,3 @@ module MemoryProfiler
|
|
26
29
|
Reporter.current_reporter = nil
|
27
30
|
end
|
28
31
|
end
|
29
|
-
|
30
|
-
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
module MemoryProfiler
|
6
|
+
class CLI
|
7
|
+
BIN_NAME = "ruby-memory-profiler"
|
8
|
+
VERSION_INFO = "#{BIN_NAME} #{MemoryProfiler::VERSION}"
|
9
|
+
|
10
|
+
STATUS_SUCCESS = 0
|
11
|
+
STATUS_ERROR = 1
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
ignore_files: "memory_profiler/lib"
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
REPORTER_KEYS = [
|
18
|
+
:top, :trace, :ignore_files, :allow_files
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
RESULTS_KEYS = [
|
22
|
+
:to_file, :color_output, :retained_strings, :allocated_strings,
|
23
|
+
:detailed_report, :scale_bytes, :normalize_paths
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
private_constant :BIN_NAME, :VERSION_INFO,:STATUS_SUCCESS, :STATUS_ERROR,
|
27
|
+
:DEFAULTS, :REPORTER_KEYS, :RESULTS_KEYS
|
28
|
+
|
29
|
+
#
|
30
|
+
|
31
|
+
def run(argv)
|
32
|
+
options = {}
|
33
|
+
parser = option_parser(options)
|
34
|
+
parser.parse!(argv)
|
35
|
+
|
36
|
+
options = DEFAULTS.merge(options)
|
37
|
+
|
38
|
+
# Make sure the user specified at least one file
|
39
|
+
unless (script = argv.shift)
|
40
|
+
puts parser
|
41
|
+
puts ""
|
42
|
+
puts "#{VERSION_INFO} | ERROR: Must specify a script to run"
|
43
|
+
return STATUS_ERROR
|
44
|
+
end
|
45
|
+
|
46
|
+
MemoryProfiler.start(reporter_options(options))
|
47
|
+
load script
|
48
|
+
|
49
|
+
STATUS_SUCCESS
|
50
|
+
rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, OptionParser::MissingArgument => e
|
51
|
+
puts parser
|
52
|
+
puts e.message
|
53
|
+
STATUS_ERROR
|
54
|
+
ensure
|
55
|
+
report = MemoryProfiler.stop
|
56
|
+
report&.pretty_print(**results_options(options))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def option_parser(options)
|
62
|
+
OptionParser.new do |opts|
|
63
|
+
opts.banner = <<~BANNER
|
64
|
+
|
65
|
+
#{VERSION_INFO}
|
66
|
+
A Memory Profiler for Ruby
|
67
|
+
|
68
|
+
Usage:
|
69
|
+
#{BIN_NAME} [options] <script.rb> [--] [script-options]
|
70
|
+
BANNER
|
71
|
+
|
72
|
+
opts.separator ""
|
73
|
+
opts.separator "Options:"
|
74
|
+
|
75
|
+
# Reporter options
|
76
|
+
opts.on("-m", "--max=NUM", Integer, "Max number of entries to output. (Defaults to 50)") do |arg|
|
77
|
+
options[:top] = arg
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on("--classes=CLASSES", Array, "A class or list of classes you explicitly want to trace.") do |arg|
|
81
|
+
options[:trace] = arg.map { |klass| Object.const_get(klass) }
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on("--ignore-files=REGEXP", "A regular expression used to exclude certain files from tracing.") do |arg|
|
85
|
+
options[:ignore_files] = "#{arg}|memory_profiler/lib"
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on("--allow-files=FILES", Array, "A string or list of strings to selectively include in tracing.") do |arg|
|
89
|
+
options[:allow_files] = arg
|
90
|
+
end
|
91
|
+
|
92
|
+
opts.separator ""
|
93
|
+
|
94
|
+
# Results options
|
95
|
+
opts.on("-o", "--out=FILE", "Write output to a file instead of STDOUT.") do |arg|
|
96
|
+
options[:to_file] = arg
|
97
|
+
end
|
98
|
+
|
99
|
+
opts.on("--[no-]color", "Force color output on or off. (Enabled by default)") do |arg|
|
100
|
+
options[:color_output] = arg
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on("--retained-strings=NUM", Integer, "How many retained strings to print.") do |arg|
|
104
|
+
options[:retained_strings] = arg
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.on("--allocated-strings=NUM", Integer, "How many allocated strings to print.") do |arg|
|
108
|
+
options[:allocated_strings] = arg
|
109
|
+
end
|
110
|
+
|
111
|
+
opts.on("--[no-]detailed", "Print detailed information. (Enabled by default)") do |arg|
|
112
|
+
options[:detailed_report] = arg
|
113
|
+
end
|
114
|
+
|
115
|
+
opts.on("--scale-bytes", "Calculates unit prefixes for the numbers of bytes.") do
|
116
|
+
options[:scale_bytes] = true
|
117
|
+
end
|
118
|
+
|
119
|
+
opts.on("--normalize-paths", "Print location paths relative to gem's source directory.") do
|
120
|
+
options[:normalize_paths] = true
|
121
|
+
end
|
122
|
+
|
123
|
+
opts.on("--pretty", "Easily enable options 'scale-bytes' and 'normalize-paths'") do
|
124
|
+
options[:scale_bytes] = options[:normalize_paths] = true
|
125
|
+
end
|
126
|
+
|
127
|
+
opts.separator ""
|
128
|
+
|
129
|
+
opts.on_tail("-h", "--help", "Show this help message.") do
|
130
|
+
puts opts
|
131
|
+
exit
|
132
|
+
end
|
133
|
+
|
134
|
+
opts.on_tail("-v", "--version", "Show program version.") do
|
135
|
+
puts VERSION_INFO
|
136
|
+
exit
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def reporter_options(options)
|
142
|
+
options.select { |k, _v| REPORTER_KEYS.include?(k) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def results_options(options)
|
146
|
+
options.select { |k, _v| RESULTS_KEYS.include?(k) }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MemoryProfiler
|
2
4
|
class Helpers
|
3
5
|
|
4
6
|
def initialize
|
5
7
|
@gem_guess_cache = Hash.new
|
6
|
-
@location_cache = Hash.new { |h,k| h[k] = Hash.new.compare_by_identity }
|
8
|
+
@location_cache = Hash.new { |h, k| h[k] = Hash.new.compare_by_identity }
|
7
9
|
@class_name_cache = Hash.new.compare_by_identity
|
8
10
|
@string_cache = Hash.new
|
9
11
|
end
|
@@ -13,13 +15,13 @@ module MemoryProfiler
|
|
13
15
|
if /(\/gems\/.*)*\/gems\/(?<gemname>[^\/]+)/ =~ path
|
14
16
|
gemname
|
15
17
|
elsif /\/rubygems[\.\/]/ =~ path
|
16
|
-
"rubygems"
|
18
|
+
"rubygems"
|
17
19
|
elsif /ruby\/2\.[^\/]+\/(?<stdlib>[^\/\.]+)/ =~ path
|
18
20
|
stdlib
|
19
21
|
elsif /(?<app>[^\/]+\/(bin|app|lib))/ =~ path
|
20
22
|
app
|
21
23
|
else
|
22
|
-
"other"
|
24
|
+
"other"
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
@@ -27,12 +29,42 @@ module MemoryProfiler
|
|
27
29
|
@location_cache[file][line] ||= "#{file}:#{line}"
|
28
30
|
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
+
KERNEL_CLASS_METHOD = Kernel.instance_method(:class)
|
33
|
+
if UnboundMethod.method_defined?(:bind_call)
|
34
|
+
def object_class(obj)
|
35
|
+
klass = obj.class rescue nil
|
36
|
+
unless Class === klass
|
37
|
+
# attempt to determine the true Class when .class returns something other than a Class
|
38
|
+
klass = KERNEL_CLASS_METHOD.bind_call(obj)
|
39
|
+
end
|
40
|
+
klass
|
41
|
+
end
|
42
|
+
else
|
43
|
+
def object_class(obj)
|
44
|
+
klass = obj.class rescue nil
|
45
|
+
unless Class === klass
|
46
|
+
# attempt to determine the true Class when .class returns something other than a Class
|
47
|
+
klass = KERNEL_CLASS_METHOD.bind(obj).call
|
48
|
+
end
|
49
|
+
klass
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if Object.name.frozen? # Since Ruby 2.7 Module#name no longer allocate a new string
|
54
|
+
def lookup_class_name(klass)
|
55
|
+
((klass.is_a?(Class) && klass.name) || '<<Unknown>>').to_s
|
56
|
+
end
|
57
|
+
else
|
58
|
+
def lookup_class_name(klass)
|
59
|
+
@class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || '<<Unknown>>').to_s
|
60
|
+
end
|
32
61
|
end
|
33
62
|
|
34
63
|
def lookup_string(obj)
|
35
|
-
|
64
|
+
# This string is shortened to 200 characters which is what the string report shows
|
65
|
+
# The string report can still list unique strings longer than 200 characters
|
66
|
+
# separately because the object_id of the shortened string will be different
|
67
|
+
@string_cache[obj] ||= String.new << obj[0, 200]
|
36
68
|
end
|
37
69
|
end
|
38
70
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MemoryProfiler
|
2
4
|
|
3
5
|
class Polychrome
|
4
6
|
|
5
7
|
def path(text)
|
6
|
-
|
8
|
+
blue(text)
|
7
9
|
end
|
8
10
|
|
9
11
|
def string(text)
|
@@ -11,7 +13,7 @@ module MemoryProfiler
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def line(text)
|
14
|
-
|
16
|
+
cyan(text)
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'objspace'
|
2
4
|
module MemoryProfiler
|
3
5
|
# Reporter is the top level API used for generating memory reports.
|
@@ -27,11 +29,13 @@ module MemoryProfiler
|
|
27
29
|
# @option opts :ignore_files a regular expression used to exclude certain files from tracing
|
28
30
|
# @option opts :allow_files a string or array of strings to selectively include in tracing
|
29
31
|
# @return [MemoryProfiler::Results]
|
30
|
-
def self.report(opts={}, &block)
|
32
|
+
def self.report(opts = {}, &block)
|
31
33
|
self.new(opts).run(&block)
|
32
34
|
end
|
33
35
|
|
34
36
|
def start
|
37
|
+
GC.start
|
38
|
+
GC.start
|
35
39
|
GC.start
|
36
40
|
GC.disable
|
37
41
|
|
@@ -46,6 +50,8 @@ module MemoryProfiler
|
|
46
50
|
|
47
51
|
GC.enable
|
48
52
|
GC.start
|
53
|
+
GC.start
|
54
|
+
GC.start
|
49
55
|
|
50
56
|
# Caution: Do not allocate any new Objects between the call to GC.start and the completion of the retained
|
51
57
|
# lookups. It is likely that a new Object would reuse an object_id from a GC'd object.
|
@@ -65,7 +71,7 @@ module MemoryProfiler
|
|
65
71
|
def run(&block)
|
66
72
|
start
|
67
73
|
begin
|
68
|
-
|
74
|
+
yield
|
69
75
|
rescue Exception
|
70
76
|
ObjectSpace.trace_object_allocations_stop
|
71
77
|
GC.enable
|
@@ -82,7 +88,6 @@ module MemoryProfiler
|
|
82
88
|
def object_list(generation)
|
83
89
|
|
84
90
|
rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
|
85
|
-
rvalue_size_adjustment = RUBY_VERSION < '2.2' ? rvalue_size : 0
|
86
91
|
helper = Helpers.new
|
87
92
|
|
88
93
|
result = StatHash.new.compare_by_identity
|
@@ -90,15 +95,11 @@ module MemoryProfiler
|
|
90
95
|
ObjectSpace.each_object do |obj|
|
91
96
|
next unless ObjectSpace.allocation_generation(obj) == generation
|
92
97
|
|
93
|
-
file = ObjectSpace.allocation_sourcefile(obj) || "(no name)"
|
98
|
+
file = ObjectSpace.allocation_sourcefile(obj) || "(no name)"
|
94
99
|
next if @ignore_files && @ignore_files =~ file
|
95
100
|
next if @allow_files && !(@allow_files =~ file)
|
96
101
|
|
97
|
-
klass = obj
|
98
|
-
unless Class === klass
|
99
|
-
# attempt to determine the true Class when .class returns something other than a Class
|
100
|
-
klass = Kernel.instance_method(:class).bind(obj).call
|
101
|
-
end
|
102
|
+
klass = helper.object_class(obj)
|
102
103
|
next if @trace && !trace.include?(klass)
|
103
104
|
|
104
105
|
begin
|
@@ -107,9 +108,11 @@ module MemoryProfiler
|
|
107
108
|
class_name = helper.lookup_class_name(klass)
|
108
109
|
gem = helper.guess_gem(file)
|
109
110
|
|
110
|
-
|
111
|
+
# we do memsize first to avoid freezing as a side effect and shifting
|
112
|
+
# storage to the new frozen string, this happens on @hash[s] in lookup_string
|
113
|
+
memsize = ObjectSpace.memsize_of(obj)
|
114
|
+
string = klass == String ? helper.lookup_string(obj) : nil
|
111
115
|
|
112
|
-
memsize = ObjectSpace.memsize_of(obj) + rvalue_size_adjustment
|
113
116
|
# compensate for API bug
|
114
117
|
memsize = rvalue_size if memsize > 100_000_000_000
|
115
118
|
result[obj.__id__] = MemoryProfiler::Stat.new(class_name, gem, file, location, memsize, string)
|
@@ -1,12 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MemoryProfiler
|
2
4
|
class Results
|
5
|
+
UNIT_PREFIXES = {
|
6
|
+
0 => 'B',
|
7
|
+
3 => 'kB',
|
8
|
+
6 => 'MB',
|
9
|
+
9 => 'GB',
|
10
|
+
12 => 'TB',
|
11
|
+
15 => 'PB',
|
12
|
+
18 => 'EB',
|
13
|
+
21 => 'ZB',
|
14
|
+
24 => 'YB'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
TYPES = ["allocated", "retained"].freeze
|
18
|
+
METRICS = ["memory", "objects"].freeze
|
19
|
+
NAMES = ["gem", "file", "location", "class"].freeze
|
3
20
|
|
4
21
|
def self.register_type(name, stat_attribute)
|
5
22
|
@@lookups ||= []
|
6
23
|
@@lookups << [name, stat_attribute]
|
7
24
|
|
8
|
-
|
9
|
-
|
25
|
+
TYPES.each do |type|
|
26
|
+
METRICS.each do |metric|
|
27
|
+
attr_accessor "#{type}_#{metric}_by_#{name}"
|
28
|
+
end
|
10
29
|
end
|
11
30
|
end
|
12
31
|
|
@@ -34,29 +53,52 @@ module MemoryProfiler
|
|
34
53
|
self.send("retained_objects_by_#{name}=", count_results)
|
35
54
|
end
|
36
55
|
|
37
|
-
|
38
56
|
self.strings_allocated = string_report(allocated, top)
|
39
57
|
self.strings_retained = string_report(retained, top)
|
40
58
|
|
41
59
|
self.total_allocated = allocated.size
|
42
|
-
self.total_allocated_memsize = allocated
|
60
|
+
self.total_allocated_memsize = total_memsize(allocated)
|
43
61
|
self.total_retained = retained.size
|
44
|
-
self.total_retained_memsize = retained
|
62
|
+
self.total_retained_memsize = total_memsize(retained)
|
45
63
|
|
46
64
|
self
|
47
65
|
end
|
48
66
|
|
67
|
+
def scale_bytes(bytes)
|
68
|
+
return "0 B" if bytes.zero?
|
69
|
+
|
70
|
+
scale = Math.log10(bytes).div(3) * 3
|
71
|
+
scale = 24 if scale > 24
|
72
|
+
"%.2f #{UNIT_PREFIXES[scale]}" % (bytes / 10.0**scale)
|
73
|
+
end
|
74
|
+
|
49
75
|
def string_report(data, top)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
76
|
+
grouped_strings = Hash.new { |hash, key| hash[key] = [] }
|
77
|
+
data.each_value do |stat|
|
78
|
+
if stat.string_value
|
79
|
+
grouped_strings[stat.string_value.object_id] << stat
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
grouped_strings = grouped_strings.values
|
84
|
+
|
85
|
+
if grouped_strings.size > top
|
86
|
+
grouped_strings.sort_by!(&:size)
|
87
|
+
grouped_strings = grouped_strings.drop(grouped_strings.size - top)
|
88
|
+
end
|
89
|
+
|
90
|
+
grouped_strings
|
91
|
+
.sort! { |a, b| a.size == b.size ? a[0].string_value <=> b[0].string_value : b.size <=> a.size }
|
92
|
+
.map! do |list|
|
93
|
+
# Return array of [string, [[location, count], [location, count], ...]
|
94
|
+
[
|
95
|
+
list[0].string_value,
|
96
|
+
list.group_by { |stat| stat.location }
|
97
|
+
.map { |location, stat_list| [location, stat_list.size] }
|
98
|
+
.sort_by!(&:last)
|
99
|
+
.reverse!
|
100
|
+
]
|
101
|
+
end
|
60
102
|
end
|
61
103
|
|
62
104
|
# Output the results of the report
|
@@ -66,6 +108,8 @@ module MemoryProfiler
|
|
66
108
|
# @option opts [Integer] :retained_strings how many retained strings to print
|
67
109
|
# @option opts [Integer] :allocated_strings how many allocated strings to print
|
68
110
|
# @option opts [Boolean] :detailed_report should report include detailed information
|
111
|
+
# @option opts [Boolean] :scale_bytes calculates unit prefixes for the numbers of bytes
|
112
|
+
# @option opts [Boolean] :normalize_paths print location paths relative to gem's source directory.
|
69
113
|
def pretty_print(io = $stdout, **options)
|
70
114
|
# Handle the special case that Ruby PrettyPrint expects `pretty_print`
|
71
115
|
# to be a customized pretty printing function for a class
|
@@ -76,64 +120,124 @@ module MemoryProfiler
|
|
76
120
|
color_output = options.fetch(:color_output) { io.respond_to?(:isatty) && io.isatty }
|
77
121
|
@colorize = color_output ? Polychrome.new : Monochrome.new
|
78
122
|
|
79
|
-
|
80
|
-
|
123
|
+
if options[:scale_bytes]
|
124
|
+
total_allocated_output = scale_bytes(total_allocated_memsize)
|
125
|
+
total_retained_output = scale_bytes(total_retained_memsize)
|
126
|
+
else
|
127
|
+
total_allocated_output = "#{total_allocated_memsize} bytes"
|
128
|
+
total_retained_output = "#{total_retained_memsize} bytes"
|
129
|
+
end
|
81
130
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
131
|
+
io.puts "Total allocated: #{total_allocated_output} (#{total_allocated} objects)"
|
132
|
+
io.puts "Total retained: #{total_retained_output} (#{total_retained} objects)"
|
133
|
+
|
134
|
+
unless options[:detailed_report] == false
|
135
|
+
TYPES.each do |type|
|
136
|
+
METRICS.each do |metric|
|
137
|
+
NAMES.each do |name|
|
138
|
+
dump_data(io, type, metric, name, options)
|
89
139
|
end
|
140
|
+
end
|
141
|
+
end
|
90
142
|
end
|
91
143
|
|
92
144
|
io.puts
|
93
|
-
|
94
|
-
io.puts
|
95
|
-
dump_strings(io, "Retained", strings_retained, limit: options[:retained_strings])
|
145
|
+
print_string_reports(io, options)
|
96
146
|
|
97
147
|
io.close if io.is_a? File
|
98
148
|
end
|
99
149
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
strings = strings[0...limit]
|
150
|
+
def print_string_reports(io, options)
|
151
|
+
TYPES.each do |type|
|
152
|
+
dump_opts = {
|
153
|
+
normalize_paths: options[:normalize_paths],
|
154
|
+
limit: options["#{type}_strings".to_sym]
|
155
|
+
}
|
156
|
+
dump_strings(io, type, dump_opts)
|
108
157
|
end
|
158
|
+
end
|
109
159
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
160
|
+
def normalize_path(path)
|
161
|
+
@normalize_path ||= {}
|
162
|
+
@normalize_path[path] ||= begin
|
163
|
+
if %r!(/gems/.*)*/gems/(?<gemname>[^/]+)(?<rest>.*)! =~ path
|
164
|
+
"#{gemname}#{rest}"
|
165
|
+
elsif %r!ruby/2\.[^/]+/(?<stdlib>[^/.]+)(?<rest>.*)! =~ path
|
166
|
+
"ruby/lib/#{stdlib}#{rest}"
|
167
|
+
elsif %r!(?<app>[^/]+/(bin|app|lib))(?<rest>.*)! =~ path
|
168
|
+
"#{app}#{rest}"
|
169
|
+
else
|
170
|
+
path
|
116
171
|
end
|
117
|
-
io.puts
|
118
172
|
end
|
119
|
-
nil
|
120
173
|
end
|
121
174
|
|
122
|
-
|
123
|
-
|
175
|
+
private
|
176
|
+
|
177
|
+
def total_memsize(stat_hash)
|
178
|
+
sum = 0
|
179
|
+
stat_hash.each_value do |stat|
|
180
|
+
sum += stat.memsize
|
181
|
+
end
|
182
|
+
sum
|
183
|
+
end
|
184
|
+
|
185
|
+
def print_title(io, title)
|
186
|
+
io.puts
|
187
|
+
io.puts title
|
124
188
|
io.puts @colorize.line("-----------------------------------")
|
189
|
+
end
|
190
|
+
|
191
|
+
def print_output(io, topic, detail)
|
192
|
+
io.puts "#{@colorize.path(topic.to_s.rjust(10))} #{detail}"
|
193
|
+
end
|
194
|
+
|
195
|
+
def dump_data(io, type, metric, name, options)
|
196
|
+
print_title io, "#{type} #{metric} by #{name}"
|
197
|
+
data = self.send "#{type}_#{metric}_by_#{name}"
|
198
|
+
|
199
|
+
scale_data = metric == "memory" && options[:scale_bytes]
|
200
|
+
normalize_paths = options[:normalize_paths]
|
201
|
+
|
125
202
|
if data && !data.empty?
|
126
203
|
data.each do |item|
|
127
|
-
|
204
|
+
count = scale_data ? scale_bytes(item[:count]) : item[:count]
|
205
|
+
value = normalize_paths ? normalize_path(item[:data]) : item[:data]
|
206
|
+
print_output io, count, value
|
128
207
|
end
|
129
208
|
else
|
130
209
|
io.puts "NO DATA"
|
131
210
|
end
|
132
|
-
|
211
|
+
|
212
|
+
nil
|
133
213
|
end
|
134
214
|
|
135
|
-
|
215
|
+
def dump_strings(io, type, options)
|
216
|
+
strings = self.send("strings_#{type}") || []
|
217
|
+
return if strings.empty?
|
136
218
|
|
137
|
-
|
219
|
+
options = {} unless options.is_a?(Hash)
|
138
220
|
|
221
|
+
if (limit = options[:limit])
|
222
|
+
return if limit == 0
|
223
|
+
strings = strings[0...limit]
|
224
|
+
end
|
139
225
|
|
226
|
+
normalize_paths = options[:normalize_paths]
|
227
|
+
|
228
|
+
print_title(io, "#{type.capitalize} String Report")
|
229
|
+
strings.each do |string, stats|
|
230
|
+
print_output io, (stats.reduce(0) { |a, b| a + b[1] }), @colorize.string(string.inspect)
|
231
|
+
stats.sort_by { |x, y| [-y, x] }.each do |location, count|
|
232
|
+
location = normalize_path(location) if normalize_paths
|
233
|
+
print_output io, count, location
|
234
|
+
end
|
235
|
+
io.puts
|
236
|
+
end
|
237
|
+
|
238
|
+
nil
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
data/lib/memory_profiler/stat.rb
CHANGED
@@ -1,20 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MemoryProfiler
|
2
4
|
module TopN
|
3
5
|
# Fast approach for determining the top_n entries in a Hash of Stat objects.
|
4
6
|
# Returns results for both memory (memsize summed) and objects allocated (count) as a tuple.
|
5
7
|
def top_n(max, metric_method)
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
+
metric_memsize = Hash.new(0)
|
10
|
+
metric_objects_count = Hash.new(0)
|
11
|
+
|
12
|
+
each_value do |value|
|
13
|
+
metric = value.send(metric_method)
|
14
|
+
|
15
|
+
metric_memsize[metric] += value.memsize
|
16
|
+
metric_objects_count[metric] += 1
|
9
17
|
end
|
10
18
|
|
11
|
-
stats_by_memsize =
|
12
|
-
|
13
|
-
|
14
|
-
|
19
|
+
stats_by_memsize =
|
20
|
+
metric_memsize
|
21
|
+
.to_a
|
22
|
+
.sort_by! { |metric, memsize| [-memsize, metric] }
|
23
|
+
.take(max)
|
24
|
+
.map! { |metric, memsize| { data: metric, count: memsize } }
|
15
25
|
|
16
|
-
|
26
|
+
stats_by_count =
|
27
|
+
metric_objects_count
|
28
|
+
.to_a
|
29
|
+
.sort_by! { |metric, count| [-count, metric] }
|
30
|
+
.take(max)
|
31
|
+
.map! { |metric, count| { data: metric, count: count } }
|
17
32
|
|
33
|
+
[stats_by_memsize, stats_by_count]
|
18
34
|
end
|
19
35
|
end
|
20
36
|
end
|
metadata
CHANGED
@@ -1,26 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memory_profiler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Saffron
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: Memory profiling routines for Ruby 2.
|
13
|
+
description: Memory profiling routines for Ruby 2.5+
|
14
14
|
email:
|
15
15
|
- sam.saffron@gmail.com
|
16
|
-
executables:
|
16
|
+
executables:
|
17
|
+
- ruby-memory-profiler
|
17
18
|
extensions: []
|
18
19
|
extra_rdoc_files: []
|
19
20
|
files:
|
20
21
|
- CHANGELOG.md
|
21
22
|
- LICENSE.txt
|
22
23
|
- README.md
|
24
|
+
- bin/ruby-memory-profiler
|
23
25
|
- lib/memory_profiler.rb
|
26
|
+
- lib/memory_profiler/cli.rb
|
24
27
|
- lib/memory_profiler/helpers.rb
|
25
28
|
- lib/memory_profiler/monochrome.rb
|
26
29
|
- lib/memory_profiler/polychrome.rb
|
@@ -34,7 +37,7 @@ homepage: https://github.com/SamSaffron/memory_profiler
|
|
34
37
|
licenses:
|
35
38
|
- MIT
|
36
39
|
metadata: {}
|
37
|
-
post_install_message:
|
40
|
+
post_install_message:
|
38
41
|
rdoc_options: []
|
39
42
|
require_paths:
|
40
43
|
- lib
|
@@ -42,16 +45,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
45
|
requirements:
|
43
46
|
- - ">="
|
44
47
|
- !ruby/object:Gem::Version
|
45
|
-
version: 2.
|
48
|
+
version: 2.5.0
|
46
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
50
|
requirements:
|
48
51
|
- - ">="
|
49
52
|
- !ruby/object:Gem::Version
|
50
53
|
version: '0'
|
51
54
|
requirements: []
|
52
|
-
|
53
|
-
|
54
|
-
signing_key:
|
55
|
+
rubygems_version: 3.1.4
|
56
|
+
signing_key:
|
55
57
|
specification_version: 4
|
56
|
-
summary: Memory profiling routines for Ruby 2.
|
58
|
+
summary: Memory profiling routines for Ruby 2.5+
|
57
59
|
test_files: []
|