jkr 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +674 -0
- data/{README.txt → README.rdoc} +0 -0
- data/Rakefile +4 -5
- data/bin/console +7 -0
- data/etc/example.plan +32 -0
- data/etc/zsh-comp.sh +82 -0
- data/exe/jkr +6 -0
- data/jkr.gemspec +31 -0
- data/lib/jkr.rb +13 -4
- data/lib/jkr/analysis.rb +5 -14
- data/lib/jkr/analytics.rb +75 -0
- data/lib/jkr/array.rb +47 -0
- data/lib/jkr/blktrace.rb +131 -0
- data/lib/jkr/cli.rb +110 -0
- data/lib/jkr/cpu_usage.rb +81 -0
- data/lib/jkr/cpufreq.rb +201 -0
- data/lib/jkr/dirlock.rb +9 -0
- data/lib/jkr/env.rb +17 -17
- data/lib/jkr/error.rb +5 -0
- data/lib/jkr/numeric.rb +28 -0
- data/lib/jkr/plan.rb +317 -26
- data/lib/jkr/planfinder.rb +40 -0
- data/lib/jkr/plot.rb +626 -0
- data/lib/jkr/stat.rb +2 -0
- data/lib/jkr/stat/kmeans-1d.rb +94 -0
- data/lib/jkr/su_cmd +163 -0
- data/lib/jkr/sysinfo.rb +34 -0
- data/lib/jkr/trial.rb +91 -16
- data/lib/jkr/userutils.rb +300 -22
- data/lib/jkr/utils.rb +38 -314
- data/lib/jkr/version.rb +3 -0
- data/sample-jkr.plan +52 -0
- metadata +171 -63
- data/bin/jkr +0 -224
- data/test/test_jkr.rb +0 -8
data/lib/jkr/stat.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
|
2
|
+
class Cluster1D
|
3
|
+
attr_accessor :center, :points
|
4
|
+
|
5
|
+
# Constructor with a starting centerpoint
|
6
|
+
def initialize(center)
|
7
|
+
@center = center
|
8
|
+
@points = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Recenters the centroid point and removes all of the associated points
|
12
|
+
def recenter!
|
13
|
+
avg = 0
|
14
|
+
old_center = @center
|
15
|
+
|
16
|
+
# Sum up all x/y coords
|
17
|
+
@points.each do |point|
|
18
|
+
avg += point
|
19
|
+
end
|
20
|
+
|
21
|
+
# Average out data
|
22
|
+
if points.length == 0
|
23
|
+
avg = center
|
24
|
+
else
|
25
|
+
avg /= points.length
|
26
|
+
end
|
27
|
+
|
28
|
+
# Reset center and return distance moved
|
29
|
+
@center = avg
|
30
|
+
return (old_center - center).abs
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# kmeans algorithm
|
36
|
+
#
|
37
|
+
|
38
|
+
def kmeans1d(data, k, delta=0.001)
|
39
|
+
clusters = []
|
40
|
+
|
41
|
+
# Assign intial values for all clusters
|
42
|
+
(1..k).each do |point|
|
43
|
+
index = (data.length * rand).to_i
|
44
|
+
|
45
|
+
rand_point = data[index]
|
46
|
+
c = Cluster1D.new(rand_point)
|
47
|
+
|
48
|
+
clusters.push c
|
49
|
+
end
|
50
|
+
|
51
|
+
# Loop
|
52
|
+
while true
|
53
|
+
# Assign points to clusters
|
54
|
+
data.each do |point|
|
55
|
+
min_dist = +Float::INFINITY
|
56
|
+
min_cluster = nil
|
57
|
+
|
58
|
+
# Find the closest cluster
|
59
|
+
clusters.each do |cluster|
|
60
|
+
dist = (point - cluster.center).abs
|
61
|
+
|
62
|
+
if dist < min_dist
|
63
|
+
min_dist = dist
|
64
|
+
min_cluster = cluster
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add to closest cluster
|
69
|
+
min_cluster.points.push point
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check deltas
|
73
|
+
max_delta = -Float::INFINITY
|
74
|
+
|
75
|
+
clusters.each do |cluster|
|
76
|
+
dist_moved = cluster.recenter!
|
77
|
+
|
78
|
+
# Get largest delta
|
79
|
+
if dist_moved > max_delta
|
80
|
+
max_delta = dist_moved
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check exit condition
|
85
|
+
if max_delta < delta
|
86
|
+
return clusters
|
87
|
+
end
|
88
|
+
|
89
|
+
# Reset points for the next iteration
|
90
|
+
clusters.each do |cluster|
|
91
|
+
cluster.points = []
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/jkr/su_cmd
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
import errno,os,pty,re,select,signal,string,sys
|
4
|
+
|
5
|
+
dbg=0
|
6
|
+
efd = None
|
7
|
+
|
8
|
+
def LOG(s):
|
9
|
+
os.write(efd, s)
|
10
|
+
|
11
|
+
#
|
12
|
+
# (1) ./su_cmd whoami ---> exec "su -c whoami"
|
13
|
+
# (2) wait for "Password: " prompt and send password
|
14
|
+
# (3) get stdout of the child process
|
15
|
+
#
|
16
|
+
# It may kill the child process when it waited until timeout
|
17
|
+
# got a strange string from the child process.
|
18
|
+
#
|
19
|
+
# Exit status is normally that of the child process (ssh), or
|
20
|
+
# 255 when the child was killed.
|
21
|
+
#
|
22
|
+
|
23
|
+
#
|
24
|
+
# Timeout for each 'read' operation.
|
25
|
+
# It should be long enough so distant hosts can respond.
|
26
|
+
#
|
27
|
+
read_timeout = 5.0
|
28
|
+
read_timeout_after_pw = 30.0
|
29
|
+
|
30
|
+
# Probably you need not modify parameters below.
|
31
|
+
write_timeout = 1.0
|
32
|
+
|
33
|
+
def safe_read(fd, bytes, timeout, show_error):
|
34
|
+
"""
|
35
|
+
Read string from child or timeout.
|
36
|
+
"""
|
37
|
+
if timeout is None:
|
38
|
+
R,W,E = select.select([fd],[],[])
|
39
|
+
else:
|
40
|
+
R,W,E = select.select([fd],[],[],timeout)
|
41
|
+
if len(R) == 0:
|
42
|
+
if dbg>=2: LOG("read: read timeout\n")
|
43
|
+
return (-1, None)
|
44
|
+
try:
|
45
|
+
x = os.read(fd, bytes)
|
46
|
+
except OSError,e:
|
47
|
+
if show_error:
|
48
|
+
if dbg>=2: LOG("read: %s\n" % (e.args, ))
|
49
|
+
return (-1, e)
|
50
|
+
if dbg>=2: LOG("read => '%s'\n" % x)
|
51
|
+
return (0, x)
|
52
|
+
|
53
|
+
def safe_write(fd, str, timeout, show_error):
|
54
|
+
"""
|
55
|
+
Write a string to child or timeout.
|
56
|
+
"""
|
57
|
+
R,W,E = select.select([],[fd],[],timeout)
|
58
|
+
if len(W) == 0:
|
59
|
+
if dbg>=2: LOG("write: write timeout\n")
|
60
|
+
return (-1, None)
|
61
|
+
try:
|
62
|
+
x = os.write(fd, str)
|
63
|
+
except OSError,e:
|
64
|
+
if show_error:
|
65
|
+
if dbg>=2: LOG("write: %s\n" % (e.args,))
|
66
|
+
return (-1, e)
|
67
|
+
if dbg>=2: LOG("write => %s\n" % x)
|
68
|
+
return (x, None)
|
69
|
+
|
70
|
+
def cleanup(pid):
|
71
|
+
"""
|
72
|
+
Check if pid exists. If so, kill it with SIGKILL.
|
73
|
+
"""
|
74
|
+
try:
|
75
|
+
os.kill(pid, 0)
|
76
|
+
except OSError,e:
|
77
|
+
if e.args[0] == errno.ESRCH: # no such process
|
78
|
+
return
|
79
|
+
try:
|
80
|
+
os.kill(pid, signal.SIGKILL)
|
81
|
+
except OSError,e:
|
82
|
+
if e.args[0] == errno.ESRCH: # no such process
|
83
|
+
return
|
84
|
+
|
85
|
+
def wait_for_password_prompt(fd, passphrase):
|
86
|
+
bytes_to_read = 1
|
87
|
+
output_s = ""
|
88
|
+
while 1:
|
89
|
+
r,s = safe_read(fd, bytes_to_read, read_timeout, 1)
|
90
|
+
if r == -1: return -1
|
91
|
+
output_s = output_s + s
|
92
|
+
if dbg>=2: LOG("[%s]\n" % output_s)
|
93
|
+
m = re.search("Password: ", output_s)
|
94
|
+
if m: break
|
95
|
+
|
96
|
+
r,s = safe_write(fd, "%s\r\n" % passphrase, write_timeout, 1)
|
97
|
+
if r == -1: return -1
|
98
|
+
while 1:
|
99
|
+
r,s = safe_read(fd, bytes_to_read, read_timeout, 1)
|
100
|
+
if r == -1: return -1
|
101
|
+
output_s = output_s + s
|
102
|
+
if dbg>=2: LOG("[%s]\n" % output_s)
|
103
|
+
m = re.search("Password: (.*)\r\n", output_s)
|
104
|
+
if m: break
|
105
|
+
return 0
|
106
|
+
|
107
|
+
def parent(fd, passphrase):
|
108
|
+
"""
|
109
|
+
Main procedure of the parent.
|
110
|
+
Input passphrase if asked.
|
111
|
+
"""
|
112
|
+
if wait_for_password_prompt(fd, passphrase) == -1:
|
113
|
+
return -1
|
114
|
+
bytes_to_read = 1
|
115
|
+
while 1:
|
116
|
+
r,s = safe_read(fd, bytes_to_read, read_timeout_after_pw, 1)
|
117
|
+
if r == -1: return -1
|
118
|
+
os.write(1, s)
|
119
|
+
return 0
|
120
|
+
|
121
|
+
def main(argv):
|
122
|
+
global efd
|
123
|
+
passphrase = os.environ.get("SU_PASSWORD")
|
124
|
+
if passphrase is None:
|
125
|
+
sys.stderr.write("su_cmd: SU_PASSWORD not set\n")
|
126
|
+
os._exit(255)
|
127
|
+
user = os.environ.get("SU_USER")
|
128
|
+
os.environ["LANG"] = "C"
|
129
|
+
# fork with pty
|
130
|
+
pid,master = pty.fork()
|
131
|
+
assert pid != -1
|
132
|
+
if pid == 0:
|
133
|
+
# child. run su
|
134
|
+
if user is None:
|
135
|
+
cmd = [ "su", "-c", string.join(argv[1:], " ") ]
|
136
|
+
else:
|
137
|
+
cmd = [ "su", user, "-c", string.join(argv[1:], " ") ]
|
138
|
+
os.execvp(cmd[0], cmd)
|
139
|
+
else:
|
140
|
+
if dbg>=2:
|
141
|
+
efd = 2 # os.open("su_cmd.log", os.O_WRONLY|os.O_CREAT|os.O_TRUNC)
|
142
|
+
# parent. talk to child.
|
143
|
+
r = parent(master, passphrase)
|
144
|
+
# ensure child is gone
|
145
|
+
if dbg>=2: LOG("cleanup\n")
|
146
|
+
cleanup(pid)
|
147
|
+
# write whatever we get from child
|
148
|
+
# os.write(1, s)
|
149
|
+
# wait for child to disappear
|
150
|
+
if dbg>=2: LOG("wait for child to terminate\n")
|
151
|
+
qid,status = os.wait()
|
152
|
+
assert pid == qid
|
153
|
+
if dbg>=2: LOG("child finished\n")
|
154
|
+
if os.WIFEXITED(status):
|
155
|
+
# child normally exited. forward its status
|
156
|
+
os._exit(os.WEXITSTATUS(status))
|
157
|
+
else:
|
158
|
+
# child was killed. return 255
|
159
|
+
os._exit(255)
|
160
|
+
if dbg>=2: os.close(efd)
|
161
|
+
|
162
|
+
if __name__ == "__main__":
|
163
|
+
main(sys.argv)
|
data/lib/jkr/sysinfo.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
module Jkr
|
3
|
+
class SysInfo
|
4
|
+
class << self
|
5
|
+
def gather
|
6
|
+
ret = Hash.new
|
7
|
+
ret[:proc] = Hash.new
|
8
|
+
|
9
|
+
# gather infomation under /proc
|
10
|
+
ret[:proc][:cpuinfo] = `cat /proc/cpuinfo`
|
11
|
+
ret[:proc][:meminfo] = `cat /proc/meminfo`
|
12
|
+
ret[:proc][:interrupts] = `cat /proc/interrupts`
|
13
|
+
ret[:proc][:mdstat] = `cat /proc/mdstat`
|
14
|
+
ret[:proc][:mounts] = `cat /proc/mounts`
|
15
|
+
|
16
|
+
ret[:sys] = Hash.new
|
17
|
+
ret[:sys][:block] = Hash.new
|
18
|
+
Dir.glob("/sys/block/*").each do |block_path|
|
19
|
+
block = File.basename(block_path).to_sym
|
20
|
+
ret[:sys][:block][block] = Hash.new
|
21
|
+
ret[:sys][:block][block][:queue] = Hash.new
|
22
|
+
Dir.glob("#{block_path}/queue/scheduler") do |path|
|
23
|
+
ret[:sys][:block][block][:queue][:scheduler] = `cat #{path}`
|
24
|
+
end
|
25
|
+
Dir.glob("#{block_path}/queue/nr_requests") do |path|
|
26
|
+
ret[:sys][:block][block][:queue][:nr_requests] = `cat #{path}`
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ret
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/jkr/trial.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
|
2
2
|
require 'fileutils'
|
3
|
+
require 'json'
|
3
4
|
require 'jkr/utils'
|
4
5
|
|
5
|
-
|
6
|
+
module Jkr
|
6
7
|
class Trial
|
7
8
|
attr_reader :params
|
8
|
-
|
9
9
|
def self.make_trials(resultset_dir, plan)
|
10
10
|
var_combs = [{}]
|
11
11
|
plan.vars.each do |key, vals|
|
@@ -16,26 +16,81 @@ class Jkr
|
|
16
16
|
}.flatten
|
17
17
|
end
|
18
18
|
|
19
|
-
var_combs.map
|
19
|
+
params_list = var_combs.map{|var_comb| plan.params.merge(var_comb)}
|
20
|
+
|
21
|
+
plan.param_filters.each do |filter|
|
22
|
+
params_list = params_list.select(&filter)
|
23
|
+
end
|
24
|
+
|
25
|
+
params_list = params_list * plan.routine_nr_run
|
26
|
+
|
27
|
+
params_list.map do |params|
|
20
28
|
result_dir = Utils.reserve_next_dir(resultset_dir)
|
21
|
-
Trial.new(result_dir, plan,
|
29
|
+
Trial.new(result_dir, plan, params)
|
22
30
|
end
|
23
31
|
end
|
24
32
|
|
25
|
-
def self.
|
33
|
+
def self.pretty_time(seconds)
|
34
|
+
hour = seconds / 3600
|
35
|
+
min = (seconds - hour * 3600) / 60
|
36
|
+
sec = seconds - hour * 3600 - min * 60
|
37
|
+
|
38
|
+
sprintf("%02d:%02d:%02d", hour, min, sec)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.run(env, plan, delete_files_on_error = true)
|
26
42
|
plan_suffix = File.basename(plan.file_path, ".plan")
|
43
|
+
plan_suffix += "_#{plan.short_desc}" if plan.short_desc
|
27
44
|
resultset_dir = Utils.reserve_next_dir(env.jkr_result_dir, plan_suffix)
|
28
|
-
|
29
|
-
|
30
|
-
FileUtils.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
45
|
+
plan.resultset_dir = resultset_dir
|
46
|
+
FileUtils.mkdir_p(File.join(resultset_dir, "plan"))
|
47
|
+
FileUtils.mkdir_p(File.join(resultset_dir, "script"))
|
48
|
+
|
49
|
+
begin
|
50
|
+
trials = self.make_trials(resultset_dir, plan)
|
51
|
+
|
52
|
+
plan_dest_dir = File.join(resultset_dir, "plan")
|
53
|
+
script_dest_dir = File.join(resultset_dir, "script")
|
54
|
+
|
55
|
+
_plan = plan
|
56
|
+
begin
|
57
|
+
if _plan == plan # copy main plan file
|
58
|
+
FileUtils.copy_file(_plan.file_path,
|
59
|
+
File.expand_path(File.basename(_plan.file_path), resultset_dir))
|
60
|
+
else
|
61
|
+
FileUtils.copy_file(_plan.file_path,
|
62
|
+
File.expand_path(File.basename(_plan.file_path),
|
63
|
+
plan_dest_dir))
|
64
|
+
end
|
65
|
+
|
66
|
+
plan.used_scripts.each do |script_path|
|
67
|
+
FileUtils.copy_file(script_path,
|
68
|
+
File.expand_path(File.basename(script_path),
|
69
|
+
script_dest_dir))
|
70
|
+
end
|
71
|
+
end while _plan = _plan.base_plan
|
72
|
+
|
73
|
+
params = plan.params.merge(plan.vars)
|
74
|
+
plan.freeze
|
75
|
+
|
76
|
+
# show estimated execution time if available
|
77
|
+
if plan.exec_time_estimate
|
78
|
+
puts("")
|
79
|
+
puts("== estimated execution time: #{pretty_time(plan.exec_time_estimate.call(plan))} ==")
|
80
|
+
puts("")
|
81
|
+
end
|
82
|
+
|
83
|
+
plan.do_prep()
|
84
|
+
trials.each do |trial|
|
85
|
+
trial.run
|
86
|
+
end
|
87
|
+
plan.do_cleanup()
|
88
|
+
rescue Exception => err
|
89
|
+
if delete_files_on_error
|
90
|
+
FileUtils.rm_rf(resultset_dir)
|
91
|
+
end
|
92
|
+
raise err
|
37
93
|
end
|
38
|
-
plan.cleanup.call(plan)
|
39
94
|
end
|
40
95
|
|
41
96
|
def initialize(result_dir, plan, params)
|
@@ -46,9 +101,29 @@ class Jkr
|
|
46
101
|
private :initialize
|
47
102
|
|
48
103
|
def run()
|
104
|
+
plan = @plan
|
105
|
+
File.open("#{@result_dir}/params.json", "w") do |f|
|
106
|
+
f.puts(@params.to_json)
|
107
|
+
end
|
108
|
+
|
109
|
+
# reset plan.metastore
|
110
|
+
plan.metastore.clear
|
111
|
+
|
112
|
+
# define utility functions for plan.routine object
|
49
113
|
Jkr::TrialUtils.define_routine_utils(@result_dir, @plan, @params)
|
50
|
-
|
114
|
+
|
115
|
+
@plan.metastore[:trial_start_time] = Time.now
|
116
|
+
@plan.do_routine(@plan, @params)
|
117
|
+
@plan.metastore[:trial_end_time] = Time.now
|
118
|
+
|
51
119
|
Jkr::TrialUtils.undef_routine_utils(@plan)
|
120
|
+
|
121
|
+
# save plan.metastore
|
122
|
+
Marshal.dump(@plan.metastore,
|
123
|
+
File.open("#{@result_dir}/metastore.msh", "w"))
|
124
|
+
File.open("#{@result_dir}/metastore.json", "w") do |f|
|
125
|
+
f.puts(@plan.metastore.to_json)
|
126
|
+
end
|
52
127
|
end
|
53
128
|
end
|
54
129
|
end
|
data/lib/jkr/userutils.rb
CHANGED
@@ -4,7 +4,7 @@ require 'time'
|
|
4
4
|
require 'date'
|
5
5
|
require 'csv'
|
6
6
|
|
7
|
-
|
7
|
+
module Jkr
|
8
8
|
class SysUtils
|
9
9
|
def self.cpu_cores()
|
10
10
|
self.num_cores()
|
@@ -34,19 +34,22 @@ class Jkr
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
result = []
|
38
|
-
bufstr = ""
|
39
|
-
while ! file.eof?
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
ret = proc.call(bufstr)
|
49
|
-
result.push(ret) if ret
|
37
|
+
#result = []
|
38
|
+
#bufstr = ""
|
39
|
+
#while ! file.eof?
|
40
|
+
# bufstr += file.read(BLOCKSIZE)
|
41
|
+
# blocks = bufstr.split(separator)
|
42
|
+
# bufstr = blocks.pop
|
43
|
+
# blocks.each do |block|
|
44
|
+
# ret = proc.call(block)
|
45
|
+
# result.push(ret) if ret
|
46
|
+
# end
|
47
|
+
#end
|
48
|
+
#ret = proc.call(bufstr)
|
49
|
+
#result.push(ret) if ret
|
50
|
+
result = file.read.split(separator).map do |x|
|
51
|
+
proc.call(x)
|
52
|
+
end.compact
|
50
53
|
|
51
54
|
result
|
52
55
|
end
|
@@ -55,6 +58,67 @@ class Jkr
|
|
55
58
|
self.read_blockseq(io_or_filepath, "\n", &block)
|
56
59
|
end
|
57
60
|
|
61
|
+
def self.read_sar(sar_filepath)
|
62
|
+
labels = nil
|
63
|
+
date = nil
|
64
|
+
last_time = nil
|
65
|
+
idx = 0
|
66
|
+
self.read_rowseq(sar_filepath){|rowstr|
|
67
|
+
if rowstr =~ /^Linux/ && rowstr =~ /(\d{2})\/(\d{2})\/(\d{2})/
|
68
|
+
y = $~[3].to_i; m = $~[1].to_i; d = $~[2].to_i
|
69
|
+
date = Date.new(2000 + y, m, d)
|
70
|
+
next
|
71
|
+
else
|
72
|
+
row = Hash.new
|
73
|
+
|
74
|
+
time, *vals = rowstr.split
|
75
|
+
|
76
|
+
if vals.size == 0
|
77
|
+
next
|
78
|
+
end
|
79
|
+
if vals.every?{|val| val =~ /\A\d+(?:\.\d+)?\Z/ }
|
80
|
+
vals = vals.map(&:to_f)
|
81
|
+
else
|
82
|
+
# label line
|
83
|
+
labels = vals
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
unless date
|
88
|
+
raise StandardError.new("cannot find date information in sar log")
|
89
|
+
end
|
90
|
+
unless labels
|
91
|
+
raise StandardError.new("no label information")
|
92
|
+
end
|
93
|
+
|
94
|
+
unless time =~ /(\d{2}):(\d{2}):(\d{2})/
|
95
|
+
if time =~ /Average/
|
96
|
+
next
|
97
|
+
end
|
98
|
+
raise StandardError.new("Invalid time format: #{time}")
|
99
|
+
else
|
100
|
+
time = Time.local(date.year, date.month, date.day,
|
101
|
+
$~[1].to_i, $~[2].to_i, $~[3].to_i)
|
102
|
+
if last_time && time < last_time
|
103
|
+
date += 1
|
104
|
+
time = Time.local(date.year, date.month, date.day,
|
105
|
+
$~[1].to_i, $~[2].to_i, $~[3].to_i)
|
106
|
+
end
|
107
|
+
|
108
|
+
row[:time] = time
|
109
|
+
row[:data] = Hash.new
|
110
|
+
labels.each_with_index do |label,idx|
|
111
|
+
row[:data][label] = vals[idx]
|
112
|
+
end
|
113
|
+
row[:labels] = labels
|
114
|
+
|
115
|
+
last_time = time
|
116
|
+
end
|
117
|
+
end
|
118
|
+
row
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
58
122
|
def self.read_mpstat_avg(io_or_filepath)
|
59
123
|
self.read_blockseq(io_or_filepath){|blockstr|
|
60
124
|
if blockstr =~ /^Average:/
|
@@ -82,13 +146,23 @@ class Jkr
|
|
82
146
|
}.last
|
83
147
|
end
|
84
148
|
|
149
|
+
# Format of returned value
|
150
|
+
# [{
|
151
|
+
# :time => <Time>,
|
152
|
+
# :labels => [:cpu, :usr, :nice, :sys, :iowait, :irq, ...],
|
153
|
+
# :data => [{:cpu => "all", :usr => 0.11, :nice => 0.00, ...],
|
154
|
+
# {:cpu => 0, :usr => 0.12, :nice => 0.00, ...},
|
155
|
+
# ...]
|
156
|
+
# },
|
157
|
+
# ...]
|
158
|
+
#
|
85
159
|
def self.read_mpstat(io_or_filepath)
|
86
160
|
hostname = `hostname`.strip
|
87
161
|
|
88
162
|
date = nil
|
89
163
|
last_time = nil
|
90
164
|
self.read_blockseq(io_or_filepath) do |blockstr|
|
91
|
-
if blockstr =~
|
165
|
+
if blockstr =~ /Linux/ && blockstr =~ /(\d{2})\/(\d{2})\/(\d{2})/
|
92
166
|
# the first line
|
93
167
|
y = $~[3].to_i; m = $~[1].to_i; d = $~[2].to_i
|
94
168
|
date = Date.new(2000 + y, m, d)
|
@@ -104,11 +178,25 @@ class Jkr
|
|
104
178
|
rows = blockstr.lines.map(&:strip)
|
105
179
|
header = rows.shift.split
|
106
180
|
next if header.shift =~ /Average/
|
107
|
-
|
181
|
+
if header.first =~ /\AAM|PM\Z/
|
182
|
+
header.shift
|
183
|
+
end
|
184
|
+
result[:labels] = header.map do |label|
|
185
|
+
{
|
186
|
+
"CPU" => :cpu, "%usr" => :usr, "%user" => :usr,
|
187
|
+
"%nice" => :nice, "%sys" => :sys, "%iowait" => :iowait,
|
188
|
+
"%irq" => :irq, "%soft" => :soft, "%steal" => :steal,
|
189
|
+
"%guest" => :guest, "%idle" => :idle
|
190
|
+
}[label] || label
|
191
|
+
end
|
108
192
|
time = nil
|
109
193
|
result[:data] = rows.map { |row|
|
110
194
|
vals = row.split
|
111
195
|
wallclock = vals.shift
|
196
|
+
if vals.first =~ /\AAM|PM\Z/
|
197
|
+
vals.shift
|
198
|
+
end
|
199
|
+
|
112
200
|
unless time
|
113
201
|
unless wallclock =~ /(\d{2}):(\d{2}):(\d{2})/
|
114
202
|
raise RuntimeError.new("Cannot extract wallclock time from mpstat data")
|
@@ -126,19 +214,121 @@ class Jkr
|
|
126
214
|
if vals.size != result[:labels].size
|
127
215
|
raise RuntimeError.new("Invalid mpstat data")
|
128
216
|
end
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
217
|
+
|
218
|
+
record = Hash.new
|
219
|
+
vals.each_with_index{|val, idx|
|
220
|
+
label = result[:labels][idx]
|
221
|
+
val = if val =~ /\A\d+\Z/
|
222
|
+
val.to_i
|
223
|
+
else
|
224
|
+
begin
|
225
|
+
Float(val)
|
226
|
+
rescue ArgumentError
|
227
|
+
val
|
228
|
+
end
|
229
|
+
end
|
230
|
+
record[label] = val
|
135
231
|
}
|
232
|
+
|
233
|
+
record
|
136
234
|
}
|
137
235
|
result
|
138
236
|
end
|
139
237
|
end
|
140
238
|
end
|
141
239
|
|
240
|
+
#
|
241
|
+
# This function parses _io_or_filepath_ as an iostat log and
|
242
|
+
# returns the parsed result.
|
243
|
+
#
|
244
|
+
# _block_ :: If given, invoked for each iostat record like
|
245
|
+
# block.call(t, record)
|
246
|
+
# t ... wallclock time of the record
|
247
|
+
# record ... e.g. {"sda" => {"rrqm/s" => 0.0, ...}, ...}
|
248
|
+
#
|
249
|
+
def self.read_iostat(io_or_filepath, &block)
|
250
|
+
hostname = `hostname`.strip
|
251
|
+
date = nil
|
252
|
+
last_time = nil
|
253
|
+
sysname_regex = Regexp.new(Regexp.quote("#{`uname -s`.strip}"))
|
254
|
+
self.read_blockseq(io_or_filepath) do |blockstr|
|
255
|
+
if blockstr =~ sysname_regex
|
256
|
+
# the first line
|
257
|
+
if blockstr =~ /(\d{2})\/(\d{2})\/(\d{2,4})/
|
258
|
+
if $~[3].size == 2
|
259
|
+
y = $~[3].to_i + 2000
|
260
|
+
else
|
261
|
+
y = $~[3].to_i
|
262
|
+
end
|
263
|
+
m = $~[1].to_i
|
264
|
+
d = $~[2].to_i
|
265
|
+
date = Date.new(y, m, d)
|
266
|
+
next
|
267
|
+
end
|
268
|
+
else
|
269
|
+
rows = blockstr.lines.map(&:strip)
|
270
|
+
timestamp = rows.shift
|
271
|
+
time = nil
|
272
|
+
unless date
|
273
|
+
raise RuntimeError.new("Cannot detect date: #{io_or_filepath}")
|
274
|
+
end
|
275
|
+
|
276
|
+
if timestamp =~ /(\d{2})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})/
|
277
|
+
y = $~[3].to_i; m = $~[1].to_i; d = $~[2].to_i
|
278
|
+
time = Time.local(date.year, date.month, date.day, $~[4].to_i, $~[5].to_i, $~[6].to_i)
|
279
|
+
elsif date && timestamp =~ /Time: (\d{2}):(\d{2}):(\d{2}) (AM|PM)/
|
280
|
+
if $~[4] == "PM"
|
281
|
+
hour = $~[1].to_i
|
282
|
+
if $~[1].to_i != 12
|
283
|
+
hour += 12
|
284
|
+
end
|
285
|
+
elsif $~[4] == "AM" && $~[1].to_i == 12
|
286
|
+
hour = 0
|
287
|
+
else
|
288
|
+
hour = $~[1].to_i
|
289
|
+
end
|
290
|
+
time = Time.local(date.year, date.month, date.day,
|
291
|
+
hour, $~[2].to_i, $~[3].to_i)
|
292
|
+
elsif date && timestamp =~ /Time: (\d{2}):(\d{2}):(\d{2})/
|
293
|
+
time = Time.local(date.year, date.month, date.day,
|
294
|
+
$~[1].to_i, $~[2].to_i, $~[3].to_i)
|
295
|
+
end
|
296
|
+
|
297
|
+
unless time
|
298
|
+
unless date
|
299
|
+
raise StandardError.new("Cannot find date in your iostat log: #{io_or_filepath}")
|
300
|
+
end
|
301
|
+
raise StandardError.new("Cannot find timestamp in your iostat log: #{io_or_filepath}")
|
302
|
+
end
|
303
|
+
|
304
|
+
labels = rows.shift.split
|
305
|
+
unless labels.shift =~ /Device:/
|
306
|
+
raise StandardError.new("Invalid iostat log: #{io_or_filepath}")
|
307
|
+
end
|
308
|
+
|
309
|
+
record = Hash.new
|
310
|
+
rows.each do |row|
|
311
|
+
vals = row.split
|
312
|
+
dev = vals.shift
|
313
|
+
unless vals.size == labels.size
|
314
|
+
raise StandardError.new("Invalid iostat log: #{io_or_filepath}")
|
315
|
+
end
|
316
|
+
record_item = Hash.new
|
317
|
+
labels.each do |label|
|
318
|
+
record_item[label] = vals.shift.to_f
|
319
|
+
end
|
320
|
+
record[dev] = record_item
|
321
|
+
end
|
322
|
+
|
323
|
+
if block.is_a? Proc
|
324
|
+
block.call(time, record)
|
325
|
+
end
|
326
|
+
|
327
|
+
[time, record]
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
142
332
|
def self.read_csv(io_or_filepath, fs = ",", rs = nil, &proc)
|
143
333
|
if io_or_filepath.is_a?(String) && File.exists?(io_or_filepath)
|
144
334
|
io_or_filepath = File.open(io_or_filepath, "r")
|
@@ -152,5 +342,93 @@ class Jkr
|
|
152
342
|
end
|
153
343
|
result
|
154
344
|
end
|
345
|
+
|
346
|
+
class << self
|
347
|
+
def read_top(io_or_filepath, opt = {}, &proc)
|
348
|
+
opt[:start_time] ||= Time.now
|
349
|
+
|
350
|
+
def block_filter(filter, block)
|
351
|
+
if filter.is_a? Symbol
|
352
|
+
filters = {
|
353
|
+
:kernel_process => /\A(kworker|ksoftirqd|migration|watchdog|kintegrityd|kblockd|events|kondemand|aio|crypto|ata|kmpathd|kconservative|rpciod|xfslogd|xfsdatad|xfsconvertd)\//
|
354
|
+
}
|
355
|
+
raise ArgumentError.new("Invalid filter: #{filter.inspect}") unless filters[filter]
|
356
|
+
filter = filters[filter]
|
357
|
+
elsif filter.is_a? Regexp
|
358
|
+
filter = filter
|
359
|
+
else
|
360
|
+
raise ArgumentError.new("Invalid filter: #{filter.inspect}")
|
361
|
+
end
|
362
|
+
|
363
|
+
block = block.select do |record|
|
364
|
+
! (record[:command] =~ filter)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def parse_block(block, opt)
|
369
|
+
y = opt[:start_time].year
|
370
|
+
m = opt[:start_time].month
|
371
|
+
d = opt[:start_time].day
|
372
|
+
|
373
|
+
lines = block.lines.map(&:strip)
|
374
|
+
head_line = lines.shift
|
375
|
+
|
376
|
+
unless head_line =~ /(\d{2}):(\d{2}):(\d{2})/
|
377
|
+
raise ArgumentError.new("Invalid top(3) data")
|
378
|
+
end
|
379
|
+
time = Time.local(y, m, d, $~[1].to_i, $~[2].to_i, $~[3].to_i)
|
380
|
+
|
381
|
+
while ! (lines[0] =~ /\APID/)
|
382
|
+
line = lines.shift
|
383
|
+
end
|
384
|
+
labels = lines.shift.split.map do |key|
|
385
|
+
{"PID" => :pid, "USER" => :user, "PR" => :pr, "NI" => :ni,
|
386
|
+
"VIRT" => :virt, "RES" => :res, "SHR" => :shr, "S" => :s,
|
387
|
+
"%CPU" => :cpu, "%MEM" => :mem, "TIME+" => :time_plus,
|
388
|
+
"COMMAND" => :command}[key] || key
|
389
|
+
end
|
390
|
+
|
391
|
+
lines = lines.select{|line| ! line.empty?}
|
392
|
+
records = lines.map do |line|
|
393
|
+
record = Hash.new
|
394
|
+
record[:time] = time
|
395
|
+
line.split.each_with_index do |val, idx|
|
396
|
+
key = labels[idx]
|
397
|
+
if val =~ /\A(\d+)([mg]?)\Z/
|
398
|
+
record[key] = Integer($~[1])
|
399
|
+
if ! $~[2].empty?
|
400
|
+
record[key] *= {'g' => 2**20, 'm' => 2**10}[$~[2]]
|
401
|
+
end
|
402
|
+
elsif val =~ /\A(\d+\.\d+)([mg]?)\Z/
|
403
|
+
record[key] = Float($~[1])
|
404
|
+
if ! $~[2].empty?
|
405
|
+
record[key] *= {'g' => 2**20, 'm' => 2**10}[$~[2]]
|
406
|
+
end
|
407
|
+
elsif val =~ /\A(\d+):(\d+\.\d+)\Z/
|
408
|
+
record[key] = Integer($~[1])*60 + Float($~[2])
|
409
|
+
else
|
410
|
+
record[key] = val
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
record
|
415
|
+
end
|
416
|
+
|
417
|
+
if opt[:filter]
|
418
|
+
records = block_filter(opt[:filter], records)
|
419
|
+
end
|
420
|
+
|
421
|
+
if opt[:top_k]
|
422
|
+
records = records.first(opt[:top_k])
|
423
|
+
end
|
424
|
+
|
425
|
+
records
|
426
|
+
end
|
427
|
+
|
428
|
+
File.open(io_or_filepath, "r").read.split("\n\n\n").map do |block|
|
429
|
+
parse_block(block, opt)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
155
433
|
end
|
156
434
|
end
|