memory_tracker 1.0.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.
@@ -0,0 +1,63 @@
1
+ module MemoryTracker
2
+ class GcStat
3
+ attr_reader :stats
4
+
5
+ def initialize(rss, vsize)
6
+ @stats = GC.stat.merge({ :rss => rss, :vsize => vsize})
7
+ end
8
+
9
+ def ordered_keys
10
+ @stats.keys.sort
11
+ end
12
+
13
+ def ordered_values
14
+ ordered_keys.inject([]) do |vals, key|
15
+ vals << @stats[key]
16
+ vals
17
+ end
18
+ end
19
+
20
+ def self.gcdiff(before, after)
21
+ return {} unless (before && before[:total_allocated_object] && before[:total_freed_object])
22
+ return {} unless (after && after[:total_allocated_object] && after[:total_freed_object])
23
+ diff = {}
24
+ b = before.clone
25
+ a = after.clone
26
+ diff[:num_alloc] = a[:total_allocated_object] - b[:total_allocated_object]
27
+ diff[:num_heaps] = a[:heap_used]
28
+ [ a, b ].each do |x|
29
+ x.delete(:heap_increment)
30
+ x.delete(:heap_length)
31
+ x.delete(:heap_final_num)
32
+ x[:in_use] = x.delete(:total_allocated_object) - x.delete(:total_freed_object)
33
+ end
34
+ b.each_key do |key|
35
+ diff[key] = a[key] - b[key]
36
+ end
37
+ diff
38
+ end
39
+ end
40
+
41
+ class GcStatDelta
42
+ attr_reader :stats
43
+
44
+ def initialize(before, after)
45
+ @after = after
46
+ @stats = after.stats.inject({}) do |h, (k, v)|
47
+ h[k] = after.stats[k] - before.stats[k]
48
+ h
49
+ end
50
+ end
51
+
52
+ def custom
53
+ return unless stats[:total_allocated_object] && stats[:total_freed_object]
54
+ h = {}
55
+ h[:total_allocated_object] = stats[:total_allocated_object]
56
+ h[:count] = stats[:count]
57
+ h[:rss] = stats[:rss]
58
+ h[:heap_used] = @after.stats[:heap_used]
59
+ h[:in_use] = @after.stats[:total_allocated_object] - @after.stats[:total_freed_object]
60
+ h
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ module MemoryTracker
2
+ class MemoryTracker
3
+ include Singleton
4
+ include Enumerable
5
+
6
+ attr_accessor :gcstat_logger
7
+
8
+ def stores
9
+ @stores ||= {}
10
+ end
11
+
12
+ def add_store(store)
13
+ stores[store.name] = store
14
+ end
15
+
16
+ def start_request(env)
17
+ @request = Request.new(env)
18
+ end
19
+
20
+ def end_request
21
+ return unless @request
22
+ @request.close
23
+ stores.each { |name, store| store.push(@request) }
24
+
25
+ @request = nil
26
+ end
27
+
28
+ def stats(store_name)
29
+ stores[store_name].stats
30
+ end
31
+
32
+ def self.track_block(*args)
33
+ self.instance.track_block(*args)
34
+ end
35
+
36
+ def track_block(name, &block)
37
+ raise ArgumentError unless block_given?
38
+ before = GC.stat
39
+ ret = yield
40
+ after = GC.stat
41
+ gcstat_logger.debug "gcstat diff for #{name}: #{GcStat.gcdiff(before, after)}"
42
+ ret
43
+ end
44
+
45
+ private
46
+
47
+ def each_store
48
+ stores.each
49
+ end
50
+
51
+ def each
52
+ each_store
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ module MemoryTracker
2
+ # Middleware responsability is to initialize and close request objects
3
+ # object at start and end of HTTP query.
4
+ class Middleware
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def memory_tracker
10
+ ::MemoryTracker::MemoryTracker.instance
11
+ end
12
+
13
+ def call(env)
14
+ memory_tracker.start_request(Env.new(env))
15
+ status, headers, body = @app.call(env)
16
+ ensure
17
+ memory_tracker.end_request
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module MemoryTracker
2
+ class Request
3
+ include Sys
4
+
5
+ attr_reader :start_gcstat, :end_gcstat
6
+ attr_reader :gcstat_delta
7
+ attr_reader :rss, :vsize
8
+
9
+ extend Forwardable
10
+ def_delegators :@env, :path, :controller, :action
11
+
12
+ def initialize(env)
13
+ @env = env
14
+ @start_gcstat = GcStat.new(self.class.rss, self.class.vsize)
15
+ end
16
+
17
+ def close
18
+ @end_gcstat = GcStat.new(self.class.rss, self.class.vsize)
19
+ @gcstat_delta = GcStatDelta.new(@start_gcstat, @end_gcstat)
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def self.rss
26
+ rss = ProcTable.ps(Process.pid).rss * 0.004096
27
+ end
28
+
29
+ def self.vsize
30
+ vsize = ProcTable.ps(Process.pid).vsize * 0.000001
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ module MemoryTracker
2
+ module Stores
3
+ class GcstatLogfileStore
4
+ def initialize(logger_class, logfile_path)
5
+ @logger = logger_class.new(logfile_path)
6
+ @num_lines = 0
7
+ end
8
+
9
+ def name
10
+ :gcstat_logfile
11
+ end
12
+
13
+ def push(request)
14
+ @request = request
15
+
16
+ write_header if @num_lines % 1000 == 0
17
+ write_request_log
18
+ @num_lines += 1
19
+ end
20
+
21
+ def stats
22
+ end
23
+
24
+ private
25
+
26
+ def write_header
27
+ @logger.info "##{@request.end_gcstat.ordered_keys.join(',')}"
28
+ end
29
+
30
+ def write_request_log
31
+ @logger.info logline
32
+ end
33
+
34
+ def logline
35
+ @request.end_gcstat.ordered_values.join ','
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,120 @@
1
+ module MemoryTracker
2
+ module Stores
3
+ module InMemoryStore
4
+ class Manager
5
+ def initialize(window_length = 60*60*4)
6
+ @length = window_length
7
+ @window1 = StatInterval.new(Time.now - @length/2, @length)
8
+ @window2 = StatInterval.new(Time.now, @length)
9
+ end
10
+
11
+ def name
12
+ :memory
13
+ end
14
+
15
+ def push(request)
16
+ rotate_windows
17
+ @window1.push(request)
18
+ @window2.push(request)
19
+ end
20
+
21
+ def stats
22
+ rotate_windows
23
+ @window1.stats
24
+ end
25
+
26
+ private
27
+ def rotate_windows
28
+ if Time.now > @window1.start_time + @length
29
+ @window1 = @window2
30
+ @window2 = StatInterval.new(Time.now, @length)
31
+ end
32
+ end
33
+ end
34
+
35
+ class StatInterval
36
+ attr_reader :start_time, :duration, :size, :stats
37
+
38
+ extend Forwardable
39
+ def_delegators :@stats, :fetch, :each
40
+
41
+ include Enumerable
42
+
43
+ def initialize(start_time, duration_seconds)
44
+ @start_time = start_time
45
+ @duration = duration_seconds
46
+ @size = 0
47
+ @stats = Stats.new
48
+ end
49
+
50
+ def push(request)
51
+ @size += 1
52
+ delta = request.gcstat_delta.stats
53
+ @stats.increment_action_count(request.controller, request.action)
54
+ delta.each_key do |attr|
55
+ @stats.increment_action_attribute(request.controller, request.action, attr, delta[attr])
56
+ end
57
+ end
58
+ end
59
+
60
+ class Stats
61
+ def initialize
62
+ @data = {}
63
+ end
64
+
65
+ def fetch(controller, action, attr)
66
+ ca = controller_action_data(controller, action)
67
+ ca[:gcstat][attr]
68
+ end
69
+
70
+ def count(controller, action)
71
+ ca = controller_action_data(controller, action)
72
+ ca[:num]
73
+ end
74
+
75
+ def each(&block)
76
+ @data.each do |controller, h|
77
+ h.each do |action, stats|
78
+ yield [controller, action, stats]
79
+ end
80
+ end
81
+ end
82
+
83
+ def increment_action_count(controller, action)
84
+ ca = controller_action_data(controller, action)
85
+ ca[:num] += 1
86
+ end
87
+
88
+ def increment_action_attribute(controller, action, attr, value)
89
+ ca = controller_action_data(controller, action)
90
+ if ca[:gcstat][attr]
91
+ ca[:gcstat][attr] += value
92
+ else
93
+ ca[:gcstat][attr] = value
94
+ end
95
+ end
96
+
97
+
98
+ def to_a
99
+ a = []
100
+ @data.each do |controller, actions|
101
+ actions.each do |action, counters|
102
+ row = { :controller => controller, :action => action, :num => counters[:num] }
103
+ row.merge!(counters[:gcstat])
104
+ a << row
105
+ end
106
+ end
107
+ a
108
+ end
109
+
110
+ private
111
+
112
+ def controller_action_data(controller, action)
113
+ @data[controller] = { action => { :num => 0, :gcstat => {} } } unless @data[controller]
114
+ @data[controller][action] = { :num => 0, :gcstat => {} } unless @data[controller][action]
115
+ @data[controller][action]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,46 @@
1
+ module MemoryTracker
2
+ module Stores
3
+ class UrlLogfileStore
4
+ def initialize(logger_class, logfile_path)
5
+ @logger = logger_class.new(logfile_path)
6
+ end
7
+
8
+ def name
9
+ :url_logfile
10
+ end
11
+
12
+ def push(request)
13
+ @request = request
14
+
15
+ write_request_log
16
+ end
17
+
18
+ def stats
19
+ end
20
+
21
+ private
22
+
23
+ def write_request_log
24
+ @logger.info logline
25
+ end
26
+
27
+ def logline
28
+ pid = Process.pid
29
+
30
+ end_gcstats = @request.end_gcstat.stats
31
+ start_gcstats = @request.start_gcstat.stats
32
+ delta_gcstats = @request.gcstat_delta.stats
33
+
34
+ log_msg = "#{Time.now.localtime.strftime("%m-%d %H:%M:%S")} pid:#{'%05d' % pid}"
35
+ log_msg << " rss=#{'%6.2f' % end_gcstats[:rss]}"
36
+ log_msg << " vsize=#{'%6.2f' % end_gcstats[:vsize]}"
37
+
38
+ if (end_gcstats[:rss] / start_gcstats[:rss] > 1.005) || delta_gcstats[:heap_used] > 0
39
+ log_msg << " *** #{@request.gcstat_delta.custom.inspect}"
40
+ end
41
+
42
+ log_msg << " #{@request.path}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,86 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "memory_tracker"
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Philippe Le Rohellec"]
12
+ s.date = "2013-11-15"
13
+ s.description = "Collect and analyze memory usage data for each individual Rails action controller."
14
+ s.email = "philippe@lerohellec.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "app/controllers/memory_tracker/dashboards_controller.rb",
27
+ "app/helpers/memory_tracker/dashboards_helper.rb",
28
+ "app/views/layouts/memory_tracker.html.erb",
29
+ "app/views/memory_tracker/dashboards/index.html.erb",
30
+ "config/routes.rb",
31
+ "docs/design.rb",
32
+ "lib/memory_tracker.rb",
33
+ "lib/memory_tracker/engine.rb",
34
+ "lib/memory_tracker/env.rb",
35
+ "lib/memory_tracker/gc_stat.rb",
36
+ "lib/memory_tracker/memory_tracker.rb",
37
+ "lib/memory_tracker/middleware.rb",
38
+ "lib/memory_tracker/request.rb",
39
+ "lib/memory_tracker/stores/gcstat_logfile_store.rb",
40
+ "lib/memory_tracker/stores/in_memory_store.rb",
41
+ "lib/memory_tracker/stores/url_logfile_store.rb",
42
+ "memory_tracker.gemspec",
43
+ "spec/lib/memory_tracker_spec.rb",
44
+ "spec/lib/request_spec.rb",
45
+ "spec/lib/stores/gcstat_logfile_store_spec.rb",
46
+ "spec/lib/stores/in_memory_store_spec.rb",
47
+ "spec/lib/stores/url_logfile_store_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+ s.homepage = "http://github.com/plerohellec/memory_tracker"
51
+ s.licenses = ["MIT"]
52
+ s.require_paths = ["lib"]
53
+ s.rubygems_version = "2.0.3"
54
+ s.summary = "Rails memory allocations tracker"
55
+
56
+ if s.respond_to? :specification_version then
57
+ s.specification_version = 4
58
+
59
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
60
+ s.add_runtime_dependency(%q<sys-proctable>, [">= 0"])
61
+ s.add_development_dependency(%q<debugger>, [">= 0"])
62
+ s.add_development_dependency(%q<rspec>, ["~> 2.14.0"])
63
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
64
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
65
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.7"])
66
+ s.add_runtime_dependency(%q<sys-proctable>, [">= 0"])
67
+ else
68
+ s.add_dependency(%q<sys-proctable>, [">= 0"])
69
+ s.add_dependency(%q<debugger>, [">= 0"])
70
+ s.add_dependency(%q<rspec>, ["~> 2.14.0"])
71
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
74
+ s.add_dependency(%q<sys-proctable>, [">= 0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<sys-proctable>, [">= 0"])
78
+ s.add_dependency(%q<debugger>, [">= 0"])
79
+ s.add_dependency(%q<rspec>, ["~> 2.14.0"])
80
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
81
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
82
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
83
+ s.add_dependency(%q<sys-proctable>, [">= 0"])
84
+ end
85
+ end
86
+