evoc 3.5.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/Makefile +4 -0
  8. data/README.md +61 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/evoc +3 -0
  12. data/bin/setup +7 -0
  13. data/evoc.gemspec +30 -0
  14. data/lib/evoc/algorithm.rb +147 -0
  15. data/lib/evoc/algorithms/top_k.rb +86 -0
  16. data/lib/evoc/analyze.rb +395 -0
  17. data/lib/evoc/array.rb +43 -0
  18. data/lib/evoc/evaluate.rb +109 -0
  19. data/lib/evoc/exceptions/aggregation_error.rb +6 -0
  20. data/lib/evoc/exceptions/expectedoutcome_nil_or_empty.rb +6 -0
  21. data/lib/evoc/exceptions/measure_calculation_error.rb +6 -0
  22. data/lib/evoc/exceptions/no_changed_items_in_changes.rb +6 -0
  23. data/lib/evoc/exceptions/no_changes_in_json_object.rb +6 -0
  24. data/lib/evoc/exceptions/no_date_in_json_object.rb +6 -0
  25. data/lib/evoc/exceptions/no_result.rb +6 -0
  26. data/lib/evoc/exceptions/non_finite.rb +8 -0
  27. data/lib/evoc/exceptions/non_numeric.rb +8 -0
  28. data/lib/evoc/exceptions/not_a_query.rb +6 -0
  29. data/lib/evoc/exceptions/not_a_result.rb +6 -0
  30. data/lib/evoc/exceptions/not_a_transaction.rb +6 -0
  31. data/lib/evoc/exceptions/not_initialized.rb +6 -0
  32. data/lib/evoc/exceptions/only_nil_in_changes.rb +6 -0
  33. data/lib/evoc/exceptions/query_nil_or_empty.rb +6 -0
  34. data/lib/evoc/exceptions/unable_to_convert_json_to_tx.rb +6 -0
  35. data/lib/evoc/experiment.rb +239 -0
  36. data/lib/evoc/hash.rb +56 -0
  37. data/lib/evoc/history_store.rb +53 -0
  38. data/lib/evoc/hyper_rule.rb +53 -0
  39. data/lib/evoc/interestingness_measure.rb +77 -0
  40. data/lib/evoc/interestingness_measure_aggregator.rb +147 -0
  41. data/lib/evoc/interestingness_measures.rb +882 -0
  42. data/lib/evoc/logger.rb +34 -0
  43. data/lib/evoc/memory_profiler.rb +43 -0
  44. data/lib/evoc/recommendation_cache.rb +152 -0
  45. data/lib/evoc/rule.rb +32 -0
  46. data/lib/evoc/rule_store.rb +340 -0
  47. data/lib/evoc/scenario.rb +303 -0
  48. data/lib/evoc/svd.rb +124 -0
  49. data/lib/evoc/tx.rb +34 -0
  50. data/lib/evoc/tx_store.rb +379 -0
  51. data/lib/evoc/version.rb +3 -0
  52. data/lib/evoc.rb +4 -0
  53. data/lib/evoc_cli/analyze.rb +198 -0
  54. data/lib/evoc_cli/cli_helper.rb +1 -0
  55. data/lib/evoc_cli/experiment.rb +78 -0
  56. data/lib/evoc_cli/info.rb +22 -0
  57. data/lib/evoc_cli/main.rb +29 -0
  58. data/lib/evoc_cli/util.rb +36 -0
  59. data/lib/evoc_helper.rb +40 -0
  60. data/mem_profiler/Gemfile.lock +39 -0
  61. data/mem_profiler/README.md +126 -0
  62. data/mem_profiler/createdb.rb +4 -0
  63. data/mem_profiler/db.rb +82 -0
  64. data/mem_profiler/gemfile +6 -0
  65. data/mem_profiler/gencsv.rb +64 -0
  66. data/mem_profiler/genimport.sh +8 -0
  67. data/mem_profiler/graph.rb +91 -0
  68. metadata +251 -0
@@ -0,0 +1,29 @@
1
+ require_relative 'cli_helper'
2
+
3
+ module EvocCLI
4
+ class Main < Thor
5
+ map %w[--version -v] => :__print_version
6
+
7
+ ##
8
+ # default thor behavior is to return exit 0 on errors (i.e., success..)
9
+ # by having exit_on_failure return true, exit(1) is returned instead
10
+ def self.exit_on_failure?
11
+ true
12
+ end
13
+
14
+ desc "--version, -v", "print the version"
15
+ def __print_version
16
+ puts Evoc::VERSION
17
+ end
18
+
19
+ desc "info SUBCOMMAND [options]", "Print info on implemented measures etc"
20
+ subcommand "info", Info
21
+
22
+ desc "analyze SUBCOMMAND [options]", "Various tools for analysis on version histories"
23
+ subcommand "analyze", Analyze
24
+
25
+ desc "experiment SUBCOMMAND", "Various tools for setting up and running experiments in the context of targeted association rule mining"
26
+ subcommand "experiment", Experiment
27
+
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'cli_helper'
2
+
3
+ module EvocCLI
4
+ class Util < Thor
5
+
6
+ method_option :exclude_columns, :aliases => '-x', :type => :array, default: [], :desc => "Keys/Columns to exclude from the csv"
7
+ desc "json2csv [OPTIONS] JSON","Convert a json file to csv"
8
+ def json2csv(path)
9
+ json = nil
10
+ if File.extname(path) == '.gz'
11
+ Zlib::GzipReader.open(path) {|gz|
12
+ json = gz.read
13
+ }
14
+ else
15
+ json = File.read(path,external_encoding: 'iso-8859-1',internal_encoding: 'utf-8')
16
+ end
17
+ header = []
18
+ header_not_written = true
19
+ JSON.parse(json).each do |json_object|
20
+ if header_not_written
21
+ header = json_object.keys
22
+ options[:exclude_columns].each {|c| header.delete(c)}
23
+ CSV {|row| row << header}
24
+ header_not_written = false
25
+ end
26
+ # remove unwanted keys
27
+ options[:exclude_columns].each {|c| json_object.delete(c)}
28
+ if json_object.keys == header
29
+ CSV {|row| row << json_object.values.map {|v| v.is_a?(Array) ? v.join(",") : v }}
30
+ else
31
+ raise ArgumentError, "CSV header (made from first json object) didn't match with the keys in the current json object, the keys were: #{json_object.keys}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # define some helper functions to reason about the runtime environment
2
+ module Evoc
3
+ module Env
4
+ def self.load(gem,failure_msg="")
5
+ begin
6
+ require gem
7
+ self.const_set(gem.upcase, true)
8
+ true
9
+ rescue LoadError
10
+ $stderr.puts "[#{gem} gem not installed] #{failure_msg}"
11
+ self.const_set(gem.upcase, false)
12
+ false
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ require 'pp'
19
+ require 'json'
20
+ require 'yaml'
21
+ require 'thor'
22
+ require 'time'
23
+ require 'digest'
24
+ require 'csv'
25
+ require 'time_difference'
26
+ require 'ruby-progressbar'
27
+ require 'logger'
28
+ require 'ruby-prof'
29
+ require 'zip'
30
+ require 'zip/filesystem'
31
+ require 'set'
32
+ require 'algorithms' # various efficient data structures
33
+ require 'mathn' # enhances the Rational (and others) number type
34
+ Evoc::Env.load('google_hash',"please install to improve performance")
35
+ #Evoc::Env.load('nmatrix')
36
+ #Evoc::Env.load('nmatrix/lapacke')
37
+
38
+ require 'require_all'
39
+ require_rel '/**/*.rb'
40
+
@@ -0,0 +1,39 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activemodel (4.2.7.1)
5
+ activesupport (= 4.2.7.1)
6
+ builder (~> 3.1)
7
+ activerecord (4.2.7.1)
8
+ activemodel (= 4.2.7.1)
9
+ activesupport (= 4.2.7.1)
10
+ arel (~> 6.0)
11
+ activesupport (4.2.7.1)
12
+ i18n (~> 0.7)
13
+ json (~> 1.7, >= 1.7.7)
14
+ minitest (~> 5.1)
15
+ thread_safe (~> 0.3, >= 0.3.4)
16
+ tzinfo (~> 1.1)
17
+ arel (6.0.3)
18
+ builder (3.2.2)
19
+ gnuplot (2.6.2)
20
+ i18n (0.7.0)
21
+ json (1.8.3)
22
+ minitest (5.9.0)
23
+ pg (0.18.4)
24
+ ruby-progressbar (1.7.5)
25
+ thread_safe (0.3.5)
26
+ tzinfo (1.2.2)
27
+ thread_safe (~> 0.1)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ activerecord (~> 4.2.5)
34
+ gnuplot (~> 2.6.2)
35
+ pg (~> 0.18.4)
36
+ ruby-progressbar (~> 1.7.5)
37
+
38
+ BUNDLED WITH
39
+ 1.12.5
@@ -0,0 +1,126 @@
1
+ Finding a Ruby memory leak using a time analysis
2
+ ================================================
3
+
4
+ When developing a program in [Ruby](http://ruby-lang.org), you may sometimes encounter a memory leak.
5
+ For a while now, Ruby has a facility to gather information about what objects are laying around:
6
+ [ObjectSpace](http://ruby-doc.org/core/ObjectSpace.html).
7
+
8
+ There are several approaches one can take to debug a leak. This discusses a time-based approach, where
9
+ a full memory dump is generated every, say, 5 minutes, during a time that the memory leak is showing up.
10
+ Afterwards, one can look at all the objects, and find out which ones are staying around, causing the
11
+ memory leak.
12
+
13
+
14
+ Gather
15
+ ------
16
+
17
+ Setup your Ruby application to dump all objects to a file. If you have an event loop, something like this would work:
18
+
19
+ ```ruby
20
+ require 'objspace'
21
+
22
+ def heap_dump
23
+ GC.start
24
+
25
+ i = Time.now.strftime('%s')
26
+
27
+ open("/tmp/ruby-heap-#{i}.dump", "w") do |io|
28
+ ObjectSpace.dump_all(output: io)
29
+ end
30
+
31
+ # On Heroku you'll need to push it elsewhere, like S3
32
+ #s3 = AWS::S3.new(access_key_id: ENV['S3_ACCESS_KEY'], secret_access_key: ENV['S3_SECRET_KEY'])
33
+ #bucket = s3.buckets['qm-import-export']
34
+ #obj = bucket.objects["ruby-heap-#{i}.jsonl"]
35
+ #obj.write(IO.binread(path))
36
+ end
37
+
38
+ ObjectSpace.trace_object_allocations_start
39
+ mainloop do
40
+ # assuming your mainloop does the work, and calls this block every 5 minutes
41
+ heap_dump
42
+ end
43
+ ```
44
+
45
+ Or, if you're having a Rails app, do this in a controller that you visit every 5 minutes
46
+
47
+ ```ruby
48
+ # app/controllers/heap_dumps_controller.rb
49
+ class HeapDumpsController < ActionController::Metal
50
+
51
+ def heap_dump
52
+ if ENV['HEAP_DUMP'] == '1' && params[:token].to_s == ENV['HEAP_DUMP_TOKEN']
53
+ heap_dump
54
+ self.response_body = 'Dumped heap'
55
+ else
56
+ self.status = 401
57
+ self.response_body = 'Invalid token'
58
+ end
59
+ end
60
+ end
61
+
62
+ # add to config/routes.rb
63
+ get "/heap_dump", to: HeapDumpsController.action(:heap_dump)
64
+
65
+ # config/initializers/heap_dump_tracing.rb
66
+ if ENV['HEAP_DUMP'] == 1
67
+ require 'objspace'
68
+ ObjectSpace.trace_object_allocations_start
69
+ end
70
+ ```
71
+
72
+
73
+ Install
74
+ -------
75
+
76
+ - Having [Ruby](http://ruby-lang.org/), install the dependencies with `bundle install`.
77
+ - Having [PostgreSQL](http://postgresql.org/), create the database with `createdb mem_analysis`.
78
+ - When getting dumps from Amazon S3, [s3cmd](https://github.com/s3tools/s3cmd) may come in handy.
79
+
80
+
81
+ Import
82
+ ------
83
+
84
+ If stored on S3, get the dump list. Update the bucket and date in the grep command to reflect your case. This stores filenames and dates in _index.txt_.
85
+
86
+ S3_URL=s3://qm-import-export/
87
+ s3cmd ls $S3_URL | grep '^2015-11-23' | sed 's/[0-9]*\+\s\+s3:.*\///' >index.txt
88
+
89
+ Then download them:
90
+
91
+ for file in `cat index.txt | awk '{print $3}'`; do s3cmd get $S3_URL/$file $file; done
92
+
93
+ Initialize the database:
94
+
95
+ bundle exec ruby createdb.rb
96
+
97
+ Because importing can take quite a while, this is split into two steps: converting each file to SQL, and loading all into the database:
98
+
99
+ bundle exec ruby gencsv.rb
100
+ sh genimport.sh | psql mem_analysis
101
+
102
+
103
+ Analyse
104
+ -------
105
+
106
+ Now that the database is loaded, we're ready to gather information.
107
+ To find out what is causing a memory leak, we can look at graphs plotting memory usage over time in different dimensions.
108
+ This is done by `graph.rb`. Let's start with the object type.
109
+
110
+ bundle exec ruby graph.rb type-mem
111
+
112
+ This will create the file _graph-type-mem.png_ showing the total size of objects by type. If there's one thing leaking,
113
+ you'll probably have a number of somewhat flat lines, and one with a positive slope, which is the culprit.
114
+
115
+ Then create a similar graph for that object type only, and plot lines by file, for example. This gives one an idea in which
116
+ gem the leaking objects may be created. If it's a string, run
117
+
118
+ bundle exec ruby graph.rb string-mem
119
+
120
+ If it's something else, edit _graph.rb_ and expand the `case`-block. In this way you may be able to zoom in on the cause.
121
+
122
+
123
+ Sample
124
+ ------
125
+
126
+ ![graph-type-count](https://cloud.githubusercontent.com/assets/503804/11392637/56f47762-935b-11e5-8122-a7bfd16cbec8.png)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative 'db'
3
+
4
+ init_database
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+ require 'active_record'
3
+
4
+ ActiveRecord::Base.establish_connection({adapter: 'postgresql', database: 'mem_analysis'})
5
+
6
+ def connection
7
+ ActiveRecord::Base.connection
8
+ end
9
+
10
+ class SpaceObject < ActiveRecord::Base
11
+ self.inheritance_column = 'zoink' # use type as ordinary column (not STI)
12
+ has_many :references, class_name: 'SpaceObjectReference', foreign_key: 'from_id', inverse_of: 'from', dependent: :destroy
13
+ has_one :default, class_name: 'SpaceObject', foreign_key: 'default', primary_key: 'address'
14
+ end
15
+
16
+ class SpaceObjectReference < ActiveRecord::Base
17
+ belongs_to :from, class_name: 'SpaceObject', required: true, inverse_of: 'references'
18
+ belongs_to :to, class_name: 'SpaceObject', foreign_key: 'to_address', primary_key: 'address'
19
+ end
20
+
21
+
22
+ def init_database(c = connection)
23
+ c.tables.each {|t| c.drop_table(t) }
24
+ c.create_table 'space_objects' do |t|
25
+ t.datetime :time
26
+ t.string :type
27
+ t.string :node_type
28
+ t.string :root
29
+ t.string :address
30
+ t.text :value
31
+ t.string :klass
32
+ t.string :name
33
+ t.string :struct
34
+ t.string :file
35
+ t.string :line
36
+ t.string :method
37
+ t.integer :generation
38
+ t.integer :size
39
+ t.integer :length
40
+ t.integer :memsize
41
+ t.integer :bytesize
42
+ t.integer :capacity
43
+ t.integer :ivars
44
+ t.integer :fd
45
+ t.string :encoding
46
+ t.string :default_address
47
+ t.boolean :freezed
48
+ t.boolean :fstring
49
+ t.boolean :embedded
50
+ t.boolean :shared
51
+ t.boolean :flag_wb_protected
52
+ t.boolean :flag_old
53
+ t.boolean :flag_long_lived
54
+ t.boolean :flag_marking
55
+ t.boolean :flag_marked
56
+ end
57
+ c.create_table 'space_object_references' do |t|
58
+ t.integer :from_id, null: false
59
+ t.string :to_address, null: false
60
+ end
61
+ restore_indexes
62
+ nil
63
+ end
64
+
65
+ def remove_indexes(c = connection)
66
+ c.indexes('space_objects').each {|i| connection.remove_index('space_objects', name: i.name) }
67
+ c.indexes('space_objects_references').each {|i| connection.remove_index('space_objects_references', name: i.name) }
68
+ end
69
+
70
+ def restore_indexes(c = connection)
71
+ c.change_table 'space_objects' do |t|
72
+ t.index :time
73
+ t.index :address
74
+ t.index :type
75
+ t.index [:klass, :method]
76
+ t.index [:file, :line]
77
+ t.index :size
78
+ t.index :memsize
79
+ end
80
+ c.execute('VACUUM ANALYZE')
81
+ end
82
+
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pg', '~> 0.18.4'
4
+ gem 'activerecord', '~> 4.2.5'
5
+ gem 'ruby-progressbar', '~> 1.7.5'
6
+ gem 'gnuplot', '~> 2.6.2'
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ruby-progressbar'
3
+ require 'json'
4
+ require 'csv'
5
+
6
+ def parse_dump(filename, &block)
7
+ lines = open(filename).readlines
8
+ lines.each do |line|
9
+ block.call JSON.parse(line), lines.count
10
+ end
11
+ end
12
+
13
+ def parse_index(filename, &block)
14
+ open(filename).each do |line|
15
+ dumpname,time = line.split(',').map(&:chomp)
16
+ block.call dumpname, time
17
+ end
18
+ end
19
+
20
+ FIELDS = %w(time type node_type root address value klass name struct file line method generation size length memsize bytesize capacity ivars fd encoding default_address freezed fstring embedded shared flag_wb_protected flag_old flag_long_lived flag_marking flag_marked)
21
+ REF_FIELDS = %w(id from_id to_address)
22
+
23
+ if ARGV[0].empty?
24
+ STDERR.puts "You must provide the index file"
25
+ exit
26
+ else
27
+ id = 1
28
+ ref_id = 1
29
+ parse_index(ARGV[0]) do |file, time|
30
+
31
+ next if ARGV.any? && !ARGV.include?(file)
32
+
33
+ progressbar = ProgressBar.create(title: file, format: "%t |%B| %c/%C %E", throttle_rate: 0.5)
34
+ CSV.open(file.gsub(/.jsonl$/i, '') + '.csv', 'w') do |csv|
35
+ csv << FIELDS
36
+ CSV.open(file.gsub(/.jsonl$/i, '') + '.refs.csv', 'w') do |ref_csv|
37
+ ref_csv << REF_FIELDS
38
+ parse_dump(file) do |data, count|
39
+ progressbar.total = count
40
+
41
+ data['value'] = data['value'].gsub(/[^[:print:]]/, '.') if data['value'] # allow string database column
42
+ data['klass'] = data.delete('class') if data['class'] # avoid error
43
+ data['freezed'] = data.delete('frozen') if data['frozen'] # idem
44
+ data['default_address'] = data.delete('default') if data['default'] # consistency
45
+ data['time'] = time
46
+ data['id'] = id
47
+ (data.delete('flags') || {}).each {|k, v| data["flag_#{k}"] = v }
48
+ data['default_address'] = data.delete('default') if data['default']
49
+ refs = data.delete('references') || []
50
+
51
+ csv << FIELDS.map {|f| data[f]}
52
+ refs.each do |ref|
53
+ ref_csv << [ref_id, id, ref]
54
+ ref_id += 1
55
+ end
56
+
57
+ id += 1
58
+ progressbar.increment
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ for file in mem_dumps/*.csv; do
3
+ table=space_objects
4
+ echo "$file" | grep -q '\.refs\.csv$' && table=space_object_references
5
+
6
+ echo "\\COPY $table (`head -n1 $file`) FROM '$file' WITH (FORMAT CSV, HEADER);"
7
+ done
8
+ echo "VACUUM ANALYZE;"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ require 'date'
3
+ require 'yaml'
4
+ require 'gnuplot'
5
+ require_relative 'db'
6
+
7
+
8
+ ### Parse arguments
9
+
10
+ type = ARGV[0]
11
+ type == 'type' and type = 'type-mem'
12
+
13
+ case type
14
+ when 'type-count'
15
+ ylabel = 'count'
16
+ query, ycolumn, group = nil, 'COUNT(id)', :type
17
+ key_pos = 'left top'
18
+ when 'type-mem'
19
+ query, ycolumn, group = nil, 'SUM(memsize)', :type
20
+ ylabel, yscale = 'memsize [MB]', 1024*1024
21
+ key_pos = 'left top'
22
+ when 'string-count'
23
+ ylabel = 'count'
24
+ query, ycolumn, group = {type: 'STRING'}, 'COUNT(id)', :file
25
+ when 'string-mem'
26
+ query, ycolumn, group = {type: 'STRING'}, 'SUM(memsize)', :file
27
+ ylabel, yscale = 'memsize [MB]', 1024*1024
28
+ when 'data-count'
29
+ ylabel = 'count'
30
+ query, ycolumn, group = {type: 'DATA'}, 'COUNT(id)', :file
31
+ when 'data-mem'
32
+ query, ycolumn, group = {type: 'DATA'}, 'SUM(memsize)', :file
33
+ ylabel, yscale = 'memsize [MB]', 1024*1024
34
+ else
35
+ STDERR.puts "Usage: graph <type>"
36
+ exit 1
37
+ end
38
+
39
+ xoffset = 60*60 # GMT+1
40
+ graph_basename = File.dirname(File.expand_path(__FILE__)) + '/graph-' + type
41
+
42
+
43
+ ### Read cache or execute query
44
+
45
+ if File.exists?(graph_basename + '.yml')
46
+ data = YAML.load(File.read(graph_basename + '.yml'))
47
+ else
48
+ scope = SpaceObject
49
+ scope = scope.where(**query) if query
50
+ scope = scope.order(ycolumn + ' DESC NULLS LAST')
51
+ scope = scope.group(:time, group)
52
+ data = scope.limit(500).pluck(group, :time, ycolumn)
53
+ File.open(graph_basename + '.yml', 'w') do |f|
54
+ f.write(data.to_yaml)
55
+ end
56
+ end
57
+
58
+
59
+ ### Then plot
60
+
61
+ Gnuplot.open(persist: true) do |gp|
62
+ Gnuplot::Plot.new(gp) do |plot|
63
+ plot.terminal 'png large'
64
+ plot.output graph_basename + '.png'
65
+
66
+ plot.xdata :time
67
+ plot.timefmt '"%s"'
68
+ plot.format 'x "%H:%M"'
69
+
70
+ plot.xlabel "time"
71
+ plot.ylabel ylabel
72
+ plot.key key_pos if key_pos
73
+
74
+ grouped_data = data.group_by(&:first)
75
+ keys = grouped_data.keys.sort_by {|key| -grouped_data[key].reduce(0) {|sum,d| sum + (d[2]||0) } }
76
+ keys[0,10].each do |key|
77
+ data = grouped_data[key]
78
+ data.sort_by!{|d| d[1] }
79
+ x = data.map{|d| d[1].to_i + (xoffset||0) }
80
+ y = data.map{|d| d[2] }
81
+ y = data.map{|d| (d[2]||0) / (yscale||1) }
82
+ plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds|
83
+ ds.using = '1:2'
84
+ ds.with = "linespoints"
85
+ ds.title = key || '(empty)'
86
+ end
87
+ end
88
+
89
+ end
90
+ end
91
+