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.
@@ -0,0 +1,2 @@
1
+
2
+ require(File.expand_path('../stat/kmeans-1d', __FILE__))
@@ -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
@@ -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)
@@ -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
@@ -1,11 +1,11 @@
1
1
 
2
2
  require 'fileutils'
3
+ require 'json'
3
4
  require 'jkr/utils'
4
5
 
5
- class Jkr
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 do |var_comb|
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, plan.params.merge(var_comb))
29
+ Trial.new(result_dir, plan, params)
22
30
  end
23
31
  end
24
32
 
25
- def self.run(env, plan)
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
- trials = self.make_trials(resultset_dir, plan)
29
-
30
- FileUtils.copy_file(plan.file_path,
31
- File.join(resultset_dir, File.basename(plan.file_path)))
32
- params = plan.params.merge(plan.vars)
33
- plan.freeze
34
- plan.prep.call(plan)
35
- trials.each do |trial|
36
- trial.run
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
- @plan.routine.call(@plan, @params)
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
@@ -4,7 +4,7 @@ require 'time'
4
4
  require 'date'
5
5
  require 'csv'
6
6
 
7
- class Jkr
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
- 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
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 =~ /^Linux/ && blockstr =~ /(\d{2})\/(\d{2})\/(\d{2})$/
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
- result[:labels] = header
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
- vals.map{|val|
130
- begin
131
- Float(val)
132
- rescue ArgumentError
133
- val
134
- end
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