memory_tracker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+