nose-cli 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|