Top Level Namespace

Defined Under Namespace

Classes: DstatPlot

Constant Summary collapse

Y_DEFAULT =
105.0

Instance Method Summary collapse

Instance Method Details

#analyze_header_create_plot_title(prefix, smooth, inversion, csv_header) ⇒ Object



105
106
107
108
109
110
111
112
113
# File 'lib/dstat_plot.rb', line 105

def analyze_header_create_plot_title(prefix, smooth, inversion, csv_header)
  plot_title = "#{prefix} over time"
  if smooth then plot_title += " (smoothing: #{smooth})" end
  if csv_header[2].index("Host:")
    plot_title += '\n' + "(Host: #{csv_header[2][1]} User: #{csv_header[2][6]} Date: #{csv_header[3].last})"
  end
  if inversion then plot_title += '\n(inverted)' end
  plot_title
end

#average(data, slice_size) ⇒ Object

Calculate the average of groups of values from data Params:

+data

Array containing the data

+slice_size

number of values each group of data should contain



61
62
63
64
65
66
67
# File 'lib/dstat_plot.rb', line 61

def average(data, slice_size)
  reduced_data = []
  data.each_slice(slice_size) do |slice|
      reduced_data.push(slice.reduce(:+) / slice.size)
  end
  reduced_data
end

#create_gnuplot_dataset(timecode, values, no_plot_key, smooth, file) ⇒ Object

Create the GnuplotDataSet that is going to be printed. Params:

+timecode

Array containing the timestamps

+values

Array containing the actual values

+no_plot_key

boolean to de-/activate plotkey

+smooth

nil or smoothing algorithm

+file

file



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/dstat_plot.rb', line 93

def create_gnuplot_dataset(timecode, values, no_plot_key, smooth, file)
  Gnuplot::DataSet.new([timecode, values]) do |gp_dataset|
    gp_dataset.with = "lines"
    if no_plot_key then
      gp_dataset.notitle
    else
      gp_dataset.title = (File.basename file).gsub('_', '\\_')
    end
    gp_dataset.smooth = smooth unless smooth.nil?
  end
end

#data_preprocessing(dataset_container, slice_size) ⇒ Object

Preprocesses the data contained in all datasets in the dataset_container Groups of values are averaged with respect to timecode and actual data Params:

+dataset_container

Hash that holds the datasets and further information

+slice_size

size of the group that averages are supposed to be calculated from



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/dstat_plot.rb', line 74

def data_preprocessing(dataset_container, slice_size)
  dataset_container[:datasets].each do |dataset|
    timecode = dataset.data[0]
    reduced_timecode = average(timecode, slice_size)

    values = dataset.data[1].map { |value| value.to_f }
    reduced_values = average(values, slice_size)

    dataset.data = [reduced_timecode, reduced_values]
  end
end

#generate_filename(output, column, category, field, target_dir) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/dstat_plot.rb', line 37

def generate_filename(output, column, category, field, target_dir)
  if output.nil? || File.directory?(output) # if an output file is not explicitly stated or if it's a directory
    # generate filename
    if column
      generated_filename = "dstat-column#{column}.png"
    else
      generated_filename = "#{category}-#{field}.png".sub("/", "_")
    end
    
    # add directory portion
    if output.nil?
      filename = File.join(target_dir, generated_filename)
    elsif File.directory?(output)
      filename = File.join(output, generated_filename)
    end
  else # specific path+file is given so just use that
    filename = output
  end
end

#plot(dataset_container, category, field, dry, filename) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/dstat_plot.rb', line 15

def plot(dataset_container, category, field, dry, filename)
  Gnuplot.open do |gp|
    Gnuplot::Plot.new(gp) do |plot|
      plot.title dataset_container[:plot_title].gsub('_', '\\\\\\\\_')
      plot.xlabel "Time in seconds"
      plot.ylabel "#{category}: #{field}"
      plot.yrange "[0:#{dataset_container[:y_max] * 1.05}]"
      if dataset_container[:autoscale] then plot.set "autoscale" end
      plot.key "out vert right top"

      unless dry
        format = filename.split('.')[-1]
        plot.terminal format + ' size 1600,800 enhanced font "Helvetica,11"'
        plot.output filename
        puts "Saving plot to '#{filename}'"
      end

      plot.data = dataset_container[:datasets]
    end
  end
end

#read_data_from_csv(files, category, field, column, no_plot_key, y_max, inversion, title, smooth) ⇒ Object

returns the values from a headerless csv file



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/dstat_plot.rb', line 135

def read_data_from_csv(files, category, field, column, no_plot_key, y_max, inversion, title, smooth)
  plot_title = nil
  datasets = []
  autoscale = false
  overall_max = y_max.nil? ? Y_DEFAULT : y_max

  files.each do |file|
    csv = CSV.read(file)

    if column
      if $verbose then puts "Reading from csv to get column #{column}." end
      prefix = "dstat-column #{column}"
    else
      if $verbose then puts "Reading from csv to get #{category}-#{field}." end
      column = translate_to_column(category, field, csv)
      prefix = "#{category}-#{field}"
    end
    
    if plot_title.nil? # this only needs to be done for the first file
      if title
        plot_title = title
      else
        plot_title = analyze_header_create_plot_title(prefix, smooth, inversion != 0.0, csv[0..6])
      end
    end

    if csv[2].index "Host:"
      csv = csv.drop(7)
    end

    begin
      csv = csv.transpose
    rescue IndexError => e
      puts 'ERROR: It appears that your csv file is malformed. Check for incomplete lines, empty lines etc.'
      puts e.backtrace[0] + e.message
      exit
    end

    timecode = csv[0].map { |timestamp| timestamp.to_f - csv[0].first.to_f }

    values = csv[column]
    if inversion != 0.0
      values.map! { |value| (value.to_f - inversion).abs }
      overall_max = inversion
    end
    
    if y_max.nil?
      local_maximum = values.max { |a, b| a.to_f <=> b.to_f }.to_f
      if local_maximum > overall_max then overall_max = local_maximum end
    end

    dataset = create_gnuplot_dataset(timecode, values, no_plot_key, smooth, file)
    datasets.push dataset
  end

  if $verbose then puts "datasets: #{datasets.count} \nplot_title: #{plot_title} \ny_max: #{y_max} \nautoscale: #{autoscale}" end
  dataset_container = { :datasets => datasets, :plot_title => plot_title, :y_max => overall_max, :autoscale => autoscale }
end

#read_options_and_argumentsObject



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/dstat_plot.rb', line 194

def read_options_and_arguments
  opts = {} # Hash that holds all the options

  optparse = OptionParser.new do |parser|
    # banner that is displayed at the top
    parser.banner = "Usage: \b
    dstat_plot.rb [options] -c CATEGORY -f FIELD [directory | file1 file2 ...] or \b 
    dstat_plot.rb [options] -l COLUMN [directory | file1 file2 ...]\n\n"

    ### options and what they do
    parser.on('-v', '--verbose', 'Output more information') do
      $verbose = true
    end

    opts[:inversion] = 0.0
    parser.on('-i', '--invert [VALUE]', Float, 'Invert the graph such that inverted(x) = VALUE - f(x),', 'default is 100.') do |value|
      opts[:inversion] = value.nil? ? 100.0 : value
    end

    opts[:no_plot_key] = false
    parser.on('-n', '--no-key', 'No plot key is printed.') do
      opts[:no_plot_key] = true
    end

    opts[:dry] = false
    parser.on('-d', '--dry', 'Dry run. Plot is not saved to file but instead displayed with gnuplot.') do
      opts[:dry] = true
    end

    opts[:output] = nil
    parser.on('-o','--output FILE|DIR', 'File or Directory that plot should be saved to. ' \
      'If a directory is given', 'the filename will be generated. Default is csv file directory.') do |path|
      opts[:output] = path
    end

    opts[:y_max]
    parser.on('-y', '--y-range RANGE', Float, 'Sets the y-axis range. Default is 105. ' \
      'If a value exceeds this range,', '"autoscale" is enabled.') do |range|
      opts[:y_max] = range      
    end

    opts[:title] = nil
    parser.on('-t', '--title TITLE', 'Override the default title of the plot.') do |title|
      opts[:title] = title
    end

    opts[:smooth] = nil
    parser.on('-s', '--smoothing ALGORITHM', 'Smoothes the graph using the given algorithm.') do |algorithm|
        algorithms = [ 'unique', 'frequency', 'cumulative', 'cnormal', 'kdensity', 'unwrap',
          'csplines', 'acsplines', 'mcsplines', 'bezier', 'sbezier' ]
        if algorithms.index(algorithm)
          opts[:smooth] = algorithm
        else
          puts "#{algorithm} is not a valid option as an algorithm."
          exit
        end
    end

    opts[:slice_size] = nil
    parser.on('-a', '--average-over SLICE_SIZE', Integer, 'Calculates the everage for slice_size large groups of values.',"\n") do |slice_size|
      opts[:slice_size] = slice_size
    end

    opts[:category] = nil
    parser.on('-c', '--category CATEGORY', 'Select the category.') do |category|
      opts[:category] = category
    end

    opts[:field] = nil
    parser.on('-f', '--field FIELD' , 'Select the field.') do |field|
      opts[:field] = field
    end

    opts[:column] = nil
    parser.on('-l', '--column COLUMN', 'Select the desired column directly.', "\n") do |column|
      unless opts[:category] && opts[:field]  # -c and -f override -l
        opts[:column] = column.to_i
      end
    end

    # This displays the help screen
    parser.on_tail('-h', '--help', 'Display this screen.' ) do
      puts parser
      exit
    end
  end

  # there are two forms of the parse method. 'parse'
  # simply parses ARGV, while 'parse!' parses ARGV
  # and removes all options and parameters found. What's
  # left is the list of files
  optparse.parse!
  if $verbose then puts "opts: #{opts.inspect}" end

  if opts[:category].nil? || opts[:category].nil?
    if opts[:column].nil?
      puts "[Error] (-c CATEGORY and -f FIELD) or (-l COLUMN) are mandatory parameters.\n\n"
      puts optparse
      exit
    end
  end

  # if ARGV is empty at this point no directory or file(s) is specified
  # and the current working directory is used
  if ARGV.empty? then ARGV.push "." end

  files = []
  if File.directory?(ARGV.last) then
    opts[:target_dir] = ARGV.last.chomp("/") # cuts of "/" from the end if present
    files = Dir.glob "#{opts[:target_dir]}/*.csv"
    files = files.sort
  else
    opts[:target_dir] = File.dirname ARGV.first
    ARGV.each do |filename|
      files.push filename
    end
  end
  puts "Plotting data from #{files.count} file(s)."
  opts[:files] = files
  if $verbose then puts "files: #{files.count} #{files.inspect}" end

# opts = { :inversion, :no_plot_key, :dry, :output, :y_max, :title, :category, :field, :column, :target_dir, :files }
  opts
end

#translate_to_column(category, field, csv) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/dstat_plot.rb', line 115

def translate_to_column(category, field, csv)
  category_index = csv[5].index category
  if category_index.nil?
    puts "'#{category}' is not a valid parameter for 'category'."
    puts "Allowed categories: #{csv[5].reject{ |elem| elem == nil }.inspect}"
    exit 0
  end

  field_index = csv[6].drop(category_index).index field
  if field_index.nil?
    puts "'#{field}' is not a valid parameter for 'field'."
    puts "Allowed fields: #{csv[6].reject{ |elem| elem == nil }.inspect}"
    exit 0
  end
  
  if $verbose then puts "'#{category}-#{field}' was translated to #{category_index + field_index}." end
  column = category_index + field_index
end