fiveruns_tuneup 0.8.2

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.
Files changed (59) hide show
  1. data/History.rdoc +3 -0
  2. data/Manifest +57 -0
  3. data/README.rdoc +44 -0
  4. data/Rakefile +15 -0
  5. data/assets/images/arrows.gif +0 -0
  6. data/assets/images/edit.png +0 -0
  7. data/assets/images/fade.png +0 -0
  8. data/assets/images/fade_down.png +0 -0
  9. data/assets/images/head.gif +0 -0
  10. data/assets/images/logo.gif +0 -0
  11. data/assets/images/logo_clear.png +0 -0
  12. data/assets/images/magnify.png +0 -0
  13. data/assets/images/pip.gif +0 -0
  14. data/assets/images/pointer.gif +0 -0
  15. data/assets/images/schema.png +0 -0
  16. data/assets/images/signin.gif +0 -0
  17. data/assets/images/spinner.gif +0 -0
  18. data/assets/images/warning.gif +0 -0
  19. data/assets/javascripts/prototype.js +2515 -0
  20. data/assets/javascripts/tuneup.js +30 -0
  21. data/assets/stylesheets/tuneup.css +204 -0
  22. data/bin/fiveruns_tuneup +26 -0
  23. data/fiveruns_tuneup.gemspec +49 -0
  24. data/init.rb +2 -0
  25. data/install.rb +18 -0
  26. data/lib/bumpspark_helper.rb +52 -0
  27. data/lib/fiveruns/tuneup.rb +103 -0
  28. data/lib/fiveruns/tuneup/asset_tags.rb +39 -0
  29. data/lib/fiveruns/tuneup/custom_methods.rb +8 -0
  30. data/lib/fiveruns/tuneup/environment.rb +29 -0
  31. data/lib/fiveruns/tuneup/instrumentation/action_controller/base.rb +59 -0
  32. data/lib/fiveruns/tuneup/instrumentation/action_view/base.rb +77 -0
  33. data/lib/fiveruns/tuneup/instrumentation/active_record/base.rb +126 -0
  34. data/lib/fiveruns/tuneup/instrumentation/cgi/session.rb +30 -0
  35. data/lib/fiveruns/tuneup/instrumentation/utilities.rb +172 -0
  36. data/lib/fiveruns/tuneup/multipart.rb +75 -0
  37. data/lib/fiveruns/tuneup/runs.rb +86 -0
  38. data/lib/fiveruns/tuneup/schema.rb +43 -0
  39. data/lib/fiveruns/tuneup/step.rb +219 -0
  40. data/lib/fiveruns/tuneup/urls.rb +23 -0
  41. data/lib/fiveruns/tuneup/version.rb +80 -0
  42. data/lib/fiveruns_tuneup.rb +1 -0
  43. data/lib/tuneup_config.rb +29 -0
  44. data/lib/tuneup_controller.rb +140 -0
  45. data/lib/tuneup_helper.rb +185 -0
  46. data/rails/init.rb +20 -0
  47. data/tasks/assets.rake +32 -0
  48. data/test/test_helper.rb +3 -0
  49. data/test/tuneup_test.rb +0 -0
  50. data/uninstall.rb +6 -0
  51. data/views/tuneup/_data.html.erb +15 -0
  52. data/views/tuneup/_flash.html.erb +6 -0
  53. data/views/tuneup/_link.html.erb +1 -0
  54. data/views/tuneup/_schema.html.erb +17 -0
  55. data/views/tuneup/_sql.html.erb +23 -0
  56. data/views/tuneup/_step.html.erb +15 -0
  57. data/views/tuneup/panel/_registered.html.erb +4 -0
  58. data/views/tuneup/panel/_unregistered.html.erb +14 -0
  59. metadata +146 -0
@@ -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,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')
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,219 @@
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
+ unless leaf?
92
+ if disparity > 0
93
+ percentages[layer] += disparity / self.time
94
+ end
95
+ end
96
+ percentages[:other] ||= 0
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
+ time - child_total
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ class Step < RootStep
114
+
115
+ attr_reader :name, :layer, :file, :line, :sql
116
+ attr_accessor :table_name
117
+ attr_writer :time, :depth
118
+
119
+ def self.disparity(time, parent)
120
+ returning Step.new("Other", parent.layer) do |step|
121
+ step.time = time
122
+ end
123
+ end
124
+
125
+ def initialize(name, layer=nil, file=nil, line=nil, sql=nil)
126
+ @name = name
127
+ @layer = layer
128
+ @file = file
129
+ @line = line
130
+ @sql = sql
131
+ end
132
+
133
+ def time
134
+ @time || 0
135
+ end
136
+
137
+ def size
138
+ children.map(&:size).sum + 1
139
+ end
140
+
141
+ class SQL
142
+
143
+ attr_reader :query, :explain
144
+
145
+ def initialize(sql, connection)
146
+ @query = sql
147
+ @explain = explain_from(connection)
148
+ end
149
+
150
+ #######
151
+ private
152
+ #######
153
+
154
+ def explain_from(connection)
155
+ return nil unless @query =~ /^select\b/i
156
+ return nil unless connection.adapter_name == 'MySQL'
157
+ explain = Explain.new(@query, connection)
158
+ explain if explain.valid?
159
+ end
160
+
161
+ class Explain
162
+
163
+ attr_reader :fields, :rows
164
+
165
+ def initialize(sql, connection)
166
+ result = connection.execute("explain #{sql}")
167
+ @fields = fetch_fields_from(result)
168
+ @rows = fetch_rows_from(result)
169
+ result.free
170
+ add_schemas(connection)
171
+ @valid = true
172
+ rescue Exception
173
+ @valid = false
174
+ end
175
+
176
+ def valid?
177
+ @valid
178
+ end
179
+
180
+ def table_offset
181
+ @table_offset ||= @fields.index('table')
182
+ end
183
+
184
+ #######
185
+ private
186
+ #######
187
+
188
+ def fetch_fields_from(result)
189
+ result.fetch_fields.map(&:name)
190
+ end
191
+
192
+ def fetch_rows_from(result)
193
+ returning [] do |rows|
194
+ result.each do |row|
195
+ rows << row
196
+ end
197
+ end
198
+ end
199
+
200
+ def add_schemas(connection)
201
+ tables.each do |table|
202
+ Fiveruns::Tuneup.add_schema_for(table, connection)
203
+ end
204
+ end
205
+
206
+ def tables
207
+ return [] unless table_offset
208
+ @rows.map { |row| row[table_offset] }.compact
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ end
218
+ end
219
+
@@ -0,0 +1,23 @@
1
+ module Fiveruns
2
+ module Tuneup
3
+ module Urls
4
+
5
+ def collector_url
6
+ @collector_url ||= begin
7
+ url = ENV['TUNEUP_COLLECTOR'] || 'https://tuneup-collector.fiveruns.com'
8
+ url = "http://#{url}" unless url =~ /^http/
9
+ url
10
+ end
11
+ end
12
+
13
+ def frontend_url
14
+ @frontend_url ||= begin
15
+ url = ENV['TUNEUP_FRONTEND'] || 'https://tuneup.fiveruns.com'
16
+ url = "http://#{url}" unless url =~ /^http/
17
+ url
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end