fiveruns_tuneup 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|