perfmonger 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.dir-locals.el +2 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +12 -0
- data/COPYING +674 -0
- data/Gemfile +5 -0
- data/HOWTO.md +15 -0
- data/NEWS +115 -0
- data/README.md +61 -0
- data/Rakefile +8 -0
- data/bin/perfmonger +6 -0
- data/data/NOTICE +8 -0
- data/data/Twitter_Bootstrap_LICENSE.txt +176 -0
- data/data/assets/css/bootstrap-responsive.css +1109 -0
- data/data/assets/css/bootstrap.css +6167 -0
- data/data/assets/css/perfmonger.css +17 -0
- data/data/assets/dashboard.erb +319 -0
- data/data/assets/img/glyphicons-halflings-white.png +0 -0
- data/data/assets/img/glyphicons-halflings.png +0 -0
- data/data/assets/js/bootstrap.js +2280 -0
- data/data/assets/js/bootstrap.min.js +6 -0
- data/data/assets/js/canvasjs.js +9042 -0
- data/data/assets/js/canvasjs.min.js +271 -0
- data/data/sysstat.ioconf +268 -0
- data/ext/perfmonger/extconf.rb +19 -0
- data/ext/perfmonger/perfmonger.h +58 -0
- data/ext/perfmonger/perfmonger_record.c +754 -0
- data/ext/perfmonger/sysstat/common.c +627 -0
- data/ext/perfmonger/sysstat/common.h +207 -0
- data/ext/perfmonger/sysstat/ioconf.c +515 -0
- data/ext/perfmonger/sysstat/ioconf.h +84 -0
- data/ext/perfmonger/sysstat/iostat.c +1100 -0
- data/ext/perfmonger/sysstat/iostat.h +121 -0
- data/ext/perfmonger/sysstat/libsysstat.h +19 -0
- data/ext/perfmonger/sysstat/mpstat.c +953 -0
- data/ext/perfmonger/sysstat/mpstat.h +79 -0
- data/ext/perfmonger/sysstat/rd_stats.c +2388 -0
- data/ext/perfmonger/sysstat/rd_stats.h +651 -0
- data/ext/perfmonger/sysstat/sysconfig.h +13 -0
- data/lib/perfmonger/cli.rb +115 -0
- data/lib/perfmonger/command/base_command.rb +39 -0
- data/lib/perfmonger/command/fingerprint.rb +453 -0
- data/lib/perfmonger/command/plot.rb +429 -0
- data/lib/perfmonger/command/record.rb +32 -0
- data/lib/perfmonger/command/record_option.rb +149 -0
- data/lib/perfmonger/command/server.rb +294 -0
- data/lib/perfmonger/command/stat.rb +60 -0
- data/lib/perfmonger/command/stat_option.rb +29 -0
- data/lib/perfmonger/command/summary.rb +402 -0
- data/lib/perfmonger/config.rb +6 -0
- data/lib/perfmonger/version.rb +5 -0
- data/lib/perfmonger.rb +12 -0
- data/misc/release-howto.txt +17 -0
- data/misc/sample-cpu.png +0 -0
- data/misc/sample-read-iops.png +0 -0
- data/perfmonger.gemspec +44 -0
- data/test/run-test.sh +39 -0
- data/test/spec/bin_spec.rb +37 -0
- data/test/spec/data/2devices.expected +42 -0
- data/test/spec/data/2devices.output +42 -0
- data/test/spec/spec_helper.rb +20 -0
- data/test/spec/summary_spec.rb +193 -0
- data/test/test-perfmonger.c +145 -0
- data/test/test.h +9 -0
- metadata +154 -0
@@ -0,0 +1,294 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
require 'json'
|
4
|
+
require 'webrick'
|
5
|
+
require 'thread'
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'erb'
|
9
|
+
|
10
|
+
# monkey patching HTTPResponse for server-sent events
|
11
|
+
class WEBrick::HTTPResponse
|
12
|
+
CRLF = "\r\n"
|
13
|
+
def send_body_io(socket)
|
14
|
+
begin
|
15
|
+
if @request_method == "HEAD"
|
16
|
+
# do nothing
|
17
|
+
elsif chunked?
|
18
|
+
begin
|
19
|
+
buf = ''
|
20
|
+
data = ''
|
21
|
+
while true
|
22
|
+
@body.readpartial( @buffer_size, buf ) # there is no need to clear buf?
|
23
|
+
data << format("%x", buf.bytesize) << CRLF
|
24
|
+
data << buf << CRLF
|
25
|
+
_write_data(socket, data)
|
26
|
+
data.clear
|
27
|
+
@sent_size += buf.bytesize
|
28
|
+
end
|
29
|
+
rescue EOFError # do nothing
|
30
|
+
rescue IOError # do nothing
|
31
|
+
end
|
32
|
+
_write_data(socket, "0#{CRLF}#{CRLF}")
|
33
|
+
else
|
34
|
+
size = @header['content-length'].to_i
|
35
|
+
_send_file(socket, @body, 0, size)
|
36
|
+
@sent_size = size
|
37
|
+
end
|
38
|
+
ensure
|
39
|
+
begin; @body.close; rescue IOError; end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
module PerfMonger
|
46
|
+
module Command
|
47
|
+
|
48
|
+
class ServerCommand < BaseCommand
|
49
|
+
register_command 'server', 'Launch self-contained HTML5 realtime graph server'
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@parser = OptionParser.new
|
53
|
+
@parser.banner = <<EOS
|
54
|
+
Usage: perfmonger server [options] -- [perfmonger record options]
|
55
|
+
|
56
|
+
Launch TCP and HTTP server which enables users to get current
|
57
|
+
perfmonger data record via network.
|
58
|
+
|
59
|
+
Options:
|
60
|
+
EOS
|
61
|
+
|
62
|
+
@hostname = `hostname -f`.strip
|
63
|
+
@http_hostname = nil
|
64
|
+
@http_port = 20202
|
65
|
+
@tcp_port = 20203
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_args(argv)
|
69
|
+
@parser.on('-H', '--hostname NAME', "Host name to display (default: #{@hostname})") do |hostname|
|
70
|
+
@hostname = hostname
|
71
|
+
end
|
72
|
+
|
73
|
+
@parser.on('--http-hostname NAME',
|
74
|
+
"Host name for HTTP server URL. If not specified, value of '--hostname' option is used.") do |hostname|
|
75
|
+
@http_hostname = hostname
|
76
|
+
end
|
77
|
+
|
78
|
+
@parser.on('--http-port PORT', 'HTTP server port to listen.') do |port|
|
79
|
+
if ! port =~ /\A\d+\Z/
|
80
|
+
puts("ERROR: invalid port number value: #{port}")
|
81
|
+
puts(@parser.help)
|
82
|
+
exit(false)
|
83
|
+
end
|
84
|
+
@http_port = port.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
# @parser.on('--tcp-port PORT', 'TCP data server port to listen.') do |port|
|
88
|
+
# if ! port =~ /\A\d+\Z/
|
89
|
+
# puts("ERROR: invalid port number value: #{port}")
|
90
|
+
# puts(@parser.help)
|
91
|
+
# exit(false)
|
92
|
+
# end
|
93
|
+
# @tcp_port = port.to_i
|
94
|
+
# end
|
95
|
+
|
96
|
+
@parser.parse!(argv)
|
97
|
+
|
98
|
+
if @http_hostname.nil?
|
99
|
+
@http_hostname = @hostname
|
100
|
+
end
|
101
|
+
|
102
|
+
@record_cmd_args = argv
|
103
|
+
end
|
104
|
+
|
105
|
+
def run(argv)
|
106
|
+
tmp_rootdir = Dir.mktmpdir
|
107
|
+
parse_args(argv)
|
108
|
+
|
109
|
+
_, record_option = PerfMonger::Command::RecordOption.parse(@record_cmd_args)
|
110
|
+
|
111
|
+
# find perfmonger command
|
112
|
+
perfmonger_bin = File.expand_path('bin/perfmonger', PerfMonger::ROOTDIR)
|
113
|
+
if ! File.executable?(perfmonger_bin)
|
114
|
+
puts("ERROR: perfmonger not found!")
|
115
|
+
exit(false)
|
116
|
+
end
|
117
|
+
|
118
|
+
record_cmd = [perfmonger_bin, 'record',
|
119
|
+
*@record_cmd_args]
|
120
|
+
|
121
|
+
@recorder = Recorder.new(record_cmd).start
|
122
|
+
|
123
|
+
puts("PerfMonger Realtime Monitor: http://#{@http_hostname}:#{@http_port}/dashboard")
|
124
|
+
puts("")
|
125
|
+
|
126
|
+
@http_server = WEBrick::HTTPServer.new({:DocumentRoot => tmp_rootdir,
|
127
|
+
:BindAddress => '0.0.0.0',
|
128
|
+
:Port => @http_port})
|
129
|
+
setup_webrick(@http_server, record_option)
|
130
|
+
|
131
|
+
trap(:INT) do
|
132
|
+
@http_server.stop
|
133
|
+
@recorder.stop
|
134
|
+
end
|
135
|
+
@http_server.start
|
136
|
+
ensure
|
137
|
+
FileUtils.rm_rf(tmp_rootdir)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
class Recorder
|
142
|
+
def initialize(record_cmd)
|
143
|
+
@current_record = nil
|
144
|
+
@mutex = Mutex.new
|
145
|
+
@cond = ConditionVariable.new
|
146
|
+
@thread = nil
|
147
|
+
@record_cmd = record_cmd
|
148
|
+
|
149
|
+
@working = false
|
150
|
+
end
|
151
|
+
|
152
|
+
def start
|
153
|
+
@mutex.synchronize do
|
154
|
+
if @thread.nil?
|
155
|
+
@thread = true
|
156
|
+
@working = true
|
157
|
+
else
|
158
|
+
return
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
@thread = Thread.start do
|
163
|
+
begin
|
164
|
+
IO.popen(@record_cmd, "r") do |io|
|
165
|
+
io.each_line do |line|
|
166
|
+
@mutex.synchronize do
|
167
|
+
@current_perf_data = line.strip
|
168
|
+
@cond.broadcast
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
rescue Exception => err
|
173
|
+
puts("ERROR: Exception in record_thread(#{@record_thread}) in perfmonger-server")
|
174
|
+
puts("#{err.class.to_s}: #{err.message}")
|
175
|
+
puts(err.backtrace)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
self
|
180
|
+
end
|
181
|
+
|
182
|
+
def stop
|
183
|
+
@mutex.synchronize do
|
184
|
+
@working = false
|
185
|
+
@thread.terminate
|
186
|
+
@cond.broadcast
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def get_current_record
|
191
|
+
@mutex.synchronize do
|
192
|
+
if @working
|
193
|
+
@cond.wait(@mutex)
|
194
|
+
current_perf_data = @current_perf_data
|
195
|
+
else
|
196
|
+
raise EOFError
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
class DashboardServlet < WEBrick::HTTPServlet::AbstractServlet
|
203
|
+
def initialize(server, assets_dir, record_option, opt = {})
|
204
|
+
@assets_dir = assets_dir
|
205
|
+
@record_option = record_option
|
206
|
+
@opt = opt
|
207
|
+
super
|
208
|
+
end
|
209
|
+
|
210
|
+
def escape_device_name(dev)
|
211
|
+
dev.gsub(' ', '_').gsub('-', '_')
|
212
|
+
end
|
213
|
+
|
214
|
+
def do_GET(req, res)
|
215
|
+
res.content_type = 'text/html'
|
216
|
+
res['cache-control'] = 'no-cache'
|
217
|
+
|
218
|
+
# Variables for erb template
|
219
|
+
devices = @record_option.devices
|
220
|
+
report_cpu = @record_option.report_cpu
|
221
|
+
hostname = @opt[:hostname]
|
222
|
+
|
223
|
+
erb = ERB.new(File.read(File.expand_path('dashboard.erb', @assets_dir)))
|
224
|
+
res.body = erb.result(Kernel.binding)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class FaucetServlet < WEBrick::HTTPServlet::AbstractServlet
|
229
|
+
def initialize(server, recorder)
|
230
|
+
super(server)
|
231
|
+
@recorder = recorder
|
232
|
+
end
|
233
|
+
|
234
|
+
def do_GET(req, res)
|
235
|
+
res.chunked = true
|
236
|
+
res.content_type = 'text/event-stream'
|
237
|
+
res['cache-control'] = 'no-cache'
|
238
|
+
r, w = IO.pipe
|
239
|
+
|
240
|
+
Thread.start do
|
241
|
+
begin
|
242
|
+
while record = @recorder.get_current_record
|
243
|
+
w << "data: " << record << "\r\n" << "\r\n"
|
244
|
+
end
|
245
|
+
rescue Errno::EPIPE
|
246
|
+
# puts("Connection closed for /faucet")
|
247
|
+
# connection closed
|
248
|
+
rescue EOFError
|
249
|
+
# puts("Recorder has been terminated")
|
250
|
+
# connection closed
|
251
|
+
rescue Exception => err
|
252
|
+
puts("ERROR: Exception in faucet pipe writer")
|
253
|
+
puts("#{err.class.to_s}: #{err.message}")
|
254
|
+
puts(err.backtrace)
|
255
|
+
ensure
|
256
|
+
# puts("[FaucetServlet][do_GET] close w,r pipe")
|
257
|
+
begin; w.close; rescue IOError; end
|
258
|
+
begin; r.close; rescue IOError; end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
res.body = r
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def setup_webrick(webrick_server, record_option)
|
267
|
+
# find assets dir
|
268
|
+
# Search build environment first, then installed dir
|
269
|
+
assets_dir = [File.expand_path('../../../../../data/assets', __FILE__),
|
270
|
+
File.expand_path('assets', PerfMonger::DATAROOTDIR)].find do |dir|
|
271
|
+
File.directory?(dir)
|
272
|
+
end
|
273
|
+
if assets_dir.nil?
|
274
|
+
puts("ERROR: Assets for PerfMonger monitor not found!")
|
275
|
+
exit(false)
|
276
|
+
end
|
277
|
+
|
278
|
+
webrick_server.mount_proc('/') do |req, res|
|
279
|
+
puts("Request Path: " + req.path)
|
280
|
+
if req.path == '/favicon.ico'
|
281
|
+
res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, '/assets/favicon.ico')
|
282
|
+
else
|
283
|
+
res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, '/dashboard')
|
284
|
+
end
|
285
|
+
end
|
286
|
+
webrick_server.mount('/dashboard', DashboardServlet, assets_dir, record_option,
|
287
|
+
:hostname => @hostname)
|
288
|
+
webrick_server.mount('/assets', WEBrick::HTTPServlet::FileHandler, assets_dir)
|
289
|
+
webrick_server.mount('/faucet', FaucetServlet, @recorder)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
end # module Command
|
294
|
+
end # module PerfMonger
|
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module PerfMonger
|
6
|
+
module Command
|
7
|
+
|
8
|
+
class StatCommand < BaseCommand
|
9
|
+
register_command 'stat', "Run a command and record system performance during execution"
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(argv)
|
16
|
+
@argv, @option = PerfMonger::Command::StatOption.parse(argv)
|
17
|
+
|
18
|
+
if @argv.size == 0
|
19
|
+
puts("ERROR: No command given.")
|
20
|
+
exit(false)
|
21
|
+
end
|
22
|
+
|
23
|
+
record_cmd = @option.make_command
|
24
|
+
|
25
|
+
begin
|
26
|
+
if RUBY_VERSION >= '1.9'
|
27
|
+
record_pid = Process.spawn(*record_cmd)
|
28
|
+
else
|
29
|
+
record_pid = Process.fork do
|
30
|
+
Process.exec(*record_cmd)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Signal.trap(:INT) do
|
35
|
+
Process.kill("INT", record_pid)
|
36
|
+
end
|
37
|
+
|
38
|
+
@start_time = Time.now
|
39
|
+
if RUBY_VERSION >= '1.9'
|
40
|
+
command_pid = Process.spawn(*@argv)
|
41
|
+
else
|
42
|
+
command_pid = Process.fork do
|
43
|
+
system(*@argv)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
Process.wait(command_pid)
|
47
|
+
ensure
|
48
|
+
@end_time = Time.now
|
49
|
+
Process.kill(:INT, record_pid)
|
50
|
+
Process.wait(record_pid)
|
51
|
+
end
|
52
|
+
|
53
|
+
puts("")
|
54
|
+
printf("Execution time: %.4f\n", @end_time - @start_time)
|
55
|
+
summary_command = SummaryCommand.new.run([@option.logfile], @argv.join(" "))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end # module Command
|
60
|
+
end # module PerfMonger
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
module PerfMonger
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class StatOption < RecordOption
|
6
|
+
attr_reader :json
|
7
|
+
|
8
|
+
private
|
9
|
+
def initialize
|
10
|
+
super()
|
11
|
+
@parser.banner = <<EOS
|
12
|
+
Usage: perfmonger stat [options] -- <command>
|
13
|
+
|
14
|
+
Run a command and gather performance information during its execution.
|
15
|
+
|
16
|
+
Options:
|
17
|
+
EOS
|
18
|
+
|
19
|
+
@logfile = './perfmonger.log'
|
20
|
+
@json = false
|
21
|
+
|
22
|
+
@parser.on('--json', "Output summary in JSON") do
|
23
|
+
@json = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end # module Command
|
29
|
+
end # module PerfMonger
|