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.
- data/History.rdoc +3 -0
- data/Manifest +57 -0
- data/README.rdoc +44 -0
- data/Rakefile +15 -0
- data/assets/images/arrows.gif +0 -0
- data/assets/images/edit.png +0 -0
- data/assets/images/fade.png +0 -0
- data/assets/images/fade_down.png +0 -0
- data/assets/images/head.gif +0 -0
- data/assets/images/logo.gif +0 -0
- data/assets/images/logo_clear.png +0 -0
- data/assets/images/magnify.png +0 -0
- data/assets/images/pip.gif +0 -0
- data/assets/images/pointer.gif +0 -0
- data/assets/images/schema.png +0 -0
- data/assets/images/signin.gif +0 -0
- data/assets/images/spinner.gif +0 -0
- data/assets/images/warning.gif +0 -0
- data/assets/javascripts/prototype.js +2515 -0
- data/assets/javascripts/tuneup.js +30 -0
- data/assets/stylesheets/tuneup.css +204 -0
- data/bin/fiveruns_tuneup +26 -0
- data/fiveruns_tuneup.gemspec +49 -0
- data/init.rb +2 -0
- data/install.rb +18 -0
- data/lib/bumpspark_helper.rb +52 -0
- data/lib/fiveruns/tuneup.rb +103 -0
- data/lib/fiveruns/tuneup/asset_tags.rb +39 -0
- data/lib/fiveruns/tuneup/custom_methods.rb +8 -0
- data/lib/fiveruns/tuneup/environment.rb +29 -0
- data/lib/fiveruns/tuneup/instrumentation/action_controller/base.rb +59 -0
- data/lib/fiveruns/tuneup/instrumentation/action_view/base.rb +77 -0
- data/lib/fiveruns/tuneup/instrumentation/active_record/base.rb +126 -0
- data/lib/fiveruns/tuneup/instrumentation/cgi/session.rb +30 -0
- data/lib/fiveruns/tuneup/instrumentation/utilities.rb +172 -0
- data/lib/fiveruns/tuneup/multipart.rb +75 -0
- data/lib/fiveruns/tuneup/runs.rb +86 -0
- data/lib/fiveruns/tuneup/schema.rb +43 -0
- data/lib/fiveruns/tuneup/step.rb +219 -0
- data/lib/fiveruns/tuneup/urls.rb +23 -0
- data/lib/fiveruns/tuneup/version.rb +80 -0
- data/lib/fiveruns_tuneup.rb +1 -0
- data/lib/tuneup_config.rb +29 -0
- data/lib/tuneup_controller.rb +140 -0
- data/lib/tuneup_helper.rb +185 -0
- data/rails/init.rb +20 -0
- data/tasks/assets.rake +32 -0
- data/test/test_helper.rb +3 -0
- data/test/tuneup_test.rb +0 -0
- data/uninstall.rb +6 -0
- data/views/tuneup/_data.html.erb +15 -0
- data/views/tuneup/_flash.html.erb +6 -0
- data/views/tuneup/_link.html.erb +1 -0
- data/views/tuneup/_schema.html.erb +17 -0
- data/views/tuneup/_sql.html.erb +23 -0
- data/views/tuneup/_step.html.erb +15 -0
- data/views/tuneup/panel/_registered.html.erb +4 -0
- data/views/tuneup/panel/_unregistered.html.erb +14 -0
- 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
|