perfmonger 0.6.1
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.
- 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
|