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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36702507c9914c94dfb3218eff88eb04baedbfd19e17773b0cc0783003fbd773
4
- data.tar.gz: 447062d601138e3640117983a523380f455798f921294d63f61e72da55c1a620
3
+ metadata.gz: dea36221189a4ea796d262870a9a09acf48fcfe3b6321814fc3e86cc1f4c37db
4
+ data.tar.gz: '059ab396fc3c0e928046dd3f5c0e6271a616a369f427ee8d134521bb934e8cd8'
5
5
  SHA512:
6
- metadata.gz: 35cf530186236d02baa145fec37e519f24eb3322334154559af3f938b955c648ad8eb298b830e83d9ab1751842e49b59aa52f8c7b2e350c7f1fec77cd065d28a
7
- data.tar.gz: a4d24d2a9c3f985fdbdec38c3d14b9dcb0db816aef70c2f590565c5a5e81beb5ffc353a56c220f814f3f135ee1d5a4a6c1e100b1a95b080a351eace0d3f0c6b6
6
+ metadata.gz: 69a830fb9ac126817342cc84e70f0796417d119ae337172a6b79a26d10e78a08146a4496915c384c7f993e8c514d43168e8e9d1d925acb856ca77092bdd1535e
7
+ data.tar.gz: 6de39e21847f551fac3b53107e5fa9d8cba5c6d00cfd39fb104432bc44f83008afefe9c46138ce4560e91880f245456b00f60718593198d735a379e4d93b8d71
@@ -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
 
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 TODO: Write your name
1
+ Copyright (c) 2013 Sam Saffron
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
- [![Build Status](https://travis-ci.org/SamSaffron/memory_profiler.svg?branch=master)](https://travis-ci.org/SamSaffron/memory_profiler)
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.1.0 and above.
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
- The report method can take a few options:
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
- Also you can print report to file. For this use `pretty_print` method with `to_file` option and `path_to_your_log_file` string:
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
 
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+
6
+ require "memory_profiler"
7
+
8
+ exit MemoryProfiler::CLI.new.run(ARGV)
@@ -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={},&block)
13
- Reporter.report(opts,&block)
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".freeze
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".freeze
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
- def lookup_class_name(klass)
31
- @class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || '<<Unknown>>').to_s
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
- @string_cache[obj] ||= String.new << obj
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MemoryProfiler
2
4
 
3
5
  class Monochrome
@@ -15,4 +17,4 @@ module MemoryProfiler
15
17
  end
16
18
  end
17
19
 
18
- end
20
+ 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
- gray(text)
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
- gray(text)
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
- block.call
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)".freeze
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.class rescue nil
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
- string = klass == String ? helper.lookup_string(obj) : nil
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
- ["allocated", "retained"].product(["objects", "memory"]).each do |type, metric|
9
- attr_accessor "#{type}_#{metric}_by_#{name}"
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.values.map!(&:memsize).inject(0, :+)
60
+ self.total_allocated_memsize = total_memsize(allocated)
43
61
  self.total_retained = retained.size
44
- self.total_retained_memsize = retained.values.map!(&:memsize).inject(0, :+)
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
- data.values
51
- .keep_if { |stat| stat.string_value }
52
- .map! { |stat| [stat.string_value, stat.location] }
53
- .group_by { |string, _location| string }
54
- .sort_by {|string, list| [-list.size, string] }
55
- .first(top)
56
- .map { |string, list| [string, list.group_by { |_string, location| location }
57
- .map { |location, locations| [location, locations.size] }
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
- io.puts "Total allocated: #{total_allocated_memsize} bytes (#{total_allocated} objects)"
80
- io.puts "Total retained: #{total_retained_memsize} bytes (#{total_retained} objects)"
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
- if options[:detailed_report] != false
83
- io.puts
84
- ["allocated", "retained"]
85
- .product(["memory", "objects"])
86
- .product(["gem", "file", "location", "class"])
87
- .each do |(type, metric), name|
88
- dump "#{type} #{metric} by #{name}", self.send("#{type}_#{metric}_by_#{name}"), io
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
- dump_strings(io, "Allocated", strings_allocated, limit: options[:allocated_strings])
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
- private
101
-
102
- def dump_strings(io, title, strings, limit: nil)
103
- return unless strings
104
-
105
- if limit
106
- return if limit == 0
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
- io.puts "#{title} String Report"
111
- io.puts @colorize.line("-----------------------------------")
112
- strings.each do |string, stats|
113
- io.puts "#{stats.reduce(0) { |a, b| a + b[1] }.to_s.rjust(10)} #{@colorize.string((string[0..200].inspect))}"
114
- stats.sort_by { |x, y| [-y, x] }.each do |location, count|
115
- io.puts "#{@colorize.path(count.to_s.rjust(10))} #{location}"
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
- def dump(description, data, io)
123
- io.puts description
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
- io.puts "#{item[:count].to_s.rjust(10)} #{item[:data]}"
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
- io.puts
211
+
212
+ nil
133
213
  end
134
214
 
135
- end
215
+ def dump_strings(io, type, options)
216
+ strings = self.send("strings_#{type}") || []
217
+ return if strings.empty?
136
218
 
137
- end
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MemoryProfiler
2
4
  class Stat
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MemoryProfiler
2
4
  class StatHash < Hash
3
5
  include TopN
@@ -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
- stat_totals = self.values.group_by(&metric_method).map do |metric, stats|
8
- [metric, stats.reduce(0) { |sum, stat| sum + stat.memsize }, stats.size]
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 = stat_totals.sort_by! { |metric, memsize, _count| [-memsize, metric] }.take(max).
12
- map! { |metric, memsize, _count| { data: metric, count: memsize } }
13
- stats_by_count = stat_totals.sort_by! { |metric, _memsize, count| [-count, metric] }.take(max).
14
- map! { |metric, _memsize, count| { data: metric, count: count } }
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
- [stats_by_memsize, stats_by_count]
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MemoryProfiler
2
- VERSION = "0.9.10"
4
+ VERSION = "1.0.0"
3
5
  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.9.10
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: 2018-02-15 00:00:00.000000000 Z
11
+ date: 2020-12-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Memory profiling routines for Ruby 2.1+
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.1.0
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
- rubyforge_project:
53
- rubygems_version: 2.7.3
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.1+
58
+ summary: Memory profiling routines for Ruby 2.5+
57
59
  test_files: []