memory 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 116b9be7bb4a7d1c7b906923132406708953a8e3e7d43687d82f7920192ad74f
4
- data.tar.gz: bf093a25998cefbf01f8fbc79d92d23bc972c016596acb3678cd6303eb09e034
3
+ metadata.gz: 5bd34b6e9c3ddd2a10a3900d0507a8af35af246c5aa2ad760eb2a204731e6fbc
4
+ data.tar.gz: 798ec525eeba73a5e08d9f557b98727096dfde83c01aa007d83efa6f066177e9
5
5
  SHA512:
6
- metadata.gz: 403676e22f95cd9239f3da72898a9be935c100e236bd51a6fca58d2ebf472a33609efb373dff628cf5f6f1e7ed9ace28dc3abc45adbbdc9975d489539b2f853d
7
- data.tar.gz: be14f62bb150ab3daec0c9743800e887e606b0ba1545725143484362278779451f9a98dab2a35126d4396d6a57e08c1ccdce28b1a21fe5e894516b16e235b79c
6
+ metadata.gz: b39e94db71df052b83bd85fc63aa4c37801470eb3daeaae687e5d3c208e280299bfba9dee11da71a82c2d9b34f9935d1662be1e18eb6f99043816963c2cd6591
7
+ data.tar.gz: 87ef4c2aae0f96a507de45f0d60b167102ba07b05e7851af80c2e306ae6e56c3a35202c766e3202485956df8a0ca4fb14b111ade24344078e09ecdcc3cfaa56e
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1,4 @@
1
- r��[�
2
- �g�.z]�ǝ�T�6��6�I I[�㙢~���[uP��f�f^t�
3
- ��P�����@�\Gv� a��F/`���-$�h{��r 'q�oE1$.0Z�����i<��N�:"r��;�%_)����1k��V�N'(�1M {O��/����d��\_ gI4�{y��K_�ŋ���a��y�?�Wb�����J
4
- ��2>/���L�??{]������KԔrRf=�~� <X�1�Ws����':G
1
+
2
+ 5�=Iπ,�4���׼k3�Ϟ�]
3
+ ��VK��q%o¯ڄ�?i4b�@�Y_�@���:�k�9��`ȁ2dR�x����5�������I'(����W�Ѕ#(T��G40q�UtT³{H���CtJ(��hjf_�%*Q24T%�.�:#fK���GiP�Z���h���nh�ڥ�ȇwS�vƏ�;&�
4
+ �ČTk[wr��t�k�vN���&��c}RncA�J�c�`�S��`������/x�� ��H�Ř�>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ def initialize(...)
7
+ super
8
+
9
+ require_relative "../../lib/memory"
10
+ end
11
+
12
+ # Print a memory report.
13
+ #
14
+ # Accepts either a `Memory::Sampler` or `Memory::Report` as input.
15
+ #
16
+ # @parameter input [Memory::Report] The sampler or report to print.
17
+ def print(input:)
18
+ # Convert Sampler to Report if needed:
19
+ report = case input
20
+ when Memory::Sampler
21
+ input.report
22
+ when Memory::Report
23
+ input
24
+ else
25
+ raise ArgumentError, "Expected Memory::Sampler or Memory::Report, got #{input.class}"
26
+ end
27
+
28
+ report.print($stderr)
29
+
30
+ return report
31
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ def initialize(...)
7
+ super
8
+
9
+ require_relative '../../lib/memory'
10
+ end
11
+
12
+ # Load a sampler from one or more .mprof files.
13
+ #
14
+ # Multiple files will be combined into a single sampler, useful for
15
+ # analyzing aggregated profiling data.
16
+ #
17
+ # @parameter paths [Array(String)] Paths to .mprof files.
18
+ # @returns [Memory::Sampler] The loaded sampler with all allocations.
19
+ def load(paths:)
20
+ sampler = Memory::Sampler.new
21
+ cache = sampler.cache
22
+ wrapper = sampler.wrapper
23
+
24
+ total_size = paths.sum{|path| File.size(path)}
25
+ progress = Console.logger.progress(sampler, total_size)
26
+
27
+ paths.each do |path|
28
+ Console.logger.info(sampler, "Loading #{path}, #{Memory.formatted_bytes File.size(path)}")
29
+
30
+ File.open(path, 'r', encoding: Encoding::BINARY) do |io|
31
+ unpacker = wrapper.unpacker(io)
32
+ count = unpacker.read_array_header
33
+
34
+ last_pos = 0
35
+
36
+ # Read allocations directly into the sampler's array:
37
+ unpacker.each do |allocation|
38
+ sampler.allocated << allocation
39
+
40
+ # Update progress based on bytes read:
41
+ current_pos = io.pos
42
+ progress.increment(current_pos - last_pos)
43
+ last_pos = current_pos
44
+ end
45
+ end
46
+
47
+ Console.logger.info(sampler, "Loaded #{sampler.allocated.size} allocations")
48
+ end
49
+
50
+ return sampler
51
+ end
52
+
53
+ # Dump a sampler to a .mprof file.
54
+ #
55
+ # @parameter input [Memory::Sampler] The sampler to dump.
56
+ # @parameter output [String] Path to write the .mprof file.
57
+ # @returns [Memory::Sampler] The input sampler.
58
+ def dump(path, input:)
59
+ File.open(path, 'w', encoding: Encoding::BINARY) do |io|
60
+ input.dump(io)
61
+ end
62
+
63
+ Console.logger.info(self, "Saved sampler to #{path} (#{File.size(path)} bytes)")
64
+
65
+ return input
66
+ end
67
+
68
+ # Load a sampler from an ObjectSpace heap dump.
69
+ #
70
+ # @parameter path [String] Path to the heap dump JSON file.
71
+ # @returns [Memory::Sampler] A sampler populated with allocations from the heap dump.
72
+ def load_object_space_dump(path)
73
+ file_size = File.size(path)
74
+ progress = Console.logger.progress(self, file_size)
75
+
76
+ Console.logger.info(self, "Loading heap dump from #{path} (#{Memory.formatted_bytes(file_size)})")
77
+
78
+ sampler = nil
79
+ File.open(path, 'r') do |io|
80
+ sampler = Memory::Sampler.load_object_space_dump(io) do |line_count, object_count|
81
+ # Update progress based on bytes read:
82
+ progress.increment(io.pos - progress.current)
83
+ end
84
+ end
85
+
86
+ Console.logger.info(self, "Loaded #{sampler.allocated.size} objects from heap dump")
87
+
88
+ return sampler
89
+ end
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  module Memory
7
7
  UNITS = {
8
- 0 => 'B',
9
- 3 => 'KiB',
10
- 6 => 'MiB',
11
- 9 => 'GiB',
12
- 12 => 'TiB',
13
- 15 => 'PiB',
14
- 18 => 'EiB',
15
- 21 => 'ZiB',
16
- 24 => 'YiB'
8
+ 0 => "B",
9
+ 3 => "KiB",
10
+ 6 => "MiB",
11
+ 9 => "GiB",
12
+ 12 => "TiB",
13
+ 15 => "PiB",
14
+ 18 => "EiB",
15
+ 21 => "ZiB",
16
+ 24 => "YiB"
17
17
  }.freeze
18
18
 
19
+ # Format bytes into human-readable units.
20
+ # @parameter bytes [Integer] The number of bytes to format.
21
+ # @returns [String] Formatted string with appropriate unit (e.g., "1.50 MiB").
19
22
  def self.formatted_bytes(bytes)
20
23
  return "0 B" if bytes.zero?
21
24
 
@@ -24,6 +27,8 @@ module Memory
24
27
  "%.2f #{UNITS[scale]}" % (bytes / 10.0**scale)
25
28
  end
26
29
 
30
+ # Aggregates memory allocations by a given metric.
31
+ # Groups allocations and tracks totals for memory usage and allocation counts.
27
32
  class Aggregate
28
33
  Total = Struct.new(:memory, :count) do
29
34
  def initialize
@@ -51,6 +56,9 @@ module Memory
51
56
  end
52
57
  end
53
58
 
59
+ # Initialize a new aggregate with a title and metric block.
60
+ # @parameter title [String] The title for this aggregate.
61
+ # @parameter block [Block] A block that extracts the metric from an allocation.
54
62
  def initialize(title, &block)
55
63
  @title = title
56
64
  @metric = block
@@ -64,6 +72,8 @@ module Memory
64
72
  attr :total
65
73
  attr :totals
66
74
 
75
+ # Add an allocation to this aggregate.
76
+ # @parameter allocation [Allocation] The allocation to add.
67
77
  def << allocation
68
78
  metric = @metric.call(allocation)
69
79
  total = @totals[metric]
@@ -75,10 +85,18 @@ module Memory
75
85
  @total.count += 1
76
86
  end
77
87
 
88
+ # Sort totals by a given key.
89
+ # @parameter key [Symbol] The key to sort by (e.g., :memory or :count).
90
+ # @returns [Array] Sorted array of [metric, total] pairs.
78
91
  def totals_by(key)
79
92
  @totals.sort_by{|metric, total| [total[key], metric]}
80
93
  end
81
94
 
95
+ # Print this aggregate to an IO stream.
96
+ # @parameter io [IO] The output stream to write to.
97
+ # @parameter limit [Integer] Maximum number of items to display.
98
+ # @parameter title [String] Optional title override.
99
+ # @parameter level [Integer] Markdown heading level for output.
82
100
  def print(io = $stderr, limit: 10, title: @title, level: 2)
83
101
  io.puts "#{'#' * level} #{title} #{@total}", nil
84
102
 
@@ -89,6 +107,9 @@ module Memory
89
107
  io.puts nil
90
108
  end
91
109
 
110
+ # Convert this aggregate to a JSON-compatible hash.
111
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
112
+ # @returns [Hash] JSON-compatible representation.
92
113
  def as_json(options = nil)
93
114
  {
94
115
  title: @title,
@@ -98,7 +119,12 @@ module Memory
98
119
  end
99
120
  end
100
121
 
122
+ # Aggregates memory allocations by value.
123
+ # Groups allocations by their actual values (e.g., string contents) and creates sub-aggregates.
101
124
  class ValueAggregate
125
+ # Initialize a new value aggregate.
126
+ # @parameter title [String] The title for this aggregate.
127
+ # @parameter block [Block] A block that extracts the metric from an allocation.
102
128
  def initialize(title, &block)
103
129
  @title = title
104
130
  @metric = block
@@ -110,6 +136,8 @@ module Memory
110
136
  attr :metric
111
137
  attr :aggregates
112
138
 
139
+ # Add an allocation to this value aggregate.
140
+ # @parameter allocation [Allocation] The allocation to add.
113
141
  def << allocation
114
142
  if value = allocation.value
115
143
  aggregate = @aggregates[value]
@@ -118,10 +146,17 @@ module Memory
118
146
  end
119
147
  end
120
148
 
149
+ # Sort aggregates by a given key.
150
+ # @parameter key [Symbol] The key to sort by (e.g., :memory or :count).
151
+ # @returns [Array] Sorted array of [value, aggregate] pairs.
121
152
  def aggregates_by(key)
122
153
  @aggregates.sort_by{|value, aggregate| [aggregate.total[key], value]}
123
154
  end
124
155
 
156
+ # Print this value aggregate to an IO stream.
157
+ # @parameter io [IO] The output stream to write to.
158
+ # @parameter limit [Integer] Maximum number of items to display.
159
+ # @parameter level [Integer] Markdown heading level for output.
125
160
  def print(io = $stderr, limit: 10, level: 2)
126
161
  io.puts "#{'#' * level} #{@title}", nil
127
162
 
@@ -130,6 +165,9 @@ module Memory
130
165
  end
131
166
  end
132
167
 
168
+ # Convert this value aggregate to a JSON-compatible hash.
169
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
170
+ # @returns [Hash] JSON-compatible representation.
133
171
  def as_json(options = nil)
134
172
  {
135
173
  title: @title,
data/lib/memory/cache.rb CHANGED
@@ -8,17 +8,23 @@
8
8
  # Copyright, 2016, by Hamdi Akoğuz.
9
9
  # Copyright, 2018, by Jonas Peschla.
10
10
  # Copyright, 2020, by Jean Boussier.
11
- # Copyright, 2020-2022, by Samuel Williams.
11
+ # Copyright, 2020-2025, by Samuel Williams.
12
12
 
13
13
  module Memory
14
+ # Cache for storing and looking up allocation metadata.
15
+ # Caches gem names, file locations, class names, and string values to reduce memory overhead during profiling.
14
16
  class Cache
17
+ # Initialize a new cache with empty lookup tables.
15
18
  def initialize
16
19
  @gem_guess_cache = Hash.new
17
- @location_cache = Hash.new { |h, k| h[k] = Hash.new.compare_by_identity }
20
+ @location_cache = Hash.new {|h, k| h[k] = Hash.new.compare_by_identity}
18
21
  @class_name_cache = Hash.new.compare_by_identity
19
22
  @string_cache = Hash.new
20
23
  end
21
-
24
+
25
+ # Guess the gem or library name from a file path.
26
+ # @parameter path [String] The file path to analyze.
27
+ # @returns [String] The guessed gem name, stdlib component, or "other".
22
28
  def guess_gem(path)
23
29
  @gem_guess_cache[path] ||=
24
30
  if /(\/gems\/.*)*\/gems\/(?<gemname>[^\/]+)/ =~ path
@@ -33,15 +39,26 @@ module Memory
33
39
  "other"
34
40
  end
35
41
  end
36
-
42
+
43
+ # Look up and cache a file location string.
44
+ # @parameter file [String] The source file path.
45
+ # @parameter line [Integer] The line number.
46
+ # @returns [String] The formatted location string "file:line".
37
47
  def lookup_location(file, line)
38
48
  @location_cache[file][line] ||= "#{file}:#{line}"
39
49
  end
40
-
50
+
51
+ # Look up and cache a class name.
52
+ # @parameter klass [Class] The class object.
53
+ # @returns [String] The class name or `unknown` if unavailable.
41
54
  def lookup_class_name(klass)
42
- @class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || '<<Unknown>>').to_s
55
+ @class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || "unknown").to_s
43
56
  end
44
-
57
+
58
+ # Look up and cache a string value.
59
+ # Strings are truncated to 64 characters to reduce memory usage.
60
+ # @parameter obj [String] The string object to cache.
61
+ # @returns [String] A cached copy of the string (truncated to 64 characters).
45
62
  def lookup_string(obj)
46
63
  # This string is shortened to 200 characters which is what the string report shows
47
64
  # The string report can still list unique strings longer than 200 characters
data/lib/memory/deque.rb CHANGED
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require 'objspace'
7
- require 'msgpack'
6
+ require "objspace"
7
+ require "msgpack"
8
8
 
9
9
  module Memory
10
+ # A double-ended queue implementation optimized for memory profiling.
11
+ # Stores items in segments to reduce memory reallocation overhead.
10
12
  class Deque
13
+ # Initialize a new empty deque.
11
14
  def initialize
12
15
  @segments = []
13
16
  @last = nil
14
17
  end
15
18
 
19
+ # Freeze this deque and all its segments.
20
+ # @returns [Deque] Self.
16
21
  def freeze
17
22
  return self if frozen?
18
23
 
@@ -24,6 +29,9 @@ module Memory
24
29
 
25
30
  include Enumerable
26
31
 
32
+ # Concatenate an array segment to this deque.
33
+ # @parameter segment [Array] The segment to append.
34
+ # @returns [Deque] Self.
27
35
  def concat(segment)
28
36
  @segments << segment
29
37
  @last = nil
@@ -31,6 +39,9 @@ module Memory
31
39
  return self
32
40
  end
33
41
 
42
+ # Append an item to this deque.
43
+ # @parameter item [Object] The item to append.
44
+ # @returns [Deque] Self.
34
45
  def << item
35
46
  unless @last
36
47
  @last = []
@@ -42,12 +53,16 @@ module Memory
42
53
  return self
43
54
  end
44
55
 
56
+ # Iterate over all items in the deque.
57
+ # @parameter block [Block] The block to yield each item to.
45
58
  def each(&block)
46
59
  @segments.each do |segment|
47
60
  segment.each(&block)
48
61
  end
49
62
  end
50
63
 
64
+ # Get the total number of items in the deque.
65
+ # @returns [Integer] The total number of items across all segments.
51
66
  def size
52
67
  @segments.sum(&:size)
53
68
  end
data/lib/memory/report.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require_relative 'aggregate'
6
+ require_relative "aggregate"
7
7
 
8
8
  module Memory
9
+ # A report containing aggregated memory allocation statistics.
10
+ # Collects and organizes allocation data by various metrics.
9
11
  class Report
12
+ # Create a general-purpose report with standard aggregates.
13
+ # @parameter options [Hash] Options to pass to the report constructor.
14
+ # @returns [Report] A new report with standard aggregates.
10
15
  def self.general(**options)
11
16
  Report.new([
12
17
  Aggregate.new("By Gem", &:gem),
@@ -18,6 +23,9 @@ module Memory
18
23
  ], **options)
19
24
  end
20
25
 
26
+ # Initialize a new report with the given aggregates.
27
+ # @parameter aggregates [Array] Array of Aggregate or ValueAggregate instances.
28
+ # @parameter retained_only [Boolean] Whether to only include retained allocations in aggregates.
21
29
  def initialize(aggregates, retained_only: true)
22
30
  @retained_only = retained_only
23
31
 
@@ -53,6 +61,8 @@ module Memory
53
61
  end
54
62
  end
55
63
 
64
+ # Print this report to an IO stream.
65
+ # @parameter io [IO] The output stream to write to.
56
66
  def print(io = $stderr)
57
67
  if @retained_only
58
68
  io.puts "\# Retained Memory Profile", nil
@@ -69,6 +79,9 @@ module Memory
69
79
  end
70
80
  end
71
81
 
82
+ # Convert this report to a JSON-compatible hash.
83
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
84
+ # @returns [Hash] JSON-compatible representation.
72
85
  def as_json(options = nil)
73
86
  {
74
87
  total_allocated: @total_allocated.as_json(options),
@@ -77,8 +90,16 @@ module Memory
77
90
  }
78
91
  end
79
92
 
80
- def to_json(...)
81
- as_json.to_json(...)
82
- end
93
+ # Convert this report to a JSON string.
94
+ # @returns [String] JSON representation of this report.
95
+ def to_json(...)
96
+ as_json.to_json(...)
97
+ end
98
+
99
+ # Generate a human-readable representation of this report.
100
+ # @returns [String] Summary showing allocated and retained totals.
101
+ def inspect
102
+ "#<#{self.class}: #{@total_allocated} allocated, #{@total_retained} retained>"
83
103
  end
84
104
  end
105
+ end
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require_relative '../sampler'
6
+ require_relative "../sampler"
7
7
 
8
8
  module Memory
9
+ # @namespace
9
10
  module RSpec
11
+ # Integration with RSpec for memory profiling test suites.
12
+ # Profiles memory allocations for RSpec example groups and generates reports.
10
13
  module Profiler
14
+ # Profile memory allocations for RSpec examples.
15
+ # @parameter scope [Object] The RSpec scope to apply profiling hooks to.
11
16
  def self.profile(scope)
12
17
  memory_sampler = nil
13
18
 
@@ -11,16 +11,21 @@
11
11
  # Copyright, 2018, by Jonas Peschla.
12
12
  # Copyright, 2018, by Espartaco Palma.
13
13
  # Copyright, 2020, by Jean Boussier.
14
- # Copyright, 2020-2024, by Samuel Williams.
14
+ # Copyright, 2020-2025, by Samuel Williams.
15
15
 
16
- require 'objspace'
17
- require 'msgpack'
18
- require 'console'
16
+ require "objspace"
17
+ require "msgpack"
18
+ require "json"
19
+ require "console"
19
20
 
20
- require_relative 'cache'
21
+ require_relative "cache"
21
22
 
22
23
  module Memory
24
+ # MessagePack factory wrapper for serializing allocations.
25
+ # Registers custom types for efficient serialization of allocation data.
23
26
  class Wrapper < MessagePack::Factory
27
+ # Initialize the wrapper with a cache.
28
+ # @parameter cache [Cache] The cache to use for allocation metadata.
24
29
  def initialize(cache)
25
30
  super()
26
31
 
@@ -61,6 +66,74 @@ module Memory
61
66
  # end
62
67
  # ~~~
63
68
  class Sampler
69
+ # Load allocations from an ObjectSpace heap dump.
70
+ #
71
+ # If a block is given, it will be called periodically with progress information.
72
+ #
73
+ # @parameter io [IO] The IO stream containing the heap dump JSON.
74
+ # @yields [line_count, object_count] Progress callback with current line and object counts.
75
+ # @returns [Sampler] A new sampler populated with allocations from the heap dump.
76
+ def self.load_object_space_dump(io, &block)
77
+ sampler = new
78
+ cache = sampler.cache
79
+
80
+ line_count = 0
81
+ object_count = 0
82
+ report_interval = 10000
83
+
84
+ io.each_line do |line|
85
+ line_count += 1
86
+
87
+ begin
88
+ object = JSON.parse(line)
89
+ rescue JSON::ParserError
90
+ # Skip invalid JSON lines
91
+ next
92
+ end
93
+
94
+ # Skip non-object entries (ROOT, SHAPE, etc.)
95
+ next unless object['address']
96
+
97
+ # Get allocation information (may be nil if tracing wasn't enabled)
98
+ file = object['file'] || '(unknown)'
99
+ line_number = object['line'] || 0
100
+
101
+ # Get object type/class
102
+ type = object['type'] || 'unknown'
103
+
104
+ # Get memory size
105
+ memsize = object['memsize'] || 0
106
+
107
+ # Get value for strings
108
+ value = object['value']
109
+
110
+ allocation = Allocation.new(
111
+ cache,
112
+ type, # class_name
113
+ file, # file
114
+ line_number, # line
115
+ memsize, # memsize
116
+ value, # value (for strings)
117
+ true # retained (all objects in heap dump are live)
118
+ )
119
+
120
+ sampler.allocated << allocation
121
+ object_count += 1
122
+
123
+ # Report progress periodically
124
+ if block && (object_count % report_interval == 0)
125
+ block.call(line_count, object_count)
126
+ end
127
+ end
128
+
129
+ # Final progress report
130
+ block.call(line_count, object_count) if block
131
+
132
+ return sampler
133
+ end
134
+
135
+ # Initialize a new sampler.
136
+ # @parameter filter [Block | Nil] Optional filter block to select which allocations to track.
64
137
  def initialize(&filter)
65
138
  @filter = filter
66
139
 
@@ -69,6 +142,8 @@ module Memory
69
142
  @allocated = Array.new
70
143
  end
71
144
 
145
+ # Generate a human-readable representation of this sampler.
146
+ # @returns [String] String showing the number of allocations tracked.
72
147
  def inspect
73
148
  "#<#{self.class} #{@allocated.size} allocations>"
74
149
  end
@@ -79,6 +154,8 @@ module Memory
79
154
  attr :wrapper
80
155
  attr :allocated
81
156
 
157
+ # Start tracking memory allocations.
158
+ # Disables GC and begins ObjectSpace allocation tracing.
82
159
  def start
83
160
  GC.disable
84
161
  3.times{GC.start}
@@ -90,6 +167,8 @@ module Memory
90
167
  ObjectSpace.trace_object_allocations_start
91
168
  end
92
169
 
170
+ # Stop tracking allocations and determine which ones were retained.
171
+ # Re-enables GC and marks retained allocations.
93
172
  def stop
94
173
  ObjectSpace.trace_object_allocations_stop
95
174
  allocated = track_allocations(@generation)
@@ -116,6 +195,9 @@ module Memory
116
195
  ObjectSpace.trace_object_allocations_clear
117
196
  end
118
197
 
198
+ # Serialize allocations to MessagePack format.
199
+ # @parameter io [IO | Nil] Optional IO stream to write to. If nil, returns serialized data.
200
+ # @returns [String | Nil] Serialized data if no IO stream provided, otherwise nil.
119
201
  def dump(io = nil)
120
202
  Console.logger.debug(self, "Dumping allocations: #{@allocated.size}")
121
203
 
@@ -128,6 +210,8 @@ module Memory
128
210
  end
129
211
  end
130
212
 
213
+ # Load allocations from MessagePack-serialized data.
214
+ # @parameter data [String] The serialized allocation data.
131
215
  def load(data)
132
216
  allocations = @wrapper.load(data)
133
217
 
@@ -136,6 +220,9 @@ module Memory
136
220
  @allocated.concat(allocations)
137
221
  end
138
222
 
223
+ # Generate a report from the tracked allocations.
224
+ # @parameter options [Hash] Options to pass to the report constructor.
225
+ # @returns [Report] A report containing allocation statistics.
139
226
  def report(**options)
140
227
  report = Report.general(**options)
141
228
 
@@ -7,5 +7,5 @@
7
7
  # Copyright, 2020-2024, by Samuel Williams.
8
8
 
9
9
  module Memory
10
- VERSION = "0.5.0"
10
+ VERSION = "0.6.0"
11
11
  end
data/lib/memory.rb CHANGED
@@ -5,14 +5,20 @@
5
5
  # Copyright, 2014, by Søren Skovsbøll.
6
6
  # Copyright, 2017, by Nick LaMuro.
7
7
  # Copyright, 2018, by Jonas Peschla.
8
- # Copyright, 2020-2022, by Samuel Williams.
8
+ # Copyright, 2020-2025, by Samuel Williams.
9
9
 
10
10
  require_relative "memory/version"
11
11
  require_relative "memory/cache"
12
12
  require_relative "memory/report"
13
13
  require_relative "memory/sampler"
14
14
 
15
+ # Memory profiler for Ruby applications.
16
+ # Provides tools to track and analyze memory allocations and retention.
15
17
  module Memory
18
+ # Capture memory allocations from a block of code.
19
+ # @parameter report [Report | Nil] Optional report instance to add samples to.
20
+ # @parameter block [Block] The code to profile.
21
+ # @returns [Report] A report containing allocation statistics.
16
22
  def self.capture(report = nil, &block)
17
23
  sampler = Sampler.new
18
24
  sampler.run(&block)
@@ -23,6 +29,9 @@ module Memory
23
29
  return report
24
30
  end
25
31
 
32
+ # Generate a memory allocation report for a block of code.
33
+ # @parameter block [Block] The code to profile.
34
+ # @returns [Report] A report containing allocation statistics.
26
35
  def self.report(&block)
27
36
  self.capture(&block)
28
37
  end
data/license.md CHANGED
@@ -24,7 +24,7 @@ Copyright, 2019-2020, by Jean Boussier.
24
24
  Copyright, 2019, by Ashwin Maroli.
25
25
  Copyright, 2019, by Olle Jonsson.
26
26
  Copyright, 2019, by Danny Ben Shitrit.
27
- Copyright, 2020-2024, by Samuel Williams.
27
+ Copyright, 2020-2025, by Samuel Williams.
28
28
 
29
29
  Permission is hereby granted, free of charge, to any person obtaining a copy
30
30
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -19,68 +19,47 @@ $ bundle add 'memory'
19
19
 
20
20
  ## Usage
21
21
 
22
- ``` ruby
23
- require 'memory'
24
-
25
- report = Memory.report do
26
- # run your code here
27
- end
28
-
29
- report.print
30
- ```
31
-
32
- Or, you can use the `.start`/`.stop` methods as well:
33
-
34
- ``` ruby
35
- require 'memory'
36
-
37
- sampler = Memory::Sampler.new
22
+ Please see the [project documentation](https://socketry.github.io/memory/) for more details.
38
23
 
39
- sampler.start
40
- # run your code here
41
- sampler.stop
42
-
43
- report = sampler.report
44
- report.print
45
- ```
24
+ - [Getting Started](https://socketry.github.io/memory/guides/getting-started/index) - This guide explains how to get started with `memory`, a Ruby gem for profiling memory allocations in your applications.
46
25
 
47
26
  ### RSpec Integration
48
27
 
49
28
  ``` ruby
50
29
  memory_sampler = nil
51
30
  config.before(:all) do |example_group|
52
- name = example_group.class.description.gsub(/[^\w]+/, '-')
31
+ name = example_group.class.description.gsub(/[^\w]+/, "-")
53
32
  path = "#{name}.mprof"
54
-
33
+
55
34
  skip if File.exist?(path)
56
-
35
+
57
36
  memory_sampler = Memory::Sampler.new
58
37
  memory_sampler.start
59
38
  end
60
39
 
61
40
  config.after(:all) do |example_group|
62
- name = example_group.class.description.gsub(/[^\w]+/, '-')
41
+ name = example_group.class.description.gsub(/[^\w]+/, "-")
63
42
  path = "#{name}.mprof"
64
-
43
+
65
44
  if memory_sampler
66
45
  memory_sampler.stop
67
-
46
+
68
47
  File.open(path, "w", encoding: Encoding::BINARY) do |io|
69
48
  memory_sampler.dump(io)
70
49
  end
71
-
50
+
72
51
  memory_sampler = nil
73
52
  end
74
53
  end
75
54
 
76
55
  config.after(:suite) do
77
56
  memory_sampler = Memory::Sampler.new
78
-
79
- Dir.glob('*.mprof') do |path|
57
+
58
+ Dir.glob("*.mprof") do |path|
80
59
  $stderr.puts "Loading #{path}..."
81
60
  memory_sampler.load(File.read(path, encoding: Encoding::BINARY))
82
61
  end
83
-
62
+
84
63
  $stderr.puts "Memory usage:"
85
64
  memory_sampler.report.print
86
65
  end
@@ -111,6 +90,14 @@ config.after(:suite) do |example|
111
90
  end
112
91
  ```
113
92
 
93
+ ## Releases
94
+
95
+ Please see the [project releases](https://socketry.github.io/memory/releases/index) for all releases.
96
+
97
+ ### v0.6.0
98
+
99
+ - Add agent context.
100
+
114
101
  ## Contributing
115
102
 
116
103
  We welcome contributions to this project.
@@ -123,8 +110,8 @@ We welcome contributions to this project.
123
110
 
124
111
  ### Developer Certificate of Origin
125
112
 
126
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
113
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
127
114
 
128
- ### Contributor Covenant
115
+ ### Community Guidelines
129
116
 
130
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
117
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.6.0
4
+
5
+ - Add agent context.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -29,7 +29,6 @@ authors:
29
29
  - Olle Jonsson
30
30
  - Vasily Kolesnikov
31
31
  - William Tabi
32
- autorequire:
33
32
  bindir: bin
34
33
  cert_chain:
35
34
  - |
@@ -61,7 +60,7 @@ cert_chain:
61
60
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
62
61
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
63
62
  -----END CERTIFICATE-----
64
- date: 2024-06-27 00:00:00.000000000 Z
63
+ date: 1980-01-02 00:00:00.000000000 Z
65
64
  dependencies:
66
65
  - !ruby/object:Gem::Dependency
67
66
  name: bake
@@ -105,13 +104,12 @@ dependencies:
105
104
  - - ">="
106
105
  - !ruby/object:Gem::Version
107
106
  version: '0'
108
- description:
109
- email:
110
107
  executables: []
111
108
  extensions: []
112
109
  extra_rdoc_files: []
113
110
  files:
114
- - bake/memory/profiler.rb
111
+ - bake/memory/report.rb
112
+ - bake/memory/sampler.rb
115
113
  - lib/memory.rb
116
114
  - lib/memory/aggregate.rb
117
115
  - lib/memory/cache.rb
@@ -122,13 +120,13 @@ files:
122
120
  - lib/memory/version.rb
123
121
  - license.md
124
122
  - readme.md
123
+ - releases.md
125
124
  homepage: https://github.com/socketry/memory
126
125
  licenses:
127
126
  - MIT
128
127
  metadata:
129
128
  documentation_uri: https://socketry.github.io/memory/
130
129
  source_code_uri: https://github.com/socketry/memory.git
131
- post_install_message:
132
130
  rdoc_options: []
133
131
  require_paths:
134
132
  - lib
@@ -136,15 +134,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
136
134
  requirements:
137
135
  - - ">="
138
136
  - !ruby/object:Gem::Version
139
- version: '3.1'
137
+ version: '3.2'
140
138
  required_rubygems_version: !ruby/object:Gem::Requirement
141
139
  requirements:
142
140
  - - ">="
143
141
  - !ruby/object:Gem::Version
144
142
  version: '0'
145
143
  requirements: []
146
- rubygems_version: 3.5.11
147
- signing_key:
144
+ rubygems_version: 3.7.2
148
145
  specification_version: 4
149
146
  summary: Memory profiling routines for Ruby 2.3+
150
147
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2013-2018, by Sam Saffron.
5
- # Copyright, 2014, by Richard Schneeman.
6
- # Copyright, 2018, by Jonas Peschla.
7
- # Copyright, 2020-2022, by Samuel Williams.
8
-
9
- def check(paths:)
10
- require 'console'
11
-
12
- total_size = paths.sum{|path| File.size(path)}
13
-
14
- require_relative '../../lib/memory'
15
-
16
- report = Memory::Report.general
17
-
18
- cache = Memory::Cache.new
19
- wrapper = Memory::Wrapper.new(cache)
20
-
21
- progress = Console.logger.progress(report, total_size)
22
-
23
- paths.each do |path|
24
- Console.logger.info(report, "Loading #{path}, #{Memory.formatted_bytes File.size(path)}")
25
-
26
- File.open(path) do |io|
27
- unpacker = wrapper.unpacker(io)
28
- count = unpacker.read_array_header
29
-
30
- report.concat(unpacker)
31
-
32
- progress.increment(io.size)
33
- end
34
-
35
- Console.logger.info(report, "Loaded allocations, #{report.total_allocated}")
36
- end
37
-
38
- report.print($stdout)
39
- end