jkr 0.0.1 → 0.1.0
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 +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
|