batch_experiment 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d594a77b4909c00eb2c07a05dbc6c4f0cc68c9a
4
+ data.tar.gz: 979f0d1800486061aff5d9a1728e55790e675e34
5
+ SHA512:
6
+ metadata.gz: cc0d896402fc7820e7c1f2741ae9e07ffdea6ab14edc3156e9bdcd5d3f4c24b71e753087e1980fdb97c8b85006cc2c7ef1bbb3bdad3a181b0853a7c94f76ea82
7
+ data.tar.gz: bfde027e77f78360a9728eacca5075a00a2b59e47cd0224e692c2e44bc6118e942c93c4913a0bd65a5861612191f92deb4402052440037e416391c9433d2629c
@@ -0,0 +1,48 @@
1
+ #!/bin/ruby
2
+
3
+ require 'batch_experiment'
4
+ require 'batch_experiment/sample_extractors'
5
+
6
+ comms_info = [{
7
+ # String with command to be executed. Must have 'pattern' as substring.
8
+ command: 'sleep 1 && echo X X',
9
+ # Substring present in 'command'. Often replaced by the instance filename.
10
+ pattern: 'X',
11
+ # Extractor object. Receives the output of the command and return
12
+ # the most important fields.
13
+ extractor: SampleExtractor.new,
14
+ # String used to identify the command. Will be used to prefix the return of
15
+ # extractor.names.
16
+ prefix: 'doubled',
17
+ }, {
18
+ command: 'sleep 3 && echo "banana X"',
19
+ pattern: 'X',
20
+ extractor: SampleExtractor.new,
21
+ prefix: 'banana',
22
+ }, {
23
+ command: 'sleep 100 && echo "never gonna happen X"',
24
+ pattern: 'X',
25
+ extractor: SampleExtractor.new,
26
+ prefix: 'timeout',
27
+ }]
28
+
29
+ execution_info = {
30
+ # IDs of the CPU cores that can be used for executing tests.
31
+ cpus_available: [1, 2, 3],
32
+ # Maximum number of seconds that a command can run. After this a kill command
33
+ # (TERM signal) will be issued.
34
+ timeout: 5,
35
+ # Maximum number of seconds that a command can run after a kill command was
36
+ # issued. After this a kill -9 command (KILL signal) will be issued.
37
+ post_timeout: 1,
38
+ }
39
+
40
+ conf = {
41
+ # The name of the file where will be written the CSV data.
42
+ csvfname: 'sample.csv',
43
+ }
44
+
45
+ files = ['apple', 'orange'] # Applejack would be proud
46
+
47
+ BatchExperiment::experiment(comms_info, execution_info, conf, files)
48
+
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative 'batch_experiment'
4
+ require_relative 'batch_experiment/sample_extractors'
5
+
6
+ # I run the three lines below in the console to disable hyperthreading cores on
7
+ # my computer before examining the cores with the top command.
8
+ # for i in 4 5 6 7; do
9
+ # sudo sh -c "echo 0 > /sys/devices/system/cpu/cpu$i/online";
10
+ # done
11
+
12
+ comms_info = [{
13
+ command: 'pyasukpt -src INST_FILE',
14
+ pattern: 'INST_FILE',
15
+ extractor: PyaExtractor.new,
16
+ prefix: 'PYAsUKP',
17
+ }, {
18
+ command: 'run_ukp5.out INST_FILE',
19
+ pattern: 'INST_FILE',
20
+ extractor: UKP5Extractor.new,
21
+ prefix: 'UKP5',
22
+ }]
23
+
24
+ execution_info = {
25
+ cpus_available: [1, 2, 3],
26
+ timeout: 10,
27
+ post_timeout: 5,
28
+ }
29
+
30
+ conf = { csvfname: 'pya_site8.csv' }
31
+
32
+ files = ['corepb.ukp', 'exnsd18.ukp', 'exnsd26.ukp', 'exnsdbis18.ukp', 'exnsd16.ukp', 'exnsd20.ukp', 'exnsdbis10.ukp', 'exnsds12.ukp']
33
+ # If you don't execute the script from the ukp files folder you need to put the
34
+ # folder relative or absolute path here (with trailing slash).
35
+ path = ''
36
+ files.map! { | f | path + f }
37
+
38
+ experiment(comms_info, execution_info, conf, files)
39
+
@@ -0,0 +1,29 @@
1
+ module Extractor
2
+ # For when there's a field whose value is after '<field>: '.
3
+ def self.get_field(lines, field)
4
+ lines.grep(/^#{field}: .*/).each { | l | return l.match(/:[\t ]+(.*)/)[1] }
5
+ ''
6
+ end
7
+
8
+ # For when there's a field whose value is in the next line.
9
+ def self.get_hfield(lines, field)
10
+ if ix = lines.find_index(field) then lines[ix + 1] else '' end
11
+ end
12
+
13
+ # Return the field names for each of the elements returned by
14
+ # extract. Ex.: ['Time', 'Max Mem Use', 'opt', ... ]
15
+ def names
16
+ fail 'This method should have been overwritten by a subclass.'
17
+ end
18
+
19
+ def extract(content)
20
+ extract_from_lines(content.lines.map! { | l | l.chomp! })
21
+ end
22
+
23
+ # Extract an array of values from the command output. This array has the same
24
+ # size as the one returned by field_names.
25
+ def extract_from_lines(lines)
26
+ fail 'This method should have been overwritten by a subclass.'
27
+ end
28
+ end
29
+
@@ -0,0 +1,49 @@
1
+ require 'require_relative'
2
+ require_relative './extractor.rb'
3
+
4
+ # Sample extractors used at https://github.com/henriquebecker91/masters, where
5
+ # this code had its beggining. This file contains the code used to extract info
6
+ # from the different outputs generated by UKP solving programs.
7
+
8
+ class SampleExtractor
9
+ include Extractor
10
+ def names
11
+ ['first word', 'second word', 'ext_time', 'ext_mem']
12
+ end
13
+
14
+ def extract_from_lines(lines)
15
+ words = lines.empty? || lines[0].nil? ? ['',''] : lines[0].split().take(2)
16
+ words << Extractor.get_field(lines, 'ext_time')
17
+ words << Extractor.get_field(lines, 'ext_mem')
18
+ words
19
+ end
20
+ end
21
+
22
+ class UKP5Extractor
23
+ include Extractor
24
+ def names
25
+ ['internal time', 'external time', 'external memory', 'opt']
26
+ end
27
+
28
+ def extract_from_lines(lines)
29
+ ['Seconds', 'ext_time', 'ext_mem', 'opt'].map do | label |
30
+ Extractor.get_field(lines, label)
31
+ end
32
+ end
33
+ end
34
+
35
+ class PyaExtractor
36
+ include Extractor
37
+ def names
38
+ ['internal time', 'external time', 'external memory', 'opt']
39
+ end
40
+
41
+ def extract_from_lines(lines)
42
+ values = ['Total Time ', 'ext_time', 'ext_mem'].map do | label |
43
+ Extractor.get_field(lines, label)
44
+ end
45
+ opt_key = '#The optimal value for the given capacity'
46
+ values << Extractor.get_hfield(lines, opt_key)
47
+ end
48
+ end
49
+
@@ -0,0 +1,345 @@
1
+ require 'childprocess'
2
+ require 'pathname'
3
+
4
+ module BatchExperiment
5
+ # The default callable class used by batch to convert a command into a
6
+ # filename.
7
+ class FilenameSanitizer
8
+ def call(command)
9
+ fname = command.strip
10
+ fname.gsub!(/[^[:alnum:]]/, '_')
11
+ fname.gsub!(/_+/, '_')
12
+ fname.gsub!(/^_/, '')
13
+ fname.gsub!(/_$/, '')
14
+ fname
15
+ end
16
+ end
17
+
18
+ # Internal use only. DO NOT DEPEND.
19
+ # Remove any finished commands from comms_running, insert the cpus
20
+ # freed by the commands termination to the free_cpus, insert the
21
+ # terminated commands on comms_executed.
22
+ def self.update_finished(free_cpus, comms_running, comms_executed)
23
+ comms_running.delete_if do | job |
24
+ if job[:proc].exited?
25
+ free_cpus.push(job[:cpu])
26
+ File.delete(job[:lockfname])
27
+ comms_executed << job[:command]
28
+ end
29
+ job[:proc].exited? # bool returned to delete_if
30
+ end
31
+ end
32
+
33
+ # Takes a list of commands, execute them only on the designed core/cpus, and
34
+ # kill them if the timeout expires, never lets a core/cpu rest for more than
35
+ # conf[:busy_loop_sleep] seconds between a command and another. The
36
+ # conf[:fname_sanitizer] is called over the commands to generate partial
37
+ # filenames. Appending '.out' to one of the partial filenames will give the
38
+ # filename were the command stdout was redirected. The analogue is valid for
39
+ # '.err' and stderr. The first partial filename corresponds to the first
40
+ # command in commands, and so on. Right before a command begans to run, a
41
+ # "partial_filename.#{conf[:unfinished_ext]}" file is created. After the
42
+ # command ends its execution this file is removed. If the command ends its
43
+ # execution by means of a timeout the file is also removed. The file only
44
+ # remains if the batch procedure is interrupted (not a specific command).
45
+ #
46
+ # @param commands [Array<String>] The shell commands.
47
+ # @param conf [Hash] The configurations, as follows:
48
+ # :cpus_available [Array<Fixnum>] Cpu cores that can be used to run the
49
+ # commands. Required parameter. The cpu numbers begin at 0, despite what
50
+ # htop tells you;
51
+ # :timeout [Number] Number of seconds before killing a command. Required
52
+ # parameter. Is the same for all the commands;
53
+ # :time_fmt [String] A string in the time (external command) format. See
54
+ # http://linux.die.net/man/1/time. Default: 'ext_time: %e\next_mem: %M\n'.
55
+ # :busy_loop_sleep [Number] How many seconds to wait before checking if a
56
+ # command ended execution. This is max time a cpu will be vacant between
57
+ # two commands. Default: 0.1;
58
+ # :post_timeout [Number] A command isn't guaranteed to end after receiving
59
+ # a TERM signal. If the command hasn't stopped, waits post_timeout seconds
60
+ # before sending a KILL signal (give it a chance to end gracefully).
61
+ # Default: 5;
62
+ # :fname_sanitizer [Callable Object] The call method of this object
63
+ # should take a String and convert it (possibly losing information), to a
64
+ # valid filename. Used over the commands to define the output files of
65
+ # commands.
66
+ # Default: BatchExperiment::FilenameSanitizer.new;
67
+ # :skip_done_comms [FalseClass,TrueClass] Skip any command for what a
68
+ # corresponding '.out' file exists, except if both a '.out' and a
69
+ # '.unfinished' file exist, in the last case the command is executed.
70
+ # Default: true;
71
+ # :unfinished_ext [String] Extension to be used in place of '.unfinished'.
72
+ # Default: '.unfinished';
73
+ # :out_ext [String] Extension to be used in place of '.out'.
74
+ # Default: '.out';
75
+ # :err_ext [String] Extension to be used in place of '.err'.
76
+ # Default: '.err';
77
+ # @return [String] Which commands were executed. Can be different from
78
+ # the 'commands' argument if commands are skipped (see :skip_done_comms).
79
+ #
80
+ # @note This procedure was not designed to support equal commands (the last
81
+ # equal command executed will subscribe the '.out', '.err' and '.unfinished'
82
+ # files used by any previous equal command). But the parameter
83
+ # conf[:fname_sanitizer] can be used to circumvent the restriction over
84
+ # equal commands (if the object has state it can return a different
85
+ # filename for every time it's called with the same argument).
86
+ # @note This procedure makes use of the following linux commands: time (not
87
+ # the bash internal one, but the package one, i.e.
88
+ # https://www.archlinux.org/packages/extra/x86_64/time/); timeout (from
89
+ # coreutils); taskset (from util-linux,
90
+ # https://www.archlinux.org/packages/core/x86_64/util-linux/); sh (the
91
+ # shell).
92
+ # @note The command is executed inside a call to "sh -c command", so it has
93
+ # to be a valid sh command.
94
+ # @note The output of the command "time -f #{conf[:time_fmt]}" will be
95
+ # appended to the '.out' file of every command. If you set conf[:time_fmt]
96
+ # to a empty string only a newline will be appended.
97
+ def self.batch(commands, conf)
98
+ # Throw exceptions if required configurations aren't provided.
99
+ fail "conf[:cpus_available] not set" unless conf[:cpus_available]
100
+ fail "conf[:timeout] not set" unless conf[:timeout]
101
+
102
+ # Initialize optional configurations with default values if they weren't
103
+ # provided. Don't change the conf argument, only our version of conf.
104
+ conf = conf.clone
105
+ conf[:time_fmt] ||= 'ext_time: %e\\next_mem: %M\\n'
106
+ conf[:unfinished_ext] ||= '.unfinished'
107
+ conf[:out_ext] ||= '.out'
108
+ conf[:err_ext] ||= '.err'
109
+ conf[:busy_loop_sleep] ||= 0.1
110
+ conf[:post_timeout] ||= 5
111
+ conf[:fname_sanitizer] ||= BatchExperiment::FilenameSanitizer.new
112
+ conf[:skip_done_comms] = true if conf[:skip_done_comms].nil?
113
+
114
+ # Initialize main variables
115
+ free_cpus = conf[:cpus_available].clone
116
+ comms_running = []
117
+ cpu = nil
118
+ comms_executed = []
119
+
120
+ commands.each do | command |
121
+ commfname = conf[:fname_sanitizer].call(command)
122
+ out_fname = commfname + conf[:out_ext]
123
+ err_fname = commfname + conf[:err_ext]
124
+ lockfname = commfname + conf[:unfinished_ext]
125
+
126
+ if conf[:skip_done_comms] && File.exists?(out_fname)
127
+ if File.exists?(lockfname)
128
+ puts "found file #{out_fname}, but a #{lockfname} also exists"
129
+ puts "will execute command '#{command}' anyway"
130
+ else
131
+ puts "found file #{commfname}, skipping command: #{command}"
132
+ STDOUT.flush
133
+ next
134
+ end
135
+ end
136
+
137
+ puts "waiting to execute command: #{command}"
138
+ STDOUT.flush
139
+
140
+ while free_cpus.empty? do
141
+ sleep conf[:busy_loop_sleep]
142
+ update_finished(free_cpus, comms_running, comms_executed)
143
+ end
144
+
145
+ cpu = free_cpus.pop
146
+
147
+ cproc = ChildProcess.build(
148
+ 'taskset', '-c', cpu.to_s,
149
+ 'time', '-f', conf[:time_fmt], '--append', '-o', out_fname,
150
+ 'timeout', '--preserve-status', '-k', "#{conf[:post_timeout]}s",
151
+ "#{conf[:timeout]}s",
152
+ 'sh', '-c', command
153
+ )
154
+
155
+ File.open(lockfname, 'w') {} # empty on purpose
156
+ out = File.open(out_fname, 'w')
157
+ err = File.open(err_fname, 'w')
158
+
159
+ cproc.io.stdout = out
160
+ cproc.io.stderr = err
161
+
162
+ cproc.start
163
+
164
+ comms_running << {
165
+ proc: cproc,
166
+ cpu: cpu,
167
+ lockfname: lockfname,
168
+ command: command
169
+ }
170
+
171
+ puts "command assigned to cpu#{cpu}"
172
+ STDOUT.flush
173
+ end
174
+
175
+ until comms_running.empty? do
176
+ sleep conf[:busy_loop_sleep]
177
+ update_finished(free_cpus, comms_running, comms_executed)
178
+ end
179
+
180
+ comms_executed
181
+ end
182
+
183
+ # gencommff: GENerate COMMands For Files
184
+ #
185
+ # @param comm [String] A string with 'patt' as a substring.
186
+ # @param patt [String] A string contained in 'comm'.
187
+ # @param files [Enumerable<String>] A list of strings to substitute patt at
188
+ # comm.
189
+ # @return [Array<String>] Example: gencommff('echo STR', 'STR', ['a', 'b',
190
+ # 'c']) returns ['echo a', 'echo b', 'echo c'].
191
+ def self.gencommff(comm, patt, files)
192
+ ret = []
193
+ files.each { | f | ret << comm.gsub(patt, f) }
194
+ ret
195
+ end
196
+
197
+ # Intercalate a variable number of variable sized arrays in one array.
198
+ #
199
+ # @param [Array<Array<Object>>] xss An array of arrays.
200
+ # @return [Array<Object>] An array of the same size as the sum of the size
201
+ # of all inner arrays. The values are the same (not copies) as the values
202
+ # of the array. Example: intercalate([[1, 4, 6, 7], [], [2, 5], [3]])
203
+ # returns [1, 2, 3, 4, 5, 6, 7].
204
+ def self.intercalate(xss)
205
+ ret = []
206
+ xss = xss.map { | xs | xs.reverse }
207
+ until xss.empty? do
208
+ xss.delete_if do | xs |
209
+ unless xs.empty?
210
+ ret << xs.pop
211
+ end
212
+ xs.empty?
213
+ end
214
+ end
215
+ ret
216
+ end
217
+
218
+ # Takes N shell commands and M files/parameters, execute each command of the
219
+ # N commands over the M files, save the output of each command/file
220
+ # combination, use objects provided with the command to extract relevant
221
+ # information from the output file, and group those information in a CVS
222
+ # file. Easier to understand seeing the sample_batch.rb example in action.
223
+ #
224
+ # @param comms_info [Array<Hash>] An array of hashs, each with the config
225
+ # needed to know how to deal with the command. Four required fields
226
+ # (all keys are symbols):
227
+ # command [String] A string with a sh shell command.
228
+ # pattern [String] A substring of command, will be replace by the strings
229
+ # in the paramenter 'files'.
230
+ # extractor [Extractor] An object that implements the Extractor interface.
231
+ # prefix [String] A string that will be used to prefix the extractor.names
232
+ # when they are used as column names. Improves Extractor reusability.
233
+ # @param batch_conf [Hash] Configuration used to call batch. See the
234
+ # explanation for parameter 'conf' on the documentation of the batch
235
+ # method. There are required fields for this hash parameter.
236
+ # @param conf [Hash] Lots of parameters. Here's a list:
237
+ # csvfname [String] The filename/filepath for the file that will contain
238
+ # the CSV data. Required field.
239
+ # separator [String] The separator used at the CSV file. Default: ';'.
240
+ # ic_columns [TrueClass, FalseClass] Intercalate the data returned by the
241
+ # extractors. In other words, the csv line for some file will not present
242
+ # all fields of the first command, then all fields of the second command,
243
+ # etc, but instead will present the first field of all commands, the second
244
+ # field of all commands, and so on. Default: true.
245
+ # ic_comms [TrueClass, FalseClass] Intercalate the commands execution.
246
+ # Instead of executing the first command over all files first, execute all
247
+ # the commands over the first file first. This was made to avoid
248
+ # confounding (statistical concept). If something disrupts the processing
249
+ # power for some period of time, the effect will probably be distributed
250
+ # between commands. The risk some algorithm seems better or worse than it
251
+ # really is will be reduced. For example: you are making tests at an
252
+ # notebook, the notebook becomes unplugged for a short time. The cores will
253
+ # probably enter in energy saving mode and affect the observed performance.
254
+ # If this happens when all tested commands are the same, then will seem
255
+ # that that an command had a worse performance. If this happens when the
256
+ # commands are intercalated, then maybe some instances will seem harder
257
+ # than others (what is less problematic). Default: true.
258
+ # skip_commands [TrueClass, FalseClass] If true, will not execute the
259
+ # commands and assume that the outputs are already saved. Will only execute
260
+ # the extractors over the already saved outputs, and create the CSV file
261
+ # from them. Default: false.
262
+ #
263
+ # @param files [Array<Strings>] The strings that will replace the :pattern
264
+ # on :command, for every element in comms_info.
265
+ #
266
+ # @return [NilClass,Array<String>] The return of the internal #batch
267
+ # call. Returns nil if conf[:skip_commands] was set to true.
268
+ #
269
+ # @see BatchExperiment.batch
270
+ def self.experiment(comms_info, batch_conf, conf, files)
271
+ # Throw exceptions if required configurations aren't provided.
272
+ fail 'conf[:csvfname] is not defined' unless conf[:csvfname]
273
+
274
+ # Initialize optional configurations with default values if they weren't
275
+ # provided. Don't change the conf argument, only our version of conf.
276
+ conf = conf.clone
277
+ conf[:separator] ||= ';'
278
+ conf[:ic_columns] = true if conf[:ic_columns].nil?
279
+ conf[:ic_comms] = true if conf[:ic_comms].nil?
280
+ #conf[:skip_commands] defaults to false/nil
281
+
282
+ # Get some of the batch config that we use inside here too.
283
+ out_ext = batch_conf[:out_ext] || '.out'
284
+ unfinished_ext = batch_conf[:unfinished_ext] || '.unfinished'
285
+ fname_sanitizer = batch_conf[:fname_sanitizer]
286
+ fname_sanitizer ||= BatchExperiment::FilenameSanitizer.new
287
+
288
+ # Create commands the templates and the file list.
289
+ comms_sets = []
290
+ comms_info.each do | comm_info |
291
+ comms_sets << gencommff(comm_info[:command], comm_info[:pattern], files)
292
+ end
293
+
294
+ comm_list = conf[:ic_comm] ? intercalate(comms_sets) : comms_sets.flatten
295
+
296
+ # Execute the commands (or not).
297
+ ret = batch(comm_list, batch_conf) unless conf[:skip_commands]
298
+
299
+ # Build header (first csv line, column names).
300
+ header = []
301
+ comms_info.each do | comm_info |
302
+ prefixed_names = comm_info[:extractor].names.map do | name |
303
+ (comm_info[:prefix] + ' ') << name
304
+ end
305
+ header << prefixed_names
306
+ end
307
+ header = intercalate(header) if conf[:ic_columns]
308
+ header = ['Filename'].concat(header).join(conf[:separator])
309
+
310
+ # Build body (inspect all output files an make csv lines).
311
+ body = [header]
312
+ files.each_with_index do | inst_fname, j |
313
+ line = []
314
+ comms_info.each_with_index do | comm_info, i |
315
+ command =
316
+ if conf[:ic_comm]
317
+ comm_list[(j * comms_info.size) + i]
318
+ else
319
+ comm_list[(i * files.size) + j]
320
+ end
321
+
322
+ partial_fname = fname_sanitizer.call(command)
323
+ out_fname = partial_fname + out_ext
324
+ lockfname = partial_fname + unfinished_ext
325
+ if File.exists?(out_fname)
326
+ f_content = File.open(out_fname, 'r') { | f | f.read }
327
+ line << comm_info[:extractor].extract(f_content)
328
+ else
329
+ # if the file wasn't created insert a empty column set
330
+ # of the same size the true column set would be
331
+ line << comm_info[:extractor].names.map { | _ | '' }
332
+ end
333
+ end
334
+ line = intercalate(line) if conf[:ic_columns]
335
+ body << [inst_fname].concat(line).join(conf[:separator])
336
+ end
337
+ body = body.map! { | line | line << conf[:separator] }.join("\n")
338
+
339
+ # Write CSV data into a CSV file.
340
+ File.open(conf[:csvfname], 'w') { | f | f.write(body) }
341
+
342
+ return ret
343
+ end
344
+ end
345
+
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: batch_experiment
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henrique Becker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: childprocess
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ description: ''
28
+ email: henriquebecker91@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - examples/sample_batch.rb
34
+ - examples/ukp_batch.rb
35
+ - lib/batch_experiment.rb
36
+ - lib/batch_experiment/extractor.rb
37
+ - lib/batch_experiment/sample_extractors.rb
38
+ homepage: https://rubygems.org/gems/batch_experiment
39
+ licenses:
40
+ - Public Domain
41
+ - Unlicense
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 2.5.1
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: A ruby script that distributes system commands between cpu cores, and save
63
+ their output.
64
+ test_files: []