memory_tracker 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +31 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/app/controllers/memory_tracker/dashboards_controller.rb +24 -0
- data/app/helpers/memory_tracker/dashboards_helper.rb +7 -0
- data/app/views/layouts/memory_tracker.html.erb +29 -0
- data/app/views/memory_tracker/dashboards/index.html.erb +21 -0
- data/config/routes.rb +3 -0
- data/docs/design.rb +111 -0
- data/lib/memory_tracker.rb +3 -0
- data/lib/memory_tracker/engine.rb +23 -0
- data/lib/memory_tracker/env.rb +14 -0
- data/lib/memory_tracker/gc_stat.rb +63 -0
- data/lib/memory_tracker/memory_tracker.rb +55 -0
- data/lib/memory_tracker/middleware.rb +20 -0
- data/lib/memory_tracker/request.rb +33 -0
- data/lib/memory_tracker/stores/gcstat_logfile_store.rb +39 -0
- data/lib/memory_tracker/stores/in_memory_store.rb +120 -0
- data/lib/memory_tracker/stores/url_logfile_store.rb +46 -0
- data/memory_tracker.gemspec +86 -0
- data/spec/lib/memory_tracker_spec.rb +47 -0
- data/spec/lib/request_spec.rb +30 -0
- data/spec/lib/stores/gcstat_logfile_store_spec.rb +17 -0
- data/spec/lib/stores/in_memory_store_spec.rb +161 -0
- data/spec/lib/stores/url_logfile_store_spec.rb +17 -0
- data/spec/spec_helper.rb +35 -0
- metadata +173 -0
@@ -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
|
+
|