memtf 0.0.2 → 0.0.3

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
  SHA1:
3
- metadata.gz: f77e6fb9813978dda4987dc20dddb424c4a1c1b5
4
- data.tar.gz: 6f5b28f54b82938efadaa508f09e5d550a1a2978
3
+ metadata.gz: 6c248874dd670cf32b12d58d1ffb8c10f799cfa8
4
+ data.tar.gz: a9c0bd0d27092285884f10abe2191a5672b7ac2f
5
5
  SHA512:
6
- metadata.gz: df1fe991d654a1c019e72121bd4c4fa38fbc8564727dc29ce0e6faa8dc2684c735e440b96758aca6bc50dd462fdeb3176aa8513978e14ffc73ce6a94d69af660
7
- data.tar.gz: 9abcd938d071c1728be1e6609f89747492d80df7b9087d510e50266a9b5a304b2aab4e418e8f0c438d0e666269fbf949d9fd2bf767223ae96280a4ee5fbe110b
6
+ metadata.gz: 69e7d5eff222b8860da1f6821f418358d80087782ab6f0fb02a7b8b0d5e164945a91f358c126a735085e7301e4336b8d14441a09ef4af5bb7586b2fd6825df64
7
+ data.tar.gz: d683378a0345795576cb00db1145a60f4ed095a4ffcd5d22fd3a8141bb2be52536aa7a072e2a6765e92b33f640e441d96cec552383a72259ffdd80c515fe3eb1
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .DS_Store
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
@@ -15,3 +16,4 @@ spec/reports
15
16
  test/tmp
16
17
  test/version_tmp
17
18
  tmp
19
+
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ [![Gem Version](https://badge.fury.io/rb/memtf.png)](http://badge.fury.io/rb/memtf)
1
2
  [![Build Status](https://secure.travis-ci.org/dresselm/memtf.png)](http://travis-ci.org/dresselm/memtf)
2
3
 
3
4
  # Memtf
@@ -6,6 +7,22 @@ A simple utility to help isolate memory leaks in your ruby applications.
6
7
 
7
8
  ## Why do we need another 'memory profiler'?
8
9
 
10
+ Simplicity and focus. This utility is limited in its features, but should help you quickly isolate the
11
+ class that is causing the bloat. This alone may be enough. No patches to ruby, no complicated setup and
12
+ no confusing output.
13
+
14
+ Future releases will support a second pass to drill into a given class. Hopefully this 2-stroke approach
15
+ will be sufficient for most memory leaks.
16
+
17
+ In the meantime, here are some other memory utilities that may help:
18
+
19
+ * [perftools](https://github.com/tmm1/perftools.rb) - Aman Gupta, enough said - low-level sampling profiler
20
+ * [memprof](https://github.com/ice799/memprof) - Joe Damato, not sure which versions of ruby are supported
21
+ * [rubymass](https://github.com/archan937/ruby-mass) - pure ruby and easy to use
22
+ * [memlog](https://rubygems.org/gems/memlog) - pure ruby and extremely limited, but may help you get started
23
+
24
+ Let me know if there are others that I should add to the above list.
25
+
9
26
  ## Installation
10
27
 
11
28
  Add this line to your application's Gemfile:
@@ -46,21 +63,34 @@ The APIs used by the gem require ruby 1.9.3+.
46
63
  > 500000.times { |i| leaky_array << "#{i % 2}-#{Time.now.to_i}" }
47
64
  > end
48
65
 
49
- +-----------------------------+---------+---------+--------+
50
- | Class | Objects | Leakage | Impact |
51
- +-----------------------------+---------+---------+--------+
52
- | Array | 2189 | 4.972MB | 96.85% |
53
- | RubyVM::InstructionSequence | 99 | 0.127MB | 2.47% |
54
- | Module | 18 | 0.017MB | 0.33% |
55
- | Class | 13 | 0.010MB | 0.20% |
56
- | String | 663007 | 0.006MB | 0.12% |
57
- | Regexp | 2 | 0.001MB | 0.02% |
58
- | Hash | 9 | 0.001MB | 0.02% |
59
- | Thread | 0 | 0.000MB | 0.00% |
60
- +-----------------------------+---------+---------+--------+
66
+ +-----------------------------+--------+---------+---------+---------+---------+
67
+ | Class | Impact | Leakage | Change | Objects | Change |
68
+ +-----------------------------+--------+---------+---------+---------+---------+
69
+ | Array | 96.85% | 4.972MB | 4.972MB | 2189 | 1985 |
70
+ | RubyVM::InstructionSequence | 2.47% | 0.127MB | 0.000MB | 99 | 0 |
71
+ | Module | 0.33% | 0.017MB | 0.002MB | 18 | 0 |
72
+ | Class | 0.20% | 0.010MB | 0.001MB | 13 | 0 |
73
+ | String | 0.12% | 0.006MB | 0.001MB | 663007 | 123650 |
74
+ | Regexp | 0.02% | 0.001MB | 0.000MB | 2 | 0 |
75
+ | Hash | 0.02% | 0.001MB | 0.001MB | 9 | 2 |
76
+ | Thread | 0.00% | 0.000MB | 0.000MB | 0 | 0 |
77
+ +-----------------------------+--------+---------+---------+---------+---------+
61
78
 
62
79
  ## What should I do with these results?
63
80
 
81
+ If there is an obvious Class that is impacting the overall memory footprint asymmetrically, then
82
+ you should focus further efforts on that and identifying where it is referenced and what it
83
+ references.
84
+
85
+ If there is no obvious Class, then either you need to better reproduce the leak (more iterations,
86
+ execute different paths, etc), or multiple Classes are leaking. If the latter, then try to create
87
+ a focused test that narrows down the leak.
88
+
89
+ If the highlighted Class is a generic container, like Array or Hash, the leak is probably one or more
90
+ Classes referenced within the container. I am working on additional functionality to expose the most
91
+ likely candidates. While I build out this and other additional functionality, please use the tools
92
+ mentioned above to help isolate the leak.
93
+
64
94
  ## Contributing
65
95
 
66
96
  1. Fork it
@@ -1,18 +1,25 @@
1
+ # A simple utility to help isolate memory leaks. Two
2
+ # memory snapshots are compared to determine which
3
+ # classes, if any, are leaking.
1
4
  module Memtf
2
- # Represents the starting
5
+ # Represents the starting memory snapshot
3
6
  START = :start
4
- # Represents the end of a run
7
+ # Represents the ending memory snapshot
5
8
  STOP = :stop
6
9
 
7
10
  class << self
8
11
  attr_accessor :runner
9
12
 
13
+ # Generate an initial memory snapshot.
14
+ #
10
15
  # @param [Hash] options
11
16
  # @return [Runner]
12
17
  def start(options={})
13
18
  self.runner = Runner.run(START, options)
14
19
  end
15
20
 
21
+ # Generate a final memory snapshot.
22
+ #
16
23
  # @param [Hash] options
17
24
  def stop(options={})
18
25
  default_group = self.runner.group
@@ -21,6 +28,9 @@ module Memtf
21
28
  self.runner = nil
22
29
  end
23
30
 
31
+ # Generate an initial memory snapshot, execute
32
+ # the block, then generate the final memory snapshot.
33
+ #
24
34
  # @param [Hash] options
25
35
  def around(options={}, &block)
26
36
  start(options)
@@ -1,22 +1,32 @@
1
- require 'objspace'
2
-
1
+ # Encapsulates logic that measures the memory footprint of all
2
+ # objects at a given point in time and compares the memory footprints
3
+ # of two points in time.
3
4
  class Memtf::Analyzer
4
5
 
5
- attr_reader :threshold, :filter
6
+ attr_reader :threshold, :filter, :memory_tracker
6
7
 
8
+ # The threshold of total memory consumption required
9
+ # to be included in the output
7
10
  DEFAULT_THRESHOLD = 0.005
11
+ # Represents 1 million bytes
8
12
  MB = 1024.0**2
9
13
 
14
+ # Determine the memory footprint of each class and filter out classes
15
+ # that do not meet the configured threshold.
16
+ #
10
17
  # @param [Hash] options
11
18
  def self.analyze(options={})
12
19
  new(options).analyze
13
20
  end
14
21
 
22
+ # Compare the memory footprints for the start and end memory snapshots
23
+ # within the same snapshot group.
24
+ #
15
25
  # @param [String] group
16
26
  # @return [Hash]
17
27
  def self.analyze_group(group)
18
- start_analysis = Memtf::Persistance.load(Memtf::START, group)
19
- end_analysis = Memtf::Persistance.load(Memtf::STOP, group)
28
+ start_analysis = Memtf::Persistance.load(Memtf::START, group)
29
+ end_analysis = Memtf::Persistance.load(Memtf::STOP, group)
20
30
 
21
31
  comparison = {}
22
32
  total_memsize = 0
@@ -29,32 +39,41 @@ class Memtf::Analyzer
29
39
  start_val = start_stats.nil? ? 0 : start_stats[stat_key]
30
40
  end_val = end_stats[stat_key]
31
41
  delta = end_val - start_val
32
- comparison[clazz][stat_key] = delta
33
42
 
34
- # Perhaps just compare this
35
- total_memsize += delta if stat_key == 'size'
43
+ comparison[clazz][stat_key] = end_val
44
+ comparison[clazz]["#{stat_key}_delta"] = delta
45
+
46
+ total_memsize += end_val if stat_key == 'size'
36
47
  end
37
48
  end
38
49
 
39
50
  # Determine the relative memory impact of each class
51
+ # TODO look into object count impact via ObjectSpace.count_objects
40
52
  comparison.keys.each do |klazz|
41
53
  stats = comparison[klazz]
42
- stats['impact'] = stats['size'] / total_memsize
54
+ stats['impact'] = (stats['size']*1.0) / total_memsize
43
55
  end
44
56
 
45
57
  comparison
46
58
  end
47
59
 
48
60
  def initialize(options={})
49
- @filter = options[:filter]
50
- @threshold = options.fetch(:threshold, DEFAULT_THRESHOLD)
61
+ @filter = options[:filter]
62
+ @threshold = options.fetch(:threshold, DEFAULT_THRESHOLD)
63
+ @memory_tracker = options.fetch(:memory_tracker, Memtf::Analyzer::Memory)
51
64
  end
52
65
 
66
+ # Determine the memory footprint of each class and filter out classes
67
+ # that do not meet the configured threshold.
68
+ #
53
69
  # @return [Hash]
54
70
  def analyze
71
+ # Signal a new GC to attempt to clear out non-leaked memory
72
+ # TODO investigate ObjectSpace.garbage_collect
55
73
  GC.start
56
74
 
57
75
  classes_stats = {}
76
+ # TODO investigate ObjectSpace.count_objects_size[:TOTAL]
58
77
  total_memsize = 0
59
78
 
60
79
  # Track the memory footprint of each class
@@ -69,13 +88,15 @@ class Memtf::Analyzer
69
88
  # 'String' => [2,1]
70
89
  # }
71
90
  #
72
- ObjectSpace.each_object do |obj|
91
+ memory_tracker.iterate do |obj|
73
92
  if (clazz = obj.class).respond_to?(:name)
74
93
  class_name = clazz.name
75
94
  class_stats = (classes_stats[class_name] ||= [])
76
95
 
77
- obj_memsize = ObjectSpace.memsize_of(obj)
96
+ obj_memsize = memory_tracker.size_of(obj)
78
97
  class_stats << obj_memsize
98
+
99
+ # Note: could also use ObjectSpace.memsize_of_all(clazz)
79
100
  total_memsize += obj_memsize
80
101
  end
81
102
  end
@@ -143,4 +164,6 @@ class Memtf::Analyzer
143
164
  smh_hash
144
165
  end
145
166
 
146
- end
167
+ end
168
+
169
+ require 'memtf/analyzer/memory'
@@ -0,0 +1,21 @@
1
+ require 'objspace'
2
+
3
+ # Encapsulate implementation of object memory tracking
4
+ class Memtf::Analyzer::Memory
5
+ class << self
6
+ # Iterate over each object on the heap
7
+ def iterate(&block)
8
+ ObjectSpace.each_object do |obj|
9
+ block.call(obj)
10
+ end
11
+ end
12
+
13
+ # Calculate the memory allocated to a given Object in bytes
14
+ #
15
+ # @param [Object] object
16
+ # @return [Number]
17
+ def size_of(object)
18
+ ObjectSpace.memsize_of(object)
19
+ end
20
+ end
21
+ end
@@ -1,10 +1,15 @@
1
1
  require 'fileutils'
2
2
  require 'multi_json'
3
3
 
4
+ # Encapsulates the logic and mechanism for saving
5
+ # and loading raw memory data.
4
6
  class Memtf::Persistance
7
+ # The directory where raw data is stored
5
8
  OUTPUT_DIR = "tmp/memtf"
6
9
 
7
10
  class << self
11
+ # Serialize group data to the filesystem.
12
+ #
8
13
  # @param [String] name
9
14
  # @param [String] group
10
15
  # @param [Object] payload
@@ -12,17 +17,19 @@ class Memtf::Persistance
12
17
  group_directory = group_dir(group)
13
18
  FileUtils.mkdir_p("#{group_directory}")
14
19
 
15
- save_file = "#{group_directory}/#{name}.json"
20
+ save_file = "#{group_directory}/#{name}-#{Process.pid}.json"
16
21
  File.open(save_file, 'w+') do |f|
17
- f.puts ::MultiJson.encode(payload)
22
+ f.print ::MultiJson.encode(payload)
18
23
  end
19
24
  end
20
25
 
26
+ # De-serialize group data from the filesystem.
27
+ #
21
28
  # @param [String] name
22
29
  # @param [String] group
23
30
  # @return [Object]
24
31
  def load(name, group)
25
- load_file = "#{group_dir(group)}/#{name}.json"
32
+ load_file = "#{group_dir(group)}/#{name}-#{Process.pid}.json"
26
33
  ::MultiJson.decode File.read(load_file)
27
34
  end
28
35
 
@@ -34,4 +41,4 @@ class Memtf::Persistance
34
41
  "#{OUTPUT_DIR}/#{group}"
35
42
  end
36
43
  end
37
- end
44
+ end
@@ -1,9 +1,23 @@
1
1
  require 'terminal-table'
2
2
 
3
+ # Encapsulates the formatting and output of Memtf analysis.
4
+ #
5
+ # Example Report:
6
+ #
7
+ # +-----------------------------+--------+---------+---------+---------+---------+
8
+ # | Class | Impact | Leakage | Change | Objects | Change |
9
+ # +-----------------------------+--------+---------+---------+---------+---------+
10
+ # | Array | 96.85% | 4.972MB | 4.972MB | 2189 | 1985 |
11
+ # ...
12
+ #
3
13
  class Memtf::Reporter
14
+ # The report table headers
15
+ HEADERS = ['Class', 'Impact', 'Leakage', 'Change', 'Objects', 'Change']
4
16
 
5
17
  attr_reader :group, :options
6
18
 
19
+ # Print the analysis in a concise tabular format.
20
+ #
7
21
  # @param [String] group
8
22
  def self.report(group)
9
23
  new(group).report
@@ -14,12 +28,19 @@ class Memtf::Reporter
14
28
  @options = options
15
29
  end
16
30
 
31
+ # Print the analysis in a concise tabular format.
32
+ #
17
33
  # @return [Terminal::Table]
18
34
  def report
19
- Terminal::Table.new(:headings => ['Class', 'Objects', 'Leakage', 'Impact']) do |t|
35
+ Terminal::Table.new(:headings => HEADERS) do |t|
20
36
  group_analysis = Memtf::Analyzer.analyze_group(group)
21
37
  group_analysis.sort_by { |k,v| -v['impact'] }.each do |k,v|
22
- t << [k,v['count'],to_MB(v['size']),to_pct(v['impact'])]
38
+ t << [k,
39
+ to_pct(v['impact']),
40
+ to_MB(v['size']),
41
+ to_MB(v['size_delta']),
42
+ v['count'],
43
+ v['count_delta']]
23
44
  end
24
45
  end
25
46
  end
@@ -35,4 +56,4 @@ class Memtf::Reporter
35
56
  def to_pct(num)
36
57
  "%.2f%" % [num * 100]
37
58
  end
38
- end
59
+ end
@@ -1,6 +1,10 @@
1
+ # Encapsulates the multiple steps required to accomplish
2
+ # Memtf analysis and reporting.
1
3
  class Memtf::Runner
2
4
  attr_reader :group, :options, :report
3
5
 
6
+ # Run the Memtf analysis and reporting.
7
+ #
4
8
  # @param [String] stage
5
9
  # @param [Hash] options
6
10
  def self.run(stage, options={})
@@ -12,7 +16,10 @@ class Memtf::Runner
12
16
  @options = options
13
17
  end
14
18
 
19
+ # Run the Memtf analysis and reporting.
20
+ #
15
21
  # @param [String] stage
22
+ # @return [Memtf::Runner]
16
23
  def run(stage)
17
24
  analysis = Memtf::Analyzer.analyze(options)
18
25
  Memtf::Persistance.save(stage, group, analysis)
@@ -25,4 +32,4 @@ class Memtf::Runner
25
32
 
26
33
  self
27
34
  end
28
- end
35
+ end
@@ -1,4 +1,5 @@
1
+ # Common utilities
1
2
  module Memtf::Utilities
2
3
  end
3
4
 
4
- require 'memtf/utilities/array'
5
+ require 'memtf/utilities/array'
@@ -1,10 +1,15 @@
1
+ # Monkeypatch Array with various utilities.
1
2
  module Memtf::Utilities::Array
3
+
4
+ # Add up all array elements.
5
+ #
2
6
  # @return [Number]
3
7
  def sum
4
8
  inject(0) { |sum, elem| sum + elem }
5
9
  end
6
10
  end
7
11
 
12
+ # Only monkeypatch if the method has not been defined already.
8
13
  unless Array.method_defined? :sum
9
14
  Array.send :include, Memtf::Utilities::Array
10
- end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Memtf
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3" #nodoc
3
3
  end
@@ -22,10 +22,11 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency 'terminal-table'
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "fakefs"
26
+ spec.add_development_dependency "guard-rspec"
25
27
  spec.add_development_dependency "pry"
26
28
  spec.add_development_dependency "rake"
27
29
  spec.add_development_dependency "rspec"
28
- spec.add_development_dependency "guard-rspec"
29
30
  spec.add_development_dependency "simplecov"
30
31
  spec.add_development_dependency "yard"
31
32
  end
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ describe Memtf::Analyzer do
4
+ let(:options) { {} }
5
+
6
+ describe '.analyze' do
7
+ it 'should create a new Analyzer' do
8
+ mock_analyzer = double(described_class)
9
+ mock_analyzer.stub(:analyze)
10
+ described_class.should_receive(:new).with(options).and_return(mock_analyzer)
11
+
12
+ described_class.analyze(options)
13
+ end
14
+
15
+ it 'should delegate to analyze' do
16
+ mock_analyzer = double(described_class)
17
+ described_class.stub(:new).with(options).and_return(mock_analyzer)
18
+ mock_analyzer.should_receive(:analyze).and_return({})
19
+
20
+ described_class.analyze(options)
21
+ end
22
+ end
23
+
24
+ describe '.analyze_group' do
25
+ let(:start_data) {
26
+ {
27
+ 'Array' => {'count' => 1, 'size' => 10},
28
+ 'Hash' => {'count' => 2, 'size' => 5},
29
+ 'Fixnum' => {'count' => 2, 'size' => 10},
30
+ 'Others*' => {'count' => 3, 'size' => 4}
31
+ }
32
+ }
33
+ let(:end_data) {
34
+ {
35
+ 'Array' => {'count' => 3, 'size' => 50},
36
+ 'Hash' => {'count' => 4, 'size' => 20},
37
+ 'Fixnum' => {'count' => 8, 'size' => 20},
38
+ 'Others*' => {'count' => 6, 'size' => 10}
39
+ }
40
+ }
41
+ let(:group) { 'test_group' }
42
+
43
+ it 'should load the start data' do
44
+ Memtf::Persistance.should_receive(:load).with(Memtf::START,group).and_return(start_data)
45
+ Memtf::Persistance.stub(:load).with(Memtf::STOP,group).and_return(end_data)
46
+
47
+ described_class.analyze_group(group)
48
+ end
49
+
50
+ it 'should load the end data' do
51
+ Memtf::Persistance.stub(:load).with(Memtf::START,group).and_return(start_data)
52
+ Memtf::Persistance.should_receive(:load).with(Memtf::STOP,group).and_return(end_data)
53
+
54
+ described_class.analyze_group(group)
55
+ end
56
+
57
+ it 'should compare the start and end object counts' do
58
+ Memtf::Persistance.stub(:load).with(Memtf::START,group).and_return(start_data)
59
+ Memtf::Persistance.stub(:load).with(Memtf::STOP,group).and_return(end_data)
60
+
61
+ output = described_class.analyze_group(group)
62
+ count_deltas = output.values.map { |o| o['count_delta']}
63
+ count_deltas.should_not be_empty
64
+ count_deltas.size.should == 4
65
+ count_deltas.should == [(3-1),(4-2),(8-2),(6-3)]
66
+ end
67
+
68
+ it 'should compare the start and end memory sizes' do
69
+ Memtf::Persistance.stub(:load).with(Memtf::START,group).and_return(start_data)
70
+ Memtf::Persistance.stub(:load).with(Memtf::STOP,group).and_return(end_data)
71
+
72
+ output = described_class.analyze_group(group)
73
+ size_deltas = output.values.map { |o| o['size_delta']}
74
+ size_deltas.should_not be_empty
75
+ size_deltas.size.should == 4
76
+ size_deltas.should == [(50-10),(20-5),(20-10),(10-4)]
77
+ end
78
+
79
+ it 'should generate an impact value' do
80
+ Memtf::Persistance.stub(:load).with(Memtf::START,group).and_return(start_data)
81
+ Memtf::Persistance.stub(:load).with(Memtf::STOP,group).and_return(end_data)
82
+
83
+ output = described_class.analyze_group(group)
84
+ impacts = output.values.map { |o| o['impact'] }
85
+ impacts.should_not be_empty
86
+ impacts.size.should == 4
87
+ # The impact is calculates as memory size of class
88
+ # compared to the total memory size
89
+ impacts.should == [0.5,0.2,0.2,0.1]
90
+ end
91
+ end
92
+
93
+ describe '#analyze' do
94
+ class StubbedMemoryTracker
95
+ STRING1 = 'some_string'
96
+ STRING2 = 'some_other_string'
97
+ ARRAY1 = [1,2,3,4,5,6]
98
+ HASH1 = {'foo' => 'bar'}
99
+
100
+ def self.iterate(&block)
101
+ [STRING1, STRING2, ARRAY1, HASH1].each do |obj|
102
+ block.call(obj)
103
+ end
104
+ end
105
+
106
+ def self.size_of(obj)
107
+ case obj
108
+ when STRING1 then convert_to_mb(10)
109
+ when STRING2 then convert_to_mb(20)
110
+ when ARRAY1 then convert_to_mb(90)
111
+ when HASH1 then convert_to_mb(0.005)
112
+ else
113
+ 0
114
+ end
115
+ end
116
+
117
+ def self.convert_to_mb(integer)
118
+ integer * 1024**2
119
+ end
120
+ end
121
+
122
+ let(:options) {
123
+ {
124
+ :memory_tracker => StubbedMemoryTracker,
125
+ :threshold => 0.05
126
+ }
127
+ }
128
+ let(:analyzer) { described_class.new(options) }
129
+
130
+ it 'should try to initiate garbage collection' do
131
+ GC.should_receive(:start)
132
+
133
+ analyzer.analyze
134
+ end
135
+
136
+ it 'should calculate the total memory allocated to each class' do
137
+ analysis = analyzer.analyze
138
+
139
+ analysis.should_not be_nil
140
+ analysis['Array'][:size].should == 90.0
141
+ analysis['String'][:size].should == 20.0 + 10.0
142
+ end
143
+
144
+ it 'should calculate the number of objects of each class in memory' do
145
+ analysis = analyzer.analyze
146
+
147
+ analysis.should_not be_nil
148
+ analysis['Array'][:count].should == 1
149
+ analysis['String'][:count].should == 2
150
+ end
151
+
152
+ it 'should isolate classes that exceed a given memory threshold' do
153
+ analysis = analyzer.analyze
154
+
155
+ analysis.should_not be_nil
156
+ analysis['Hash'].should be_nil # doesn't meet threshold
157
+ end
158
+
159
+ it 'should generate an aggregated Others* row' do
160
+ analysis = analyzer.analyze
161
+
162
+ analysis.should_not be_nil
163
+ analysis['Others*'][:size].should == 0.005
164
+ analysis['Others*'][:count].should == 1
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Memtf::Persistance do
4
+ include FakeFS::SpecHelpers
5
+
6
+ let(:name) { 'test_name' }
7
+ let(:group) { 'test_group' }
8
+ let(:date) { '2013-12-26' }
9
+ let(:pid) { 9 }
10
+
11
+ let(:expected_dir) { "tmp/memtf/#{group}" }
12
+ let(:expected_file_path) { "#{expected_dir}/#{name}-#{pid}.json" }
13
+
14
+ before(:each) do
15
+ Process.stub(:pid).and_return(pid)
16
+ end
17
+
18
+ describe '.save' do
19
+ let(:payload) {{ :test => :payload }}
20
+
21
+ context 'when the group directory does not exist' do
22
+ it 'should create the directory' do
23
+ Dir.exist?(expected_dir).should == false
24
+ described_class.save(name, group, payload)
25
+ Dir.exist?(expected_dir).should == true
26
+ end
27
+ end
28
+
29
+ it 'should save the payload' do
30
+ expected_file_path = "#{expected_dir}/#{name}-#{pid}.json"
31
+ File.exist?(expected_file_path).should == false
32
+ described_class.save(name, group, payload)
33
+ File.exist?(expected_file_path).should == true
34
+ end
35
+
36
+ it 'should encode the payload' do
37
+ described_class.save(name, group, payload)
38
+ f = File.read(expected_file_path)
39
+ expect(f).to eq "{\"test\":\"payload\"}"
40
+ end
41
+ end
42
+
43
+ describe '.load' do
44
+ before(:each) do
45
+ FileUtils.mkdir_p(expected_dir)
46
+ File.open(expected_file_path, 'w+') do |f|
47
+ f.print("{\"test\":\"payload\"}")
48
+ end
49
+ end
50
+
51
+ it 'should load the payload' do
52
+ payload = described_class.load(name, group)
53
+ expect(payload).not_to be_nil
54
+ end
55
+
56
+ it 'should decode the payload' do
57
+ payload = described_class.load(name, group)
58
+ expect(payload).to eq({ 'test' => 'payload' })
59
+ end
60
+
61
+ context 'when the file does not exist' do
62
+ it 'should raise an Errno::ENOENT error' do
63
+ expect {
64
+ described_class.load("bogus_name", group)
65
+ }.to raise_error(Errno::ENOENT)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe Memtf::Reporter do
4
+
5
+ let(:group) { 'test_group' }
6
+ let(:reporter) { described_class.new(group) }
7
+
8
+ describe '.report' do
9
+
10
+ it 'should initialize a new reporter' do
11
+ reporter.stub(:report)
12
+ described_class.should_receive(:new).with(group).and_return(reporter)
13
+
14
+ described_class.report(group)
15
+ end
16
+
17
+ it 'should delegate to report' do
18
+ described_class.stub(:new).and_return(reporter)
19
+ reporter.should_receive(:report)
20
+
21
+ described_class.report(group)
22
+ end
23
+ end
24
+
25
+ describe '#report' do
26
+ let(:analysis) {
27
+ {
28
+ 'key1' => {'size' => 2, 'size_delta' => 0, 'count' => 1000, 'count_delta' => 50, 'impact' => 0.02},
29
+ 'key2' => {'size' => 78, 'size_delta' => 76, 'count' => 600, 'count_delta' => 500, 'impact' => 0.78},
30
+ 'key3' => {'size' => 20, 'size_delta' => 15, 'count' => 50, 'count_delta' => 25, 'impact' => 0.20}
31
+ }
32
+ }
33
+
34
+ it 'should delegate to Memtf::Analyzer.analyze_group' do
35
+ Memtf::Analyzer.should_receive(:analyze_group).with(group).and_return(analysis)
36
+
37
+ reporter.report
38
+ end
39
+
40
+ it 'should sort the analysis by impact' do
41
+ Memtf::Analyzer.stub(:analyze_group).with(group).and_return(analysis)
42
+
43
+ table = reporter.report
44
+ table.rows.map {|r| r.cells.first.value }.should == ['key2', 'key3', 'key1']
45
+ end
46
+
47
+ it 'should print headers' do
48
+ Memtf::Analyzer.stub(:analyze_group).with(group).and_return(analysis)
49
+
50
+ table = reporter.report
51
+ table.headings.cells.map(&:value).should == ["Class", "Impact", "Leakage", "Change", "Objects", "Change"]
52
+ end
53
+
54
+ it 'should convert impact to a human readable percentage' do
55
+ Memtf::Analyzer.stub(:analyze_group).with(group).and_return(analysis)
56
+
57
+ table = reporter.report
58
+ table.rows.map {|r| r.cells[1].value }.should == ['78.00%','20.00%','2.00%']
59
+ end
60
+
61
+ it 'should convert sizes to human readable MB' do
62
+ Memtf::Analyzer.stub(:analyze_group).with(group).and_return(analysis)
63
+
64
+ table = reporter.report
65
+ table.rows.map {|r| r.cells[2].value }.should == ['78.000MB','20.000MB','2.000MB']
66
+ table.rows.map {|r| r.cells[3].value }.should == ['76.000MB','15.000MB','0.000MB']
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -58,4 +58,4 @@ describe Memtf do
58
58
  Memtf.around(options,&lambda)
59
59
  end
60
60
  end
61
- end
61
+ end
@@ -1,2 +1,30 @@
1
+ require 'fakefs/spec_helpers'
1
2
  require 'simplecov'
3
+
2
4
  require 'memtf'
5
+
6
+ # Silence stdout:
7
+ # https://gist.github.com/adamstegman/926858
8
+
9
+ # Redirects stderr and stdout to /dev/null.
10
+ def silence_output
11
+ @orig_stderr = $stderr
12
+ @orig_stdout = $stdout
13
+
14
+ # redirect stderr and stdout to /dev/null
15
+ $stderr = File.new('/dev/null', 'w')
16
+ $stdout = File.new('/dev/null', 'w')
17
+ end
18
+
19
+ # Replace stdout and stderr so anything else is output correctly.
20
+ def enable_output
21
+ $stderr = @orig_stderr
22
+ $stdout = @orig_stdout
23
+ @orig_stderr = nil
24
+ @orig_stdout = nil
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.before(:all) { silence_output }
29
+ config.after(:all) { enable_output }
30
+ end
metadata CHANGED
@@ -1,139 +1,153 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memtf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Dressel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-24 00:00:00.000000000 Z
11
+ date: 2013-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: terminal-table
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ~>
46
46
  - !ruby/object:Gem::Version
47
47
  version: '1.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ~>
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.3'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: fakefs
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: guard-rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - '>='
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - '>='
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: pry
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - '>='
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: guard-rspec
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - ">="
115
+ - - '>='
102
116
  - !ruby/object:Gem::Version
103
117
  version: '0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - ">="
122
+ - - '>='
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: simplecov
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
- - - ">="
129
+ - - '>='
116
130
  - !ruby/object:Gem::Version
117
131
  version: '0'
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
- - - ">="
136
+ - - '>='
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: yard
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
- - - ">="
143
+ - - '>='
130
144
  - !ruby/object:Gem::Version
131
145
  version: '0'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
- - - ">="
150
+ - - '>='
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  description: A simple utility to help you isolate the little bastards that are stealing
@@ -144,10 +158,10 @@ executables: []
144
158
  extensions: []
145
159
  extra_rdoc_files: []
146
160
  files:
147
- - ".gitignore"
148
- - ".ruby-gemset"
149
- - ".ruby-version"
150
- - ".travis.yml"
161
+ - .gitignore
162
+ - .ruby-gemset
163
+ - .ruby-version
164
+ - .travis.yml
151
165
  - CHANGELOG.md
152
166
  - Gemfile
153
167
  - Guardfile
@@ -156,6 +170,7 @@ files:
156
170
  - Rakefile
157
171
  - lib/memtf.rb
158
172
  - lib/memtf/analyzer.rb
173
+ - lib/memtf/analyzer/memory.rb
159
174
  - lib/memtf/persistance.rb
160
175
  - lib/memtf/reporter.rb
161
176
  - lib/memtf/runner.rb
@@ -164,6 +179,9 @@ files:
164
179
  - lib/memtf/version.rb
165
180
  - memtf.gemspec
166
181
  - spec/integration/memtf_spec.rb
182
+ - spec/lib/memtf/analyzer_spec.rb
183
+ - spec/lib/memtf/persistance_spec.rb
184
+ - spec/lib/memtf/reporter_spec.rb
167
185
  - spec/lib/memtf_spec.rb
168
186
  - spec/spec_helper.rb
169
187
  homepage: http://github.com/dresselm/memtf
@@ -176,22 +194,25 @@ require_paths:
176
194
  - lib
177
195
  required_ruby_version: !ruby/object:Gem::Requirement
178
196
  requirements:
179
- - - ">="
197
+ - - '>='
180
198
  - !ruby/object:Gem::Version
181
199
  version: '0'
182
200
  required_rubygems_version: !ruby/object:Gem::Requirement
183
201
  requirements:
184
- - - ">="
202
+ - - '>='
185
203
  - !ruby/object:Gem::Version
186
204
  version: '0'
187
205
  requirements: []
188
206
  rubyforge_project:
189
- rubygems_version: 2.2.0
207
+ rubygems_version: 2.0.14
190
208
  signing_key:
191
209
  specification_version: 4
192
210
  summary: Leaking memory like a sieve? Cursing? Memtf is here to help.
193
211
  test_files:
194
212
  - spec/integration/memtf_spec.rb
213
+ - spec/lib/memtf/analyzer_spec.rb
214
+ - spec/lib/memtf/persistance_spec.rb
215
+ - spec/lib/memtf/reporter_spec.rb
195
216
  - spec/lib/memtf_spec.rb
196
217
  - spec/spec_helper.rb
197
218
  has_rdoc: