ghazel-fiveruns_tuneup 0.8.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/CHANGELOG +33 -0
  2. data/CONTRIBUTORS +5 -0
  3. data/README.rdoc +65 -0
  4. data/assets/images/arrows.gif +0 -0
  5. data/assets/images/edit.png +0 -0
  6. data/assets/images/fade.png +0 -0
  7. data/assets/images/fade_down.png +0 -0
  8. data/assets/images/head.gif +0 -0
  9. data/assets/images/logo.gif +0 -0
  10. data/assets/images/logo_clear.png +0 -0
  11. data/assets/images/magnify.png +0 -0
  12. data/assets/images/pin.png +0 -0
  13. data/assets/images/pip.gif +0 -0
  14. data/assets/images/pointer.gif +0 -0
  15. data/assets/images/pushed_pin.png +0 -0
  16. data/assets/images/schema.png +0 -0
  17. data/assets/images/signin.gif +0 -0
  18. data/assets/images/spinner.gif +0 -0
  19. data/assets/images/warning.gif +0 -0
  20. data/assets/javascripts/init.js +12 -0
  21. data/assets/javascripts/prototype.js +2515 -0
  22. data/assets/javascripts/tuneup.js +115 -0
  23. data/assets/stylesheets/tuneup.css +209 -0
  24. data/init.rb +2 -0
  25. data/install.rb +13 -0
  26. data/lib/bumpspark_helper.rb +52 -0
  27. data/lib/fiveruns/tuneup.rb +154 -0
  28. data/lib/fiveruns/tuneup/asset_tags.rb +54 -0
  29. data/lib/fiveruns/tuneup/configuration.rb +25 -0
  30. data/lib/fiveruns/tuneup/custom_methods.rb +8 -0
  31. data/lib/fiveruns/tuneup/environment.rb +29 -0
  32. data/lib/fiveruns/tuneup/instrumentation/action_controller/base.rb +59 -0
  33. data/lib/fiveruns/tuneup/instrumentation/action_view/base.rb +81 -0
  34. data/lib/fiveruns/tuneup/instrumentation/action_view/partial_template.rb +28 -0
  35. data/lib/fiveruns/tuneup/instrumentation/active_record/base.rb +126 -0
  36. data/lib/fiveruns/tuneup/instrumentation/cgi/session.rb +30 -0
  37. data/lib/fiveruns/tuneup/instrumentation/utilities.rb +187 -0
  38. data/lib/fiveruns/tuneup/multipart.rb +75 -0
  39. data/lib/fiveruns/tuneup/routing.rb +25 -0
  40. data/lib/fiveruns/tuneup/runs.rb +86 -0
  41. data/lib/fiveruns/tuneup/schema.rb +43 -0
  42. data/lib/fiveruns/tuneup/step.rb +221 -0
  43. data/lib/fiveruns/tuneup/version.rb +89 -0
  44. data/lib/fiveruns_tuneup.rb +11 -0
  45. data/lib/tuneup_controller.rb +46 -0
  46. data/lib/tuneup_helper.rb +181 -0
  47. data/rails/init.rb +14 -0
  48. data/tasks/assets.rake +32 -0
  49. data/test/test_helper.rb +3 -0
  50. data/test/tuneup_test.rb +0 -0
  51. data/uninstall.rb +6 -0
  52. data/views/tuneup/_data.html.erb +15 -0
  53. data/views/tuneup/_flash.html.erb +6 -0
  54. data/views/tuneup/_link.html.erb +1 -0
  55. data/views/tuneup/_schema.html.erb +17 -0
  56. data/views/tuneup/_sql.html.erb +23 -0
  57. data/views/tuneup/_step.html.erb +17 -0
  58. data/views/tuneup/panel/_show.html.erb +4 -0
  59. data/views/tuneup/sandbox.html.erb +6 -0
  60. metadata +124 -0
@@ -0,0 +1,187 @@
1
+ module Fiveruns
2
+ module Tuneup
3
+ module Instrumentation
4
+ module Utilities
5
+
6
+ def stack
7
+ @stack ||= []
8
+ end
9
+
10
+ def exclusion_stack
11
+ @exclusion_stack ||= [0]
12
+ end
13
+
14
+ def custom_methods
15
+ @custom_methods ||= {}
16
+ end
17
+
18
+ def add_custom_methods(target, *methods)
19
+ custom_methods[target] = [] unless custom_methods.key?(target)
20
+ custom_methods[target].push(*methods)
21
+ end
22
+
23
+ def stopwatch
24
+ start = Time.now.to_f
25
+ yield
26
+ (Time.now.to_f - start) * 1000
27
+ end
28
+
29
+ def exclude
30
+ result = nil
31
+ exclusion_stack[-1] += stopwatch { result = yield }
32
+ result
33
+ end
34
+
35
+ def step(name, layer=nil, link=true, sql=nil, table_name=nil, &block)
36
+ if recording?
37
+ result = nil
38
+ caller_line = caller.detect { |l| l.include?(RAILS_ROOT) && l !~ /tuneup|vendor\/rails/ } if link
39
+ file, line = caller_line ? caller_line.split(':')[0, 2] : [nil, nil]
40
+ line = line.to_i if line
41
+ returning ::Fiveruns::Tuneup::Step.new(name, layer, file, line, sql, &block) do |step|
42
+ step.table_name = table_name
43
+ stack.last << step
44
+ stack << step
45
+ begin
46
+ handle_exclusions_in step do
47
+ step.time = stopwatch { result = yield(sql) }
48
+ end
49
+ ensure
50
+ stack.pop
51
+ end
52
+ end
53
+ result
54
+ else
55
+ yield(sql)
56
+ end
57
+ end
58
+
59
+ # Handle removal of excluded time from total for this step, and
60
+ # bubble the value up for removal from the parent step
61
+ def handle_exclusions_in(step)
62
+ exclusion_stack << 0
63
+ yield # Must set +step.time+
64
+ time_to_exclude = exclusion_stack.pop
65
+ step.time -= time_to_exclude
66
+ exclusion_stack[-1] += time_to_exclude unless exclusion_stack.blank?
67
+ end
68
+
69
+ def instrument(target, *mods)
70
+ mods.each do |mod|
71
+ # Change target for 'ClassMethods' module
72
+ real_target = mod.name.demodulize == 'ClassMethods' ? (class << target; self; end) : target
73
+ real_target.__send__(:include, mod)
74
+ # Find all the instrumentation hooks and chain them in
75
+ mod.instance_methods.each do |meth|
76
+ name = meth.to_s.sub('_with_fiveruns_tuneup', '')
77
+ real_target.alias_method_chain(name, :fiveruns_tuneup) rescue nil
78
+ end
79
+ end
80
+ end
81
+
82
+ def instrument_action_methods(controller)
83
+ klass = controller.class
84
+ actions_for(klass).each do |meth|
85
+ format = alias_format_for(meth)
86
+ next if controller.respond_to?(format % :with, true)
87
+ wrap(klass, format, meth, "Invoke #{meth} action", :controller)
88
+ end
89
+ end
90
+
91
+ def instrument_filters(controller)
92
+ klass = controller.class
93
+ filters_for(klass).each do |filter|
94
+ format = alias_format_for(name_of_filter(filter))
95
+ next if controller.respond_to?(format % :with, true)
96
+ wrap(klass, format, name_of_filter(filter), "#{filter.type.to_s.titleize} filter #{name_of_filter(filter)}", :controller)
97
+ end
98
+ end
99
+
100
+ def instrument_custom_methods
101
+ custom_methods.each do |meth_target, meths|
102
+ lineage = meth_target.ancestors
103
+ layer = if lineage.include?(ActionController::Base)
104
+ :controller
105
+ elsif lineage.include?(ActiveRecord::Base)
106
+ :model
107
+ elsif lineage.include?(ActionView::Base)
108
+ :view
109
+ else
110
+ :other
111
+ end
112
+ meths.each do |meth|
113
+ format = alias_format_for(meth)
114
+ wrap(meth_target, format, meth, "Method #{meth}", layer)
115
+ end
116
+ end
117
+ end
118
+
119
+ #######
120
+ private
121
+ #######
122
+
123
+ def wrap(klass, format, meth, name, layer)
124
+ return if klass.instance_methods.include?(format % :with)
125
+ text = <<-EOC
126
+ def #{format % :with}(*args, &block)
127
+ Fiveruns::Tuneup.step "#{name}", :#{layer} do
128
+ #{format % :without}(*args, &block)
129
+ end
130
+ end
131
+ alias_method_chain :#{meth}, :fiveruns_tuneup
132
+ EOC
133
+ begin
134
+ klass.class_eval text
135
+ rescue SyntaxError => e
136
+ # XXX: Catch-all for reports of oddly-named methods affecting dynamically generated code
137
+ log :warn, %[Bad syntax wrapping #{klass}##{meth}, "#{name}"]
138
+ end
139
+ end
140
+
141
+ def alias_format_for(name)
142
+ name.to_s =~ /^(.*?)(\?|!|=)$/ ? "#{$1}_%s_fiveruns_tuneup#{$2}" : "#{name}_%s_fiveruns_tuneup"
143
+ end
144
+
145
+ def actions_for(klass)
146
+ klass.action_methods.reject { |meth| meth.to_s.include?('fiveruns') }
147
+ end
148
+
149
+ def filters_for(klass)
150
+ klass.filter_chain.select { |f| name_of_filter(f).is_a?(Symbol) }
151
+ end
152
+
153
+ def name_of_filter(filter)
154
+ if filter.respond_to?(:filter)
155
+ filter.filter
156
+ else
157
+ filter.method
158
+ end
159
+ end
160
+
161
+ def install_instrumentation
162
+ instrumentation_path = File.dirname(__FILE__)
163
+ Dir[File.join(instrumentation_path, '/*/**/*.rb')].each do |filename|
164
+ constant_path = filename[(instrumentation_path.size + 1)..-4]
165
+ constant_name = path_to_constant_name(constant_path)
166
+
167
+ instrumentation = "Fiveruns::Tuneup::Instrumentation::#{constant_name}".constantize
168
+ next if instrumentation.respond_to?(:relevant?) && !instrumentation.relevant?
169
+
170
+ if (constant = constant_name.constantize rescue nil)
171
+ constant.__send__(:include, instrumentation)
172
+ else
173
+ log :debug, "#{constant_name} not found; skipping instrumentation."
174
+ end
175
+ end
176
+ end
177
+
178
+ def path_to_constant_name(path)
179
+ parts = path.split(File::SEPARATOR)
180
+ parts.map(&:camelize).join('::').sub('Cgi', 'CGI')
181
+ end
182
+
183
+ end
184
+ end
185
+ end
186
+ end
187
+
@@ -0,0 +1,75 @@
1
+ require 'net/http'
2
+ require 'cgi'
3
+
4
+ module Fiveruns
5
+
6
+ module Tuneup
7
+
8
+ class Multipart
9
+
10
+ BOUNDARY_ROOT = 'B0UND~F0R~UPL0AD'
11
+
12
+ attr_reader :file, :params
13
+ def initialize(file, params={})
14
+ @file = file
15
+ @params = params
16
+ end
17
+
18
+ def content_type
19
+ %(multipart/form-data, boundary="#{boundary}")
20
+ end
21
+
22
+ def to_s
23
+ %(#{parts}\r\n#{separator}--)
24
+ end
25
+
26
+ #######
27
+ private
28
+ #######
29
+
30
+ def boundary
31
+ "#{BOUNDARY_ROOT}*#{nonce}"
32
+ end
33
+
34
+ def parts
35
+ params.merge(:file => file).map do |name, value|
36
+ [
37
+ separator,
38
+ headers_for(name, value)
39
+ ].flatten.join(crlf) + crlf + crlf + content_of(value)
40
+ end.flatten.join(crlf)
41
+ end
42
+
43
+ def separator
44
+ %(--#{boundary})
45
+ end
46
+
47
+ def crlf
48
+ @crlf ||= "\r\n"
49
+ end
50
+
51
+ def headers_for(name, value)
52
+ if value.respond_to?(:read)
53
+ [
54
+ %(Content-Disposition: form-data; name="#{name}"; filename="#{File.basename(value.path)}"),
55
+ %(Content-Transfer-Encoding: binary),
56
+ %(Content-Type: application/octet-stream)
57
+ ]
58
+ else
59
+ [ %(Content-Disposition: form-data; name="#{name}") ]
60
+ end
61
+ end
62
+
63
+ def nonce
64
+ @nonce ||= (Time.now.utc.to_f * 1000).to_i
65
+ end
66
+
67
+ def content_of(value)
68
+ value.respond_to?(:read) ? value.read : value.to_s
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,25 @@
1
+ module Fiveruns::Tuneup
2
+
3
+ module Routing
4
+
5
+ def self.install
6
+ ActionController::Routing::RouteSet.send(:include, self)
7
+ end
8
+
9
+ def self.included(base)
10
+ base.alias_method_chain :draw, :fiveruns_tuneup
11
+ end
12
+ def draw_with_fiveruns_tuneup(*args, &block)
13
+ draw_without_fiveruns_tuneup(*args) do |map|
14
+ map.connect '/tuneup', :controller => 'tuneup', :action => 'show'
15
+ map.connect '/tuneup/:action', :controller => 'tuneup'
16
+ yield map
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+
25
+
@@ -0,0 +1,86 @@
1
+ module Fiveruns
2
+ module Tuneup
3
+ module Runs
4
+
5
+ def run_dir
6
+ @run_dir ||= File.join(RAILS_ROOT, 'tmp', 'tuneup', 'runs', RAILS_ENV)
7
+ end
8
+
9
+ def retrieve_run(run_id)
10
+ filename = filename_for(run_id)
11
+ if File.file?(filename)
12
+ load_from_file(filename)
13
+ else
14
+ log :error, "Couldn't find filename: #{filename}"
15
+ nil
16
+ end
17
+ end
18
+
19
+ def load_from_file(filename)
20
+ decompressed = Zlib::Inflate.inflate(File.open(filename, 'rb') { |f| f.read })
21
+ YAML.load(decompressed)
22
+ end
23
+
24
+ def last_filename_for_run_uri(uri)
25
+ filename_for(last_run_id_for(uri))
26
+ end
27
+
28
+ def last_run
29
+ last_file = sorted_run_files.last
30
+ load_from_file(last_file)
31
+ end
32
+
33
+ #######
34
+ private
35
+ #######
36
+
37
+ def sorted_run_files
38
+ Dir[File.join(run_dir, '*/*.gz')].sort_by do |f|
39
+ File.basename(f).split('_').first.to_i
40
+ end
41
+ end
42
+
43
+ def trend_for(run_id)
44
+ Dir[File.join(run_dir, File.dirname(run_id), "*.gz")].map do |filename|
45
+ Integer(File.basename(filename, '.yml.gz').split('_').last)
46
+ end
47
+ end
48
+
49
+ def last_run_id_for(url)
50
+ last_file = Dir[File.join(run_dir, stub(url), '*.gz')].last
51
+ if last_file
52
+ File.join(File.basename(File.dirname(last_file)), File.basename(last_file, '.yml.gz'))
53
+ end
54
+ end
55
+
56
+ # Use Run ID, current timestamp, and total time (in microseconds)
57
+ def generate_run_id(url, time)
58
+ timestamp = '%d' % (Time.now.to_f * 1000)
59
+ File.join(stub(url), timestamp.to_s << "_#{(time * 1000).to_i}")
60
+ end
61
+
62
+ def persist(run_id, environment, schemas, data)
63
+ log :info, "Persisting #{run_id}"
64
+ filename = filename_for(run_id)
65
+ FileUtils.mkdir_p File.dirname(filename)
66
+ compressed = Zlib::Deflate.deflate(package_for(run_id, environment, schemas, data).to_yaml)
67
+ File.open(filename, 'wb') { |f| f.write compressed }
68
+ end
69
+
70
+ def filename_for(run_id)
71
+ File.join(run_dir, run_id) << '.yml.gz'
72
+ end
73
+
74
+ def stub(url)
75
+ Digest::SHA1.hexdigest(url)
76
+ end
77
+
78
+ def package_for(run_id, environment, schemas, data)
79
+ {'id' => run_id, 'environment' => environment, 'schemas' => schemas, 'stack' => data}
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,43 @@
1
+ module Fiveruns
2
+ module Tuneup
3
+ module Schema
4
+
5
+ def schemas
6
+ @schemas ||= {}
7
+ end
8
+
9
+ def add_schema_for(table, connection)
10
+ schemas[table] ||= begin
11
+ {
12
+ :columns => columns_for(table, connection),
13
+ :indexes => indexes_for(table, connection)
14
+ }
15
+ end
16
+ end
17
+
18
+ #######
19
+ private
20
+ #######
21
+
22
+ def columns_for(table, connection)
23
+ connection.columns(table).map do |column|
24
+ extract(column, :name, :sql_type)
25
+ end
26
+ end
27
+
28
+ def indexes_for(table, connection)
29
+ connection.indexes(table).map do |index|
30
+ extract(index, :name, :unique, :columns)
31
+ end
32
+ end
33
+
34
+ def extract(obj, *fields)
35
+ fields.inject({}) do |hash, field|
36
+ hash[field] = obj.__send__(field)
37
+ hash
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,221 @@
1
+ module Fiveruns
2
+ module Tuneup
3
+
4
+ class RootStep
5
+
6
+ delegate :blank?, :to => :children
7
+ alias_method :id, :object_id # Avoid record identitication warnings
8
+
9
+ def self.layers
10
+ framework_layers + [:other]
11
+ end
12
+
13
+ def self.framework_layers
14
+ [:model, :view, :controller]
15
+ end
16
+
17
+ def schemas
18
+ @schemas ||= {}
19
+ end
20
+
21
+ def time
22
+ children.map(&:time).sum || 0
23
+ end
24
+
25
+ def depth
26
+ @depth ||= 0
27
+ end
28
+
29
+ def <<(child)
30
+ child.depth = depth + 1
31
+ children << child
32
+ end
33
+
34
+ def size
35
+ children.map(&:size).sum || 0
36
+ end
37
+
38
+ def children_with_disparity
39
+ children + [Step.disparity(disparity, self)]
40
+ end
41
+
42
+ def children
43
+ @children ||= []
44
+ end
45
+
46
+ def leaf?
47
+ children.blank?
48
+ end
49
+
50
+ def leaves
51
+ @leaves ||= begin
52
+ if children.blank?
53
+ [self]
54
+ else
55
+ children.map(&:leaves).flatten
56
+ end
57
+ end
58
+ end
59
+
60
+ def child_times_by_layer
61
+ @child_times_by_layer ||= children.inject(Hash.new(0)) do |totals, child|
62
+ child.percentages_by_layer.each do |layer, percentage|
63
+ totals[layer] += child.time * percentage
64
+ end
65
+ totals
66
+ end
67
+ end
68
+
69
+ def percentages_by_layer
70
+ @percentages_by_layer ||= begin
71
+ percentages = self.class.framework_layers.inject({}) do |map, layer|
72
+ map[layer] = if leaf?
73
+ self.layer == layer ? 1.0 : 0
74
+ else
75
+ result = child_times_by_layer[layer] / self.time
76
+ result = nil unless result.to_s =~ /\d/
77
+ result.is_a?(Numeric) ? result : 0 # TODO: Fix issue at source
78
+ end
79
+ map
80
+ end
81
+ fill percentages
82
+ end
83
+ end
84
+
85
+ #######
86
+ private
87
+ #######
88
+
89
+ def fill(percentages)
90
+ returning percentages do
91
+ percentages[:other] ||= 0
92
+ unless leaf?
93
+ if disparity > 0
94
+ percentages[layer] += disparity / self.time
95
+ end
96
+ end
97
+ total = percentages.values.sum
98
+ if total < 0.999
99
+ percentages[:other] += 1.0 - total
100
+ end
101
+ end
102
+ end
103
+
104
+ def disparity
105
+ @disparity ||= begin
106
+ child_total = children.map(&:time).sum || 0
107
+ disparity = time - child_total
108
+ disparity > 0 ? disparity : 0
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ class Step < RootStep
115
+
116
+ attr_reader :name, :layer, :file, :line, :sql
117
+ attr_accessor :table_name
118
+ attr_writer :time, :depth
119
+
120
+ def self.disparity(time, parent)
121
+ returning Step.new("Other", parent.layer) do |step|
122
+ step.time = time
123
+ end
124
+ end
125
+
126
+ def initialize(name, layer=nil, file=nil, line=nil, sql=nil)
127
+ @name = name
128
+ @layer = layer
129
+ @file = file
130
+ @line = line
131
+ @sql = sql
132
+ end
133
+
134
+ def time
135
+ # FIXME: rank hack to get around weird JRuby YAML bug
136
+ @time.respond_to?(:value) ? @time.value.to_f : @time || 0
137
+ end
138
+
139
+ def size
140
+ children.map(&:size).sum + 1
141
+ end
142
+
143
+ class SQL
144
+
145
+ attr_reader :query, :explain
146
+
147
+ def initialize(sql, connection)
148
+ @query = sql
149
+ @explain = explain_from(connection)
150
+ end
151
+
152
+ #######
153
+ private
154
+ #######
155
+
156
+ def explain_from(connection)
157
+ return nil unless @query =~ /^select\b/i
158
+ return nil unless connection.adapter_name == 'MySQL'
159
+ explain = Explain.new(@query, connection)
160
+ explain if explain.valid?
161
+ end
162
+
163
+ class Explain
164
+
165
+ attr_reader :fields, :rows
166
+
167
+ def initialize(sql, connection)
168
+ result = connection.execute("explain #{sql}")
169
+ @fields = fetch_fields_from(result)
170
+ @rows = fetch_rows_from(result)
171
+ result.free
172
+ add_schemas(connection)
173
+ @valid = true
174
+ rescue Exception
175
+ @valid = false
176
+ end
177
+
178
+ def valid?
179
+ @valid
180
+ end
181
+
182
+ def table_offset
183
+ @table_offset ||= @fields.index('table')
184
+ end
185
+
186
+ #######
187
+ private
188
+ #######
189
+
190
+ def fetch_fields_from(result)
191
+ result.fetch_fields.map(&:name)
192
+ end
193
+
194
+ def fetch_rows_from(result)
195
+ returning [] do |rows|
196
+ result.each do |row|
197
+ rows << row
198
+ end
199
+ end
200
+ end
201
+
202
+ def add_schemas(connection)
203
+ tables.each do |table|
204
+ Fiveruns::Tuneup.add_schema_for(table, connection)
205
+ end
206
+ end
207
+
208
+ def tables
209
+ return [] unless table_offset
210
+ @rows.map { |row| row[table_offset] }.compact
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ end
218
+
219
+ end
220
+ end
221
+