nose-cli 0.1.0pre
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 +7 -0
- data/bin/nose +26 -0
- data/bin/random_rubis +105 -0
- data/bin/restart-cassandra.sh +20 -0
- data/bin/run-experiments.sh +61 -0
- data/data/nose-cli/nose.yml.example +32 -0
- data/lib/nose_cli.rb +364 -0
- data/lib/nose_cli/analyze.rb +94 -0
- data/lib/nose_cli/benchmark.rb +145 -0
- data/lib/nose_cli/collect_results.rb +55 -0
- data/lib/nose_cli/console.rb +50 -0
- data/lib/nose_cli/create.rb +35 -0
- data/lib/nose_cli/diff_plans.rb +39 -0
- data/lib/nose_cli/dump.rb +67 -0
- data/lib/nose_cli/execute.rb +241 -0
- data/lib/nose_cli/export.rb +39 -0
- data/lib/nose_cli/genworkload.rb +24 -0
- data/lib/nose_cli/graph.rb +24 -0
- data/lib/nose_cli/load.rb +44 -0
- data/lib/nose_cli/measurements.rb +36 -0
- data/lib/nose_cli/plan_schema.rb +84 -0
- data/lib/nose_cli/proxy.rb +32 -0
- data/lib/nose_cli/random_plans.rb +82 -0
- data/lib/nose_cli/recost.rb +45 -0
- data/lib/nose_cli/reformat.rb +22 -0
- data/lib/nose_cli/repl.rb +144 -0
- data/lib/nose_cli/search.rb +77 -0
- data/lib/nose_cli/search_all.rb +120 -0
- data/lib/nose_cli/search_bench.rb +52 -0
- data/lib/nose_cli/shared_options.rb +30 -0
- data/lib/nose_cli/texify.rb +141 -0
- data/lib/nose_cli/why.rb +70 -0
- data/templates/completions.erb +56 -0
- data/templates/man.erb +33 -0
- data/templates/report.erb +138 -0
- data/templates/subman.erb +19 -0
- metadata +345 -0
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
|