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.
- 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
|
+
|