1 | 1 | require 'csv' |
|
0 ignored issues
–
show
coding-style
introduced
by
Loading history...
|
|||
2 | 1 | require 'table_print' |
|
3 | |||
4 | 1 | module NoSE |
|
5 | 1 | module CLI |
|
6 | # Run performance tests on plans for a particular schema |
||
7 | 1 | class NoSECLI < Thor |
|
8 | 1 | desc 'benchmark PLAN_FILE', 'test performance of plans in PLAN_FILE' |
|
9 | |||
10 | 1 | long_desc <<-LONGDESC |
|
11 | `nose benchmark` will take a JSON file output by `nose search`, |
||
12 | execute each statement, and output a summary of the execution times. |
||
13 | Before runnng benchmark, `nose create` and `nose load` must be used to |
||
14 | prepare the target database. |
||
15 | LONGDESC |
||
16 | |||
17 | 1 | shared_option :mix |
|
18 | |||
19 | 1 | option :num_iterations, type: :numeric, default: 100, |
|
20 | banner: 'ITERATIONS', |
||
21 | desc: 'the number of times to execute each ' \ |
||
22 | 'statement' |
||
23 | 1 | option :repeat, type: :numeric, default: 1, |
|
24 | desc: 'how many times to repeat the benchmark' |
||
25 | 1 | option :group, type: :string, default: nil, aliases: '-g', |
|
26 | desc: 'restrict the benchmark to statements in the ' \ |
||
27 | 'given group' |
||
28 | 1 | option :fail_on_empty, type: :boolean, default: true, |
|
29 | desc: 'abort if a column family is empty' |
||
30 | 1 | option :totals, type: :boolean, default: false, aliases: '-t', |
|
31 | desc: 'whether to include group totals in the output' |
||
32 | 1 | option :format, type: :string, default: 'txt', |
|
33 | enum: %w(txt csv), aliases: '-f', |
||
34 | desc: 'the format of the output data' |
||
35 | |||
36 | 1 | def benchmark(plan_file) |
|
0 ignored issues
–
show
|
|||
37 | label = File.basename plan_file, '.*' |
||
38 | result = load_results plan_file, options[:mix] |
||
39 | |||
40 | backend = get_backend(options, result) |
||
41 | |||
42 | index_values = index_values result.indexes, backend, |
||
43 | options[:num_iterations], |
||
44 | options[:fail_on_empty] |
||
45 | |||
46 | group_tables = Hash.new { |h, k| h[k] = [] } |
||
47 | group_totals = Hash.new { |h, k| h[k] = 0 } |
||
48 | result.plans.each do |plan| |
||
49 | query = plan.query |
||
50 | weight = result.workload.statement_weights[query] |
||
51 | next if query.is_a?(SupportQuery) || !weight |
||
52 | @logger.debug { "Executing #{query.text}" } |
||
53 | |||
54 | next unless options[:group].nil? || plan.group == options[:group] |
||
55 | |||
56 | indexes = plan.select do |step| |
||
57 | step.is_a? Plans::IndexLookupPlanStep |
||
58 | end.map(&:index) |
||
59 | |||
60 | measurement = bench_query backend, indexes, plan, index_values, |
||
61 | options[:num_iterations], options[:repeat], |
||
62 | weight: weight |
||
63 | next if measurement.empty? |
||
64 | |||
65 | measurement.estimate = plan.cost |
||
66 | group_totals[plan.group] += measurement.mean |
||
67 | group_tables[plan.group] << measurement |
||
68 | end |
||
69 | |||
70 | result.workload.updates.each do |update| |
||
71 | weight = result.workload.statement_weights[update] |
||
72 | next unless weight |
||
73 | |||
74 | plans = (result.update_plans || []).select do |possible_plan| |
||
75 | possible_plan.statement == update |
||
76 | end |
||
77 | next if plans.empty? |
||
78 | |||
79 | @logger.debug { "Executing #{update.text}" } |
||
80 | |||
81 | plans.each do |plan| |
||
82 | next unless options[:group].nil? || plan.group == options[:group] |
||
83 | |||
84 | # Get all indexes used by support queries |
||
85 | indexes = plan.query_plans.flat_map(&:indexes) << plan.index |
||
86 | |||
87 | measurement = bench_update backend, indexes, plan, index_values, |
||
88 | options[:num_iterations], |
||
89 | options[:repeat], weight: weight |
||
90 | next if measurement.empty? |
||
91 | |||
92 | measurement.estimate = plan.cost |
||
93 | group_totals[plan.group] += measurement.mean |
||
94 | group_tables[plan.group] << measurement |
||
95 | end |
||
96 | end |
||
97 | |||
98 | total = 0 |
||
99 | table = [] |
||
100 | group_totals.each do |group, group_total| |
||
101 | total += group_total |
||
102 | total_measurement = Measurements::Measurement.new nil, 'TOTAL' |
||
103 | group_table = group_tables[group] |
||
104 | total_measurement << group_table.map(&:weighted_mean) \ |
||
105 | .inject(0, &:+) |
||
106 | group_table << total_measurement if options[:totals] |
||
107 | table << OpenStruct.new(label: label, group: group, |
||
108 | measurements: group_table) |
||
109 | end |
||
110 | |||
111 | if options[:totals] |
||
112 | total_measurement = Measurements::Measurement.new nil, 'TOTAL' |
||
113 | total_measurement << table.map do |group| |
||
114 | View Code Duplication | group.measurements.find { |m| m.name == 'TOTAL' }.mean |
|
0 ignored issues
–
show
|
|||
115 | end.inject(0, &:+) |
||
116 | table << OpenStruct.new(label: label, group: 'TOTAL', |
||
117 | measurements: [total_measurement]) |
||
118 | end |
||
119 | |||
120 | case options[:format] |
||
121 | when 'txt' |
||
122 | output_table table |
||
123 | else |
||
124 | output_csv table |
||
125 | end |
||
126 | end |
||
127 | |||
128 | 1 | private |
|
129 | |||
130 | # Get a sample of values from each index used by the queries |
||
131 | # @return [Hash] |
||
132 | 1 | def index_values(indexes, backend, iterations, fail_on_empty = true) |
|
133 | Hash[indexes.map do |index| |
||
134 | values = backend.index_sample(index, iterations).to_a |
||
135 | fail "Index #{index.key} is empty and will produce no results" \ |
||
136 | if values.empty? && fail_on_empty |
||
137 | |||
138 | [index, values] |
||
139 | end] |
||
140 | end |
||
141 | end |
||
142 | end |
||
143 | end |
||
144 |