nose-cli 0.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69482346fff002e27d56b7826eec5ab14f4626db
4
+ data.tar.gz: 07e536d085b0a88fb38f8650a940e4a54c18fbf6
5
+ SHA512:
6
+ metadata.gz: 1c0e8ab04a53a5107a6ff9559f218eea28db5437d21597720c508867d5d737ce065a742a331ab0742df81bb85efe5f1c6b6737ec00cab204cad1879444b1e73b
7
+ data.tar.gz: 3e7ab9f5d4b30a71697a15cface20dbf99998f738504f0d83cba5b165cb3cfbad588bc380378c3dd637e5e2f021acffc908d03fe3abaad17fb687f66ac9da93f
data/bin/nose ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Optionally enable debug logging
5
+ ENV['NOSE_LOG'] = 'debug' if ARGV.include?('--debug') || ARGV.include?('-d')
6
+
7
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
8
+
9
+ require 'nose'
10
+ require 'nose_cli'
11
+
12
+ # Start profiling if asked
13
+ unless ENV['NOSE_PROFILE'].nil?
14
+ require 'ruby-prof'
15
+ Parallel.instance_variable_set(:@processor_count, 0)
16
+ RubyProf.start
17
+ end
18
+
19
+ NoSE::CLI::NoSECLI.start ARGV
20
+
21
+ # Stop profiling and output results
22
+ unless ENV['NOSE_PROFILE'].to_i == 0
23
+ result = RubyProf.stop
24
+ printer = RubyProf::CallTreePrinter.new(result)
25
+ printer.print
26
+ end
data/bin/random_rubis ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Get and print the seed which is used
4
+ seed = ::Random.new_seed
5
+ $stderr.puts "SEED #{seed}"
6
+ ::Random.srand seed
7
+
8
+ require 'nose'
9
+
10
+ # Record times for the longest running sets of methods
11
+ times = {
12
+ indexes_for_workload: 0,
13
+ query_costs: 0,
14
+ update_costs: 0,
15
+ setup_model: 0,
16
+ solve: 0
17
+ }
18
+ NoSE::Timer.enable do |_cls, method, time|
19
+ times[method] = time if times.key? method
20
+ end
21
+
22
+ factor = ARGV[0].to_i
23
+
24
+ # Create a random workload generator
25
+ network = NoSE::Random::WattsStrogatzNetwork.new(nodes_nb: 7 * factor)
26
+ workload = NoSE::Workload.new
27
+ network.entities.each { |entity| workload << entity }
28
+ sgen = NoSE::Random::StatementGenerator.new workload.model
29
+
30
+ # Add random queries
31
+ 1.upto(30 * factor).each do |i|
32
+ path_length = rand > 0.9 ? 2 : 1
33
+ path_length = 3 if i <= factor
34
+
35
+ r = rand
36
+ conditions = if r > 0.95
37
+ 3
38
+ elsif r > 0.75
39
+ 2
40
+ else
41
+ 1
42
+ end
43
+ q = sgen.random_query path_length,
44
+ 3,
45
+ conditions,
46
+ rand > 0.9
47
+ $stderr.puts q.unparse
48
+ workload.add_statement q, 10
49
+ end
50
+
51
+ # Add random updates
52
+ 1.upto(3 * factor).each do
53
+ u = sgen.random_update 1, 2, 1
54
+ $stderr.puts u.text
55
+ workload.add_statement u
56
+ end
57
+
58
+ # Add random inserts
59
+ 1.upto(5 * factor).each do
60
+ i = sgen.random_insert
61
+ $stderr.puts i.text
62
+ workload.add_statement i
63
+ end
64
+
65
+ # Uncomment the lines below to enable profiling
66
+ # (along with the lines above to save the output)
67
+ # require 'ruby-prof'
68
+ # Parallel.instance_variable_set(:@processor_count, 0)
69
+ # RubyProf.start
70
+
71
+ # Execute NoSE for the random workload and report the time
72
+ start = Time.now.utc
73
+ indexes = NoSE::IndexEnumerator.new(workload).indexes_for_workload.to_a
74
+ search = NoSE::Search::Search.new(workload,
75
+ NoSE::Cost::RequestCountCost.new)
76
+ search.search_overlap(indexes)
77
+ elapsed = Time.now.utc - start
78
+
79
+ # Output the timing values
80
+ total = 0
81
+ times[:costs] = times.delete(:query_costs) + times.delete(:update_costs)
82
+ times.each do |key, time|
83
+ puts "#{key},#{time}"
84
+ total += time
85
+ end
86
+ puts "other,#{elapsed - total}"
87
+
88
+ # Uncomment the lines below to save profile output
89
+ # (along with the lines above to enable profiling)
90
+ # result = RubyProf.stop
91
+ # result.eliminate_methods!([
92
+ # /NoSE::Field#hash/,
93
+ # /Range#/,
94
+ # /Array#/,
95
+ # /Set#/,
96
+ # /Hash#/,
97
+ # /Integer#downto/,
98
+ # /Hashids#/,
99
+ # /String#/,
100
+ # /Enumerable#/,
101
+ # /Integer#times/,
102
+ # /Class#new/
103
+ # ])
104
+ # printer = RubyProf::CallTreePrinter.new(result)
105
+ # printer.print(File.open('prof.out', 'w'))
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+
3
+ # Get the first data directory (we assume there's only one)
4
+ # The backup directory is just this directory with -bk appended
5
+ DATA_DIR=`grep data_file_directories -A 1 /etc/cassandra/cassandra.yaml | \
6
+ tail -n +2 | sed 's/^\s\+-\s\+//; s/\/[^/]*$//'`
7
+
8
+ # Ideally sudo should be usable without a password so this can be automated
9
+ # We simply stop Cassandra, restore old data from a backup and restart
10
+ time sudo sh -c "service cassandra stop; \
11
+ rm -rf $DATA_DIR; \
12
+ cp -r $DATA_DIR-bk $DATA_DIR; \
13
+ chown -R cassandra:cassandra $DATA_DIR; \
14
+ service cassandra start"
15
+
16
+ until cqlsh `hostname -i` -e "USE $1"
17
+ do
18
+ echo 'Waiting for Cassandra...'
19
+ sleep 5
20
+ done
@@ -0,0 +1,61 @@
1
+ #!/bin/sh
2
+
3
+ eval `bundle exec nose export`
4
+
5
+ # The first argument should be the directory where results are stored
6
+ RESULTS_DIR=$1
7
+ REPEAT=1
8
+ ITERATIONS=1000
9
+ COMMON_OPTIONS="--num-iterations=$ITERATIONS --repeat=$REPEAT --format=csv"
10
+
11
+ # Enable command output and fail on error
12
+ set -e
13
+ set -x
14
+
15
+ mkdir -p $RESULTS_DIR
16
+
17
+ # Passwordless SSH access must be set up to the backend host
18
+ restart_cassandra() {
19
+ ssh $BACKEND_HOSTS_0 `pwd`/bin/restart_cassandra.sh $BACKEND_KEYSPACE
20
+ }
21
+
22
+ run_nose_search() {
23
+ bundle exec nose search rubis --format=json --mix=$1 > $RESULTS_DIR/$1.json
24
+ }
25
+
26
+ run_nose_search bidding
27
+ restart_cassandra
28
+
29
+ bundle exec nose benchmark $COMMON_OPTIONS --mix=bidding \
30
+ $RESULTS_DIR/bidding.json > $RESULTS_DIR/bidding.csv
31
+
32
+ run_nose_search write_heavy
33
+ restart_cassandra
34
+
35
+ bundle exec nose benchmark $COMMON_OPTIONS --mix=write_heavy \
36
+ $RESULTS_DIR/write_heavy.json > $RESULTS_DIR/write_heavy.csv
37
+
38
+ restart_cassandra
39
+
40
+ bundle exec nose benchmark $COMMON_OPTIONS --mix=write_heavy \
41
+ $RESULTS_DIR/bidding.json > $RESULTS_DIR/bidding_write_heavy.csv
42
+
43
+ restart_cassandra
44
+
45
+ bundle exec nose execute $COMMON_OPTIONS --mix=bidding \
46
+ rubis_expert > $RESULTS_DIR/expert.csv
47
+
48
+ restart_cassandra
49
+
50
+ bundle exec nose execute $COMMON_OPTIONS --mix=write_heavy \
51
+ rubis_expert > $RESULTS_DIR/expert_write_heavy.csv
52
+
53
+ restart_cassandra
54
+
55
+ bundle exec nose execute $COMMON_OPTIONS --mix=bidding \
56
+ rubis_baseline > $RESULTS_DIR/baseline.csv
57
+
58
+ restart_cassandra
59
+
60
+ bundle exec nose execute $COMMON_OPTIONS --mix=write_heavy \
61
+ rubis_baseline > $RESULTS_DIR/baseline_write_heavy.csv
@@ -0,0 +1,32 @@
1
+ # Connection to the backend database being targeted, currently only Cassandra
2
+ backend:
3
+ name: cassandra
4
+ hosts:
5
+ - localhost
6
+ port: 9042
7
+ keyspace: nose
8
+
9
+ # Cost model name and parameters
10
+ cost_model:
11
+ name: request_count
12
+
13
+ # Loader-specific configuration
14
+ # The mysql loader is recommended, but csv might work as well if
15
+ # your generated indexes all have path length one
16
+ loader:
17
+ name: mysql
18
+ host: 127.0.0.1
19
+ database: rubis
20
+ username: root
21
+ password: root
22
+
23
+ # Query proxy
24
+ proxy:
25
+ name: mysql
26
+ port: 3307
27
+
28
+ # vim: set syntax=yaml:
29
+
30
+ # Local Variables:
31
+ # mode:yaml
32
+ # End:
data/lib/nose_cli.rb ADDED
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'formatador'
5
+ require 'parallel'
6
+ require 'thor'
7
+ require 'yaml'
8
+
9
+ require 'nose'
10
+ require_relative 'nose_cli/measurements'
11
+
12
+ module NoSE
13
+ # CLI tools for running the advisor
14
+ module CLI
15
+ # A command-line interface to running the advisor tool
16
+ class NoSECLI < Thor
17
+ # The path to the configuration file in the working directory
18
+ CONFIG_FILE_NAME = 'nose.yml'
19
+
20
+ check_unknown_options!
21
+
22
+ class_option :debug, type: :boolean, aliases: '-d',
23
+ desc: 'enable detailed debugging information'
24
+ class_option :parallel, type: :boolean, default: false,
25
+ desc: 'run various operations in parallel'
26
+ class_option :colour, type: :boolean, default: nil, aliases: '-c',
27
+ desc: 'enabled coloured output'
28
+ class_option :interactive, type: :boolean, default: true,
29
+ desc: 'allow actions which require user input'
30
+
31
+ def initialize(_options, local_options, config)
32
+ super
33
+
34
+ # Set up a logger for this command
35
+ cmd_name = config[:current_command].name
36
+ @logger = Logging.logger["nose::#{cmd_name}"]
37
+
38
+ # Peek ahead into the options and prompt the user to create a config
39
+ check_config_file interactive?(local_options)
40
+
41
+ force_colour(options[:colour]) unless options[:colour].nil?
42
+
43
+ # Disable parallel processing if desired
44
+ Parallel.instance_variable_set(:@processor_count, 0) \
45
+ unless options[:parallel]
46
+ end
47
+
48
+ private
49
+
50
+ # Check if the user has disabled interaction
51
+ # @return [Boolean]
52
+ def interactive?(options = [])
53
+ parse_options = self.class.class_options
54
+ opts = Thor::Options.new(parse_options).parse(options)
55
+ opts[:interactive]
56
+ end
57
+
58
+ # Check if the user has created a configuration file
59
+ # @return [void]
60
+ def check_config_file(interactive)
61
+ return if File.file?(CONFIG_FILE_NAME)
62
+
63
+ if interactive
64
+ no_create = no? 'nose.yml is missing, ' \
65
+ 'create from nose.yml.example? [Yn]'
66
+ example_cfg = File.join Gem.loaded_specs['nose-cli'].full_gem_path,
67
+ 'data', 'nose-cli', 'nose.yml.example'
68
+ FileUtils.cp example_cfg, CONFIG_FILE_NAME unless no_create
69
+ else
70
+ @logger.warn 'Configuration file missing'
71
+ end
72
+ end
73
+
74
+ # Add the possibility to set defaults via configuration
75
+ # @return [Thor::CoreExt::HashWithIndifferentAccess]
76
+ def options
77
+ original_options = super
78
+ return original_options unless File.exist? CONFIG_FILE_NAME
79
+ defaults = YAML.load_file(CONFIG_FILE_NAME).deep_symbolize_keys || {}
80
+ Thor::CoreExt::HashWithIndifferentAccess \
81
+ .new(defaults.merge(original_options))
82
+ end
83
+
84
+ # Get a backend instance for a given configuration and dataset
85
+ # @return [Backend::BackendBase]
86
+ def get_backend(config, result)
87
+ be_class = get_class 'backend', config
88
+ be_class.new result.workload.model, result.indexes,
89
+ result.plans, result.update_plans, config[:backend]
90
+ end
91
+
92
+ # Get a class of a particular name from the configuration
93
+ # @return [Object]
94
+ def get_class(class_name, config)
95
+ name = config
96
+ name = config[class_name.to_sym][:name] if config.is_a? Hash
97
+ require "nose/#{class_name}/#{name}"
98
+ name = name.split('_').map(&:capitalize).join
99
+ full_class_name = ['NoSE', class_name.capitalize,
100
+ name + class_name.capitalize]
101
+ full_class_name.reduce(Object) do |mod, name_part|
102
+ mod.const_get name_part
103
+ end
104
+ end
105
+
106
+ # Get a class given a set of options
107
+ # @return [Object]
108
+ def get_class_from_config(options, name, type)
109
+ object_class = get_class name, options[type][:name]
110
+ object_class.new(**options[type])
111
+ end
112
+
113
+ # Collect all advisor results for schema design problem
114
+ # @return [Search::Results]
115
+ def search_result(workload, cost_model, max_space = Float::INFINITY,
116
+ objective = Search::Objective::COST,
117
+ by_id_graph = false)
118
+ enumerated_indexes = IndexEnumerator.new(workload) \
119
+ .indexes_for_workload.to_a
120
+ Search::Search.new(workload, cost_model, objective, by_id_graph) \
121
+ .search_overlap enumerated_indexes, max_space
122
+ end
123
+
124
+ # Load results of a previous search operation
125
+ # @return [Search::Results]
126
+ def load_results(plan_file, mix = 'default')
127
+ representer = Serialize::SearchResultRepresenter.represent \
128
+ Search::Results.new
129
+ json = File.read(plan_file)
130
+ result = representer.from_json(json)
131
+ result.workload.mix = mix.to_sym unless \
132
+ mix.nil? || (mix == 'default' && result.workload.mix != :default)
133
+
134
+ result
135
+ end
136
+
137
+ # Load plans either from an explicit file or the name
138
+ # of something in the plans/ directory
139
+ def load_plans(plan_file, options)
140
+ if File.exist? plan_file
141
+ result = load_results(plan_file, options[:mix])
142
+ else
143
+ schema = Schema.load plan_file
144
+ result = OpenStruct.new
145
+ result.workload = Workload.new schema.model
146
+ result.indexes = schema.indexes.values
147
+ end
148
+ backend = get_backend(options, result)
149
+
150
+ [result, backend]
151
+ end
152
+
153
+ # Output a list of indexes as text
154
+ # @return [void]
155
+ def output_indexes_txt(header, indexes, file)
156
+ file.puts Formatador.parse("[blue]#{header}[/]")
157
+ indexes.sort_by(&:key).each { |index| file.puts index.inspect }
158
+ file.puts
159
+ end
160
+
161
+ # Output a list of query plans as text
162
+ # @return [void]
163
+ def output_plans_txt(plans, file, indent, weights)
164
+ plans.each do |plan|
165
+ weight = (plan.weight || weights[plan.query || plan.name])
166
+ next if weight.nil?
167
+ cost = plan.cost * weight
168
+
169
+ file.puts "GROUP #{plan.group}" unless plan.group.nil?
170
+
171
+ weight = " * #{weight} = #{cost}"
172
+ file.puts ' ' * (indent - 1) + plan.query.label \
173
+ unless plan.query.nil? || plan.query.label.nil?
174
+ file.puts ' ' * (indent - 1) + plan.query.inspect + weight
175
+ plan.each { |step| file.puts ' ' * indent + step.inspect }
176
+ file.puts
177
+ end
178
+ end
179
+
180
+ # Output update plans as text
181
+ # @return [void]
182
+ def output_update_plans_txt(update_plans, file, weights, mix = nil)
183
+ unless update_plans.empty?
184
+ header = "Update plans\n" + '━' * 50
185
+ file.puts Formatador.parse("[blue]#{header}[/]")
186
+ end
187
+
188
+ update_plans.group_by(&:statement).each do |statement, plans|
189
+ weight = if weights.key?(statement)
190
+ weights[statement]
191
+ elsif weights.key?(statement.group)
192
+ weights[statement.group]
193
+ else
194
+ weights[statement.group][mix]
195
+ end
196
+ next if weight.nil?
197
+
198
+ total_cost = plans.sum_by(&:cost)
199
+
200
+ file.puts "GROUP #{statement.group}" unless statement.group.nil?
201
+
202
+ file.puts statement.label unless statement.label.nil?
203
+ file.puts "#{statement.inspect} * #{weight} = #{total_cost * weight}"
204
+ plans.each do |plan|
205
+ file.puts Formatador.parse(" for [magenta]#{plan.index.key}[/] " \
206
+ "[yellow]$#{plan.cost}[/]")
207
+ query_weights = Hash[plan.query_plans.map do |query_plan|
208
+ [query_plan.query, weight]
209
+ end]
210
+ output_plans_txt plan.query_plans, file, 2, query_weights
211
+
212
+ plan.update_steps.each do |step|
213
+ file.puts ' ' + step.inspect
214
+ end
215
+
216
+ file.puts
217
+ end
218
+
219
+ file.puts "\n"
220
+ end
221
+ end
222
+
223
+ # Output the results of advising as text
224
+ # @return [void]
225
+ def output_txt(result, file = $stdout, enumerated = false,
226
+ _backend = nil)
227
+ if enumerated
228
+ header = "Enumerated indexes\n" + '━' * 50
229
+ output_indexes_txt header, result.enumerated_indexes, file
230
+ end
231
+
232
+ # Output selected indexes
233
+ header = "Indexes\n" + '━' * 50
234
+ output_indexes_txt header, result.indexes, file
235
+
236
+ file.puts Formatador.parse(' Total size: ' \
237
+ "[blue]#{result.total_size}[/]\n\n")
238
+
239
+ # Output query plans for the discovered indices
240
+ header = "Query plans\n" + '━' * 50
241
+ file.puts Formatador.parse("[blue]#{header}[/]")
242
+ weights = result.workload.statement_weights
243
+ weights = result.weights if weights.nil? || weights.empty?
244
+ output_plans_txt result.plans, file, 1, weights
245
+
246
+ result.update_plans = [] if result.update_plans.nil?
247
+ output_update_plans_txt result.update_plans, file, weights,
248
+ result.workload.mix
249
+
250
+ file.puts Formatador.parse(' Total cost: ' \
251
+ "[blue]#{result.total_cost}[/]\n")
252
+ end
253
+
254
+ # Output an HTML file with a description of the search results
255
+ # @return [void]
256
+ def output_html(result, file = $stdout, enumerated = false,
257
+ backend = nil)
258
+ # Get an SVG diagram of the model
259
+ tmpfile = Tempfile.new %w(model svg)
260
+ result.workload.model.output :svg, tmpfile.path, true
261
+ svg = File.open(tmpfile.path).read
262
+
263
+ enumerated &&= result.enumerated_indexes
264
+ tmpl = File.read File.join(File.dirname(__FILE__),
265
+ '../../templates/report.erb')
266
+ ns = OpenStruct.new svg: svg,
267
+ backend: backend,
268
+ indexes: result.indexes,
269
+ enumerated_indexes: enumerated,
270
+ workload: result.workload,
271
+ update_plans: result.update_plans,
272
+ plans: result.plans,
273
+ total_size: result.total_size,
274
+ total_cost: result.total_cost
275
+
276
+ force_colour
277
+ file.write ERB.new(tmpl, nil, '>').result(ns.instance_eval { binding })
278
+ end
279
+
280
+ # Output the results of advising as JSON
281
+ # @return [void]
282
+ def output_json(result, file = $stdout, enumerated = false,
283
+ _backend = nil)
284
+ # Temporarily remove the enumerated indexes
285
+ if enumerated
286
+ enumerated = result.enumerated_indexes
287
+ result.delete_field :enumerated_indexes
288
+ end
289
+
290
+ file.puts JSON.pretty_generate \
291
+ Serialize::SearchResultRepresenter.represent(result).to_hash
292
+
293
+ result.enumerated_indexes = enumerated if enumerated
294
+ end
295
+
296
+ # Output the results of advising as YAML
297
+ # @return [void]
298
+ def output_yml(result, file = $stdout, enumerated = false,
299
+ _backend = nil)
300
+ # Temporarily remove the enumerated indexes
301
+ if enumerated
302
+ enumerated = result.enumerated_indexes
303
+ result.delete_field :enumerated_indexes
304
+ end
305
+
306
+ file.puts Serialize::SearchResultRepresenter.represent(result).to_yaml
307
+
308
+ result.enumerated_indexes = enumerated if enumerated
309
+ end
310
+
311
+ # Filter an options hash for those only relevant to a given command
312
+ # @return [Thor::CoreExt::HashWithIndifferentAccess]
313
+ def filter_command_options(opts, command)
314
+ Thor::CoreExt::HashWithIndifferentAccess.new(opts.select do |key|
315
+ self.class.commands[command].options \
316
+ .each_key.map(&:to_sym).include? key.to_sym
317
+ end)
318
+ end
319
+
320
+ # Enable forcing the colour or no colour for output
321
+ # We just lie to Formatador about whether or not $stdout is a tty
322
+ # @return [void]
323
+ def force_colour(colour = true)
324
+ stdout_metaclass = class << $stdout; self; end
325
+ method = colour ? ->() { true } : ->() { false }
326
+ stdout_metaclass.send(:define_method, :tty?, &method)
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ require_relative 'nose_cli/shared_options'
333
+
334
+ # Require the various subcommands
335
+ require_relative 'nose_cli/analyze'
336
+ require_relative 'nose_cli/benchmark'
337
+ require_relative 'nose_cli/collect_results'
338
+ require_relative 'nose_cli/create'
339
+ require_relative 'nose_cli/diff_plans'
340
+ require_relative 'nose_cli/dump'
341
+ require_relative 'nose_cli/export'
342
+ require_relative 'nose_cli/execute'
343
+ require_relative 'nose_cli/load'
344
+ require_relative 'nose_cli/genworkload'
345
+ require_relative 'nose_cli/graph'
346
+ require_relative 'nose_cli/plan_schema'
347
+ require_relative 'nose_cli/proxy'
348
+ require_relative 'nose_cli/random_plans'
349
+ require_relative 'nose_cli/reformat'
350
+ require_relative 'nose_cli/repl'
351
+ require_relative 'nose_cli/recost'
352
+ require_relative 'nose_cli/search'
353
+ require_relative 'nose_cli/search_all'
354
+ require_relative 'nose_cli/search_bench'
355
+ require_relative 'nose_cli/texify'
356
+ require_relative 'nose_cli/why'
357
+
358
+ # Only include the console command if pry is available
359
+ begin
360
+ require 'pry'
361
+ require_relative 'nose_cli/console'
362
+ rescue LoadError
363
+ nil
364
+ end