bigbench 0.0.3 → 0.0.4
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.
- data/.DS_Store +0 -0
- data/README.textile +254 -4
- data/Rakefile +43 -9
- data/doc/Array.html +288 -0
- data/doc/BigBench.html +32 -6
- data/doc/BigBench/Benchmark.html +24 -0
- data/doc/BigBench/Benchmark/Benchmark.html +24 -0
- data/doc/BigBench/Benchmark/Looper.html +24 -0
- data/doc/BigBench/Bot.html +24 -0
- data/doc/BigBench/Configuration.html +24 -0
- data/doc/BigBench/Configuration/Config.html +24 -0
- data/doc/BigBench/Configuration/InvalidOptions.html +24 -0
- data/doc/BigBench/Executor.html +33 -3
- data/doc/BigBench/Executor/InvalidCommand.html +25 -1
- data/doc/BigBench/Fragment.html +24 -0
- data/doc/BigBench/Fragment/Fragment.html +24 -0
- data/doc/BigBench/Output.html +24 -0
- data/doc/BigBench/PostProcessor.html +33 -6
- data/doc/BigBench/PostProcessor/Environment.html +489 -2
- data/doc/BigBench/PostProcessor/Environment/Appearings.html +327 -0
- data/doc/BigBench/PostProcessor/Environment/AttributeCluster.html +275 -0
- data/doc/BigBench/PostProcessor/Environment/BenchmarkNotFound.html +293 -0
- data/doc/BigBench/PostProcessor/Environment/Cluster.html +387 -0
- data/doc/BigBench/PostProcessor/Environment/NormalDistribution.html +383 -0
- data/doc/BigBench/PostProcessor/Environment/PolynomialRegression.html +438 -0
- data/doc/BigBench/PostProcessor/Environment/Statistics.html +568 -0
- data/doc/BigBench/PostProcessor/Graphs.html +270 -0
- data/doc/BigBench/PostProcessor/Graphs/LineGraph.html +403 -0
- data/doc/BigBench/PostProcessor/Graphs/PieGraph.html +396 -0
- data/doc/BigBench/PostProcessor/InvalidProcessor.html +25 -1
- data/doc/BigBench/PostProcessor/Processor.html +59 -7
- data/doc/BigBench/PostProcessor/Statistics.html +26 -2
- data/doc/BigBench/PostProcessor/Test.html +26 -2
- data/doc/BigBench/Runner.html +24 -0
- data/doc/BigBench/Runner/NoBenchmarksDefined.html +24 -0
- data/doc/BigBench/Store.html +24 -0
- data/doc/BigBench/Tracker.html +24 -0
- data/doc/BigBench/Tracker/Tracker.html +24 -0
- data/doc/EventMachineLoop.html +24 -0
- data/doc/Float.html +24 -0
- data/doc/Gemfile.html +24 -0
- data/doc/Helpers.html +78 -0
- data/doc/Object.html +29 -0
- data/doc/README_rdoc.html +803 -0
- data/doc/Rakefile.html +66 -10
- data/doc/created.rid +46 -40
- data/doc/index.html +667 -1
- data/doc/js/search_index.js +1 -1
- data/doc/lib/bigbench/help/executor_txt.html +32 -2
- data/doc/rdoc.css +4 -0
- data/doc/table_of_contents.html +179 -23
- data/doc/test_rdoc.html +159 -0
- data/lib/bigbench.rb +2 -0
- data/lib/bigbench/executor.rb +17 -1
- data/lib/bigbench/help/executor.txt +5 -0
- data/lib/bigbench/post_processor.rb +16 -32
- data/lib/bigbench/post_processor/environment.rb +525 -0
- data/lib/bigbench/post_processor/graphs.rb +209 -0
- data/lib/bigbench/post_processor/statistics.rb +29 -49
- data/lib/bigbench/version.rb +1 -1
- data/spec/executor_spec.rb +35 -0
- data/spec/helpers.rb +15 -1
- data/spec/post_processor_spec.rb +19 -4
- data/spec/post_processors/environment_spec.rb +412 -0
- data/spec/post_processors/graphs_spec.rb +23 -0
- data/spec/post_processors/statistics_spec.rb +3 -2
- data/spec/tests/local.rb +1 -1
- data/spec/tests/sample_results_big.ljson +51925 -0
- data/spec/tests/sample_results_small.ljson +3875 -0
- data/spec/tests/with_post_processor.ljson +43 -0
- data/spec/tests/with_post_processor.rb +12 -0
- data/spec/tmp/.DS_Store +0 -0
- data/spec/tracker_spec.rb +8 -8
- metadata +61 -101
data/lib/bigbench.rb
CHANGED
@@ -5,6 +5,7 @@ require 'redis'
|
|
5
5
|
require 'eventmachine'
|
6
6
|
require 'em-http'
|
7
7
|
require 'hirb'
|
8
|
+
require 'matrix'
|
8
9
|
|
9
10
|
require "bigbench/float_extensions"
|
10
11
|
require "bigbench/version"
|
@@ -18,6 +19,7 @@ require "bigbench/executor"
|
|
18
19
|
require "bigbench/store"
|
19
20
|
require "bigbench/bot"
|
20
21
|
require "bigbench/output"
|
22
|
+
require "bigbench/post_processor/environment"
|
21
23
|
require "bigbench/post_processor"
|
22
24
|
require "bigbench/post_processor/statistics"
|
23
25
|
|
data/lib/bigbench/executor.rb
CHANGED
@@ -10,7 +10,9 @@ module BigBench
|
|
10
10
|
"run local PATH_TO_TEST",
|
11
11
|
"run bots PATH_TO_TEST [REDIS_URL_WITH_PORT REDIS_PASSWORD]",
|
12
12
|
"start bot [REDIS_URL_WITH_PORT REDIS_PASSWORD]",
|
13
|
-
"reset all [REDIS_URL_WITH_PORT REDIS_PASSWORD]"
|
13
|
+
"reset all [REDIS_URL_WITH_PORT REDIS_PASSWORD]",
|
14
|
+
"run postprocessors PATH_TO_TEST",
|
15
|
+
"run postprocessor PATH_TO_TEST POSTPROCESSOR"
|
14
16
|
]
|
15
17
|
|
16
18
|
# Is thrown when the command is not known
|
@@ -129,5 +131,19 @@ module BigBench
|
|
129
131
|
BigBench::Output.reset
|
130
132
|
end
|
131
133
|
|
134
|
+
# Re-runs all receipt defined post processors
|
135
|
+
def self.run_postprocessors(argv)
|
136
|
+
BigBench.load_test! File.open(argv[2], "rb"){ |file| file.read }
|
137
|
+
BigBench.run_post_processors!
|
138
|
+
end
|
139
|
+
|
140
|
+
# Runs a new postprocessor for receipt
|
141
|
+
def self.run_postprocessor(argv)
|
142
|
+
BigBench.load_test! File.open(argv[2], "rb"){ |file| file.read }
|
143
|
+
BigBench::PostProcessor.reset!
|
144
|
+
BigBench.post_process(argv[3])
|
145
|
+
BigBench.run_post_processors!
|
146
|
+
end
|
147
|
+
|
132
148
|
end
|
133
149
|
end
|
@@ -8,6 +8,10 @@ Usage:
|
|
8
8
|
|
9
9
|
# Reseting
|
10
10
|
bigbench reset all # Resets everything
|
11
|
+
|
12
|
+
# Post Processors
|
13
|
+
bigbench run postprocessors PATH_TO_TEST # Runs all post processors of the test without running the test itself
|
14
|
+
bigbench run postprocessor PATH_TO_TEST POSTPROCESSOR # Runs the specified post processor on the output file
|
11
15
|
|
12
16
|
# Help
|
13
17
|
bigbench --help || show help # Displays this help
|
@@ -15,3 +19,4 @@ Usage:
|
|
15
19
|
Options:
|
16
20
|
REDIS_URL_WITH_PORT # Defaults to http://localhost:6379
|
17
21
|
REDIS_PASSWORD # Defaults to no password
|
22
|
+
POSTPROCESSOR # A post processor module or its short name e.g. 'statistics'
|
@@ -60,16 +60,22 @@ module BigBench
|
|
60
60
|
|
61
61
|
# A single processor, be it a block or a module is always mapped into this class
|
62
62
|
class Processor
|
63
|
+
include ActionView::Helpers
|
64
|
+
|
63
65
|
attr_accessor :proc
|
66
|
+
attr_accessor :runs
|
67
|
+
attr_accessor :options
|
64
68
|
|
65
69
|
# Creates the processor with the block or a processor
|
66
|
-
def initialize(processor = nil, &block)
|
70
|
+
def initialize(processor = nil, options = {}, &block)
|
67
71
|
@proc = processor_to_proc(processor) || block
|
72
|
+
@runs, @options = 0, options
|
68
73
|
end
|
69
74
|
|
70
75
|
# Run the block of code or the run! method of the processor
|
71
76
|
def run!
|
72
|
-
|
77
|
+
@runs += 1
|
78
|
+
PostProcessor::Environment.module_exec(@options, &@proc)
|
73
79
|
end
|
74
80
|
|
75
81
|
private
|
@@ -88,13 +94,15 @@ module BigBench
|
|
88
94
|
|
89
95
|
def processor_to_module(processor)
|
90
96
|
return processor if processor.is_a? Module
|
91
|
-
"BigBench::PostProcessor::#{processor.to_s.camelize}"
|
97
|
+
mod = "BigBench::PostProcessor::#{processor.to_s.camelize}"
|
98
|
+
require mod.underscore.sub('big_bench', 'bigbench')
|
99
|
+
mod.constantize
|
92
100
|
end
|
93
101
|
end
|
94
102
|
|
95
103
|
# Adds a new processor
|
96
|
-
def self.add(processor = nil, &block)
|
97
|
-
@processors << Processor.new(processor, &block)
|
104
|
+
def self.add(processor = nil, options = nil, &block)
|
105
|
+
@processors << Processor.new(processor, options, &block)
|
98
106
|
end
|
99
107
|
|
100
108
|
# Returns all initialized processors
|
@@ -105,6 +113,7 @@ module BigBench
|
|
105
113
|
# Resets all post processors
|
106
114
|
def self.reset!
|
107
115
|
@processors = []
|
116
|
+
Environment.reset!
|
108
117
|
end
|
109
118
|
|
110
119
|
# Thrown if a processor wasn't initialized the right way
|
@@ -114,31 +123,6 @@ module BigBench
|
|
114
123
|
end
|
115
124
|
end
|
116
125
|
|
117
|
-
# The environment in which the post processors are evaluated. Every method defined here is available in
|
118
|
-
# the post_process block and run! methods of the predefined post processors
|
119
|
-
module Environment
|
120
|
-
|
121
|
-
# Iterates through every tracking and returns a tracking hash of the following form:
|
122
|
-
#
|
123
|
-
# {
|
124
|
-
# :elapsed => 2.502132,
|
125
|
-
# :start => 1333986292.1755981,
|
126
|
-
# :stop => 1333986293.618884,
|
127
|
-
# :duration => 1443,
|
128
|
-
# :benchmark => "index page",
|
129
|
-
# :url => "http://www.google.de/",
|
130
|
-
# :path => "/",
|
131
|
-
# :method => "get",
|
132
|
-
# :status => 200
|
133
|
-
# }
|
134
|
-
#
|
135
|
-
def each_tracking
|
136
|
-
File.open(BigBench.config.output, "r+") do |file|
|
137
|
-
file.each_line { |line| yield JSON.parse(line).inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} unless line.blank? }
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
126
|
end
|
143
127
|
|
144
128
|
# To setup a post processor simply do this:
|
@@ -189,9 +173,9 @@ module BigBench
|
|
189
173
|
#
|
190
174
|
# end
|
191
175
|
#
|
192
|
-
def self.post_process processor = nil
|
176
|
+
def self.post_process processor = nil, options = {}
|
193
177
|
raise PostProcessor::InvalidProcessor.new if processor.nil? && !block_given?
|
194
|
-
block_given? ? PostProcessor.add(&Proc.new) : PostProcessor.add(processor)
|
178
|
+
block_given? ? PostProcessor.add(processor, options, &Proc.new) : PostProcessor.add(processor, options)
|
195
179
|
end
|
196
180
|
|
197
181
|
# List all initialized post processors
|
@@ -0,0 +1,525 @@
|
|
1
|
+
module BigBench
|
2
|
+
module PostProcessor
|
3
|
+
|
4
|
+
# The environment in which the post processors are evaluated. Every method defined here is available in
|
5
|
+
# the post_process block and run! methods of the predefined post processors
|
6
|
+
module Environment
|
7
|
+
|
8
|
+
# Resets the whole post processor environment
|
9
|
+
def self.reset!
|
10
|
+
@@clusters, @@statistics, @@normal_distribution, @@regressions, @@appearings = {}, {}, {}, {}, {}
|
11
|
+
@@trackings = []
|
12
|
+
@@scope = :all
|
13
|
+
end
|
14
|
+
reset!
|
15
|
+
|
16
|
+
# Adding the sum and average methods to the default array
|
17
|
+
class ::Array
|
18
|
+
def sum
|
19
|
+
reduce(:+).to_f
|
20
|
+
end
|
21
|
+
|
22
|
+
def average
|
23
|
+
result = sum / size.to_f
|
24
|
+
result.nan? ? 0 : result
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Is raised when a benchmark scope block doesn't find the desired benchmark
|
29
|
+
class BenchmarkNotFound < StandardError
|
30
|
+
|
31
|
+
def initialize(unexistant_benchmark)
|
32
|
+
@unexistant_benchmark = unexistant_benchmark
|
33
|
+
end
|
34
|
+
|
35
|
+
def message
|
36
|
+
"Could not find Benchmark: '#{@unexistant_benchmark}'. Available benchmarks are: #{BigBench.benchmarks.map{ |b| b.name }.join(', ')}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterates through every tracking and returns a tracking hash of the following form:
|
41
|
+
#
|
42
|
+
# {
|
43
|
+
# :elapsed => 2.502132,
|
44
|
+
# :start => 1333986292.1755981,
|
45
|
+
# :stop => 1333986293.618884,
|
46
|
+
# :duration => 1443,
|
47
|
+
# :benchmark => "index page",
|
48
|
+
# :url => "http://www.google.de/",
|
49
|
+
# :path => "/",
|
50
|
+
# :method => "get",
|
51
|
+
# :status => 200
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
def each_tracking
|
55
|
+
File.open(BigBench.config.output, "r+") do |file|
|
56
|
+
file.each_line { |line| yield JSON.parse(line).inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} unless line.blank? }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Puts all tracking hashes into a huge array. Warning, this method call might take quite long!
|
61
|
+
# The results are cached, so you can call <tt>trackings</tt> in the future without any pain
|
62
|
+
def trackings
|
63
|
+
return @@trackings unless @@trackings.empty?
|
64
|
+
each_tracking{ |tracking| @@trackings << tracking }
|
65
|
+
@@trackings
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the current scope the environment works in
|
69
|
+
def scope
|
70
|
+
@@scope
|
71
|
+
end
|
72
|
+
|
73
|
+
# Executes the including methods in the scope of the benchmark:
|
74
|
+
#
|
75
|
+
# # For the "index page" benchmark
|
76
|
+
# scope_to_benchmark "index page" do
|
77
|
+
# cluster.durations
|
78
|
+
# cluster.requests
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
def scope_to_benchmark name
|
82
|
+
raise BenchmarkNotFound.new(name) unless BigBench.benchmarks.map{|b| b.name }.include?(name)
|
83
|
+
@@scope = name
|
84
|
+
yield
|
85
|
+
@@scope = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Iterates over all benchmarks and automatically executes all methods in the benchmark scope like this:
|
89
|
+
#
|
90
|
+
# # For all benchmarks
|
91
|
+
# cluster.durations
|
92
|
+
# cluster.requests
|
93
|
+
#
|
94
|
+
# # For each benchmark
|
95
|
+
# each_benchmark do |benchmark|
|
96
|
+
# cluster.durations
|
97
|
+
# cluster.requests
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
def each_benchmark
|
101
|
+
BigBench.benchmarks.each do |benchmark|
|
102
|
+
scope_to_benchmark(benchmark.name) do
|
103
|
+
yield benchmark
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns a clustered overview of all trackings. By default the trackings are clustered by second, but you
|
109
|
+
# can also specify any ammount of seconds to group together. A cluster then has the following methods:
|
110
|
+
#
|
111
|
+
# # Duration was 120 seconds
|
112
|
+
# cluster.timesteps # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,..., 120] (seconds)
|
113
|
+
# cluster.durations # => [50.3, 51.2, 40.3, 51.3, 50.3, 55.3, 52.3, 50.3, 51.3, 50.3, 54.3,..., 50.3] (average duration in milliseconds)
|
114
|
+
# cluster.requests # => [580, 569, 540, 524, 524, 525, 528, 520, 529, 527, 523,..., 524] (requests in that second)
|
115
|
+
# cluster.methods(:get) # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (GET requests in that second)
|
116
|
+
# cluster.methods(:post) # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (POST requests in that second)
|
117
|
+
# cluster.statuses(200) # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (successful - requests in that second)
|
118
|
+
# cluster.statuses(404) # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (not founds - requests in that second)
|
119
|
+
# cluster.paths("/") # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (requests to a path in that second)
|
120
|
+
# cluster.paths("/home") # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (requests to "/home" path in that second)
|
121
|
+
# cluster.benchmark("index") # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (requests from the index benchmark in that second)
|
122
|
+
# cluster.benchmark("user") # => [400, 509, 340, 424, 324, 525, 528, 520, 529, 527, 523,..., 524] (requests from the user benchmark in that second)
|
123
|
+
#
|
124
|
+
# # Duration was 120 seconds = 2 minutes
|
125
|
+
# cluster(1.minute).timesteps # => [0, 1] (minutes)
|
126
|
+
# cluster(1.minute).durations # => [50.3, 51.2] (average duration in milliseconds)
|
127
|
+
# cluster(1.minute).requests # => [27836, 27684] (requests in that minute)
|
128
|
+
#
|
129
|
+
def cluster(timebase = 1.second, extra_scope = nil)
|
130
|
+
cluster_scope = extra_scope || scope
|
131
|
+
timebase_and_scope = [timebase.to_i, cluster_scope]
|
132
|
+
|
133
|
+
return @@clusters[timebase_and_scope] unless @@clusters[timebase_and_scope].nil?
|
134
|
+
@@clusters[timebase_and_scope] = Cluster.new(timebase, cluster_scope)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns an array of appearing attributes in the selected tracking scope.
|
138
|
+
#
|
139
|
+
# appearing.statuses # => [200, 404]
|
140
|
+
# appearing.methods # => ["get", "post"]
|
141
|
+
# appearing.paths # => ["/", "/basic/auth"
|
142
|
+
#
|
143
|
+
def appearing(timebase = 1.second)
|
144
|
+
return @@appearings[scope] unless @@appearings[scope].nil?
|
145
|
+
|
146
|
+
@@appearings[scope] ||= Appearings.new(scope)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns the default statistics for a given attribute. The following statistics are available:
|
150
|
+
#
|
151
|
+
# statistics.durations.max # => 78.2
|
152
|
+
# statistics.durations.min # => 12.3
|
153
|
+
#
|
154
|
+
# statistics.durations.mean # => 45.2
|
155
|
+
# statistics.durations.average # => 45.2
|
156
|
+
#
|
157
|
+
# statistics.durations.standard_deviation # => 11.3
|
158
|
+
# statistics.durations.sd # => 11.3
|
159
|
+
#
|
160
|
+
# statistics.durations.squared_deviation # => 60.7
|
161
|
+
# statistics.durations.variance # => 60.7
|
162
|
+
#
|
163
|
+
def statistics(timebase = 1.second)
|
164
|
+
timebase_and_scope = [timebase.to_i, scope]
|
165
|
+
return @@statistics[timebase_and_scope] unless @@statistics[timebase_and_scope].nil?
|
166
|
+
|
167
|
+
@@statistics[timebase_and_scope] ||= AttributeCluster.new(Statistics, :timebase => timebase, :scope => scope)
|
168
|
+
|
169
|
+
# Duration is the only value that shouldn't be clustered in the statistics because we have real float values in it, and do not only count +1
|
170
|
+
# for the request. Because of this, we'll exchange the clustered durations y values with an array of all unclustered tracking durations
|
171
|
+
@@statistics[timebase_and_scope].instance_eval do
|
172
|
+
@durations.instance_eval do
|
173
|
+
@y = trackings.map{ |tracking| tracking[:duration] if tracking[:benchmark] == scope or scope == :all }.compact
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
@@statistics[timebase_and_scope]
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns a gaussian distribution for the specified attribute. It automatically calculates the neccessary mean and variance values and adapts
|
181
|
+
# the x values to fit the bell curve best.
|
182
|
+
#
|
183
|
+
# normal_distribution.durations.x # => [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, ...]
|
184
|
+
# normal_distribution.durations.y # => [0, 0, 7, 8, 9, 10, 11, 10, 9, 8, 7, 4, ...]
|
185
|
+
#
|
186
|
+
def normal_distribution(timebase = 1.second)
|
187
|
+
timebase_and_scope = [timebase.to_i, scope]
|
188
|
+
return @@normal_distribution[timebase_and_scope] unless @@normal_distribution[timebase_and_scope].nil?
|
189
|
+
|
190
|
+
@@normal_distribution[timebase_and_scope] ||= AttributeCluster.new(NormalDistribution, :timebase => timebase, :scope => scope)
|
191
|
+
|
192
|
+
# Duration is the only value that shouldn't be clustered in the statistics because we have real float values in it, and do not only count +1
|
193
|
+
# for the request. Because of this, we'll exchange the clustered durations y values with an array of all unclustered tracking durations
|
194
|
+
@@normal_distribution[timebase_and_scope].instance_eval do
|
195
|
+
@durations.instance_eval do
|
196
|
+
@y = trackings.map{ |tracking| tracking[:duration] if tracking[:benchmark] == scope or scope == :all }.compact
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
@@normal_distribution[timebase_and_scope]
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns a polynomial regression of a degree, a derivation and a timebase. Possible options are:
|
204
|
+
#
|
205
|
+
# [:degree] By default the degree is 1 which results in a linear regression. There's no limit to the degree.
|
206
|
+
# [:derivation] By default the normal function, which means no derivation is returned. Currently only the first derivation is supported.
|
207
|
+
# [:timebase] By default the cluster size is 1.second. Any timelimit can be added here, e.g. 1.minute
|
208
|
+
#
|
209
|
+
#
|
210
|
+
# # Return a linear regression for the durations, clustered by seconds
|
211
|
+
# polynomial_regression.durations.y
|
212
|
+
# polynomial_regression(:degree => 1, :timebase => 1.second).durations.y
|
213
|
+
# polynomial_regression(:degree => 1, :timebase => 1.second).durations.derivation(0)
|
214
|
+
#
|
215
|
+
# # Return the first derivation of the linear regression for the durations, clustered by seconds
|
216
|
+
# polynomial_regression(:degree => 1).durations.derivation(1)
|
217
|
+
# polynomial_regression(:degree => 1, :timebase => 1.second).durations.derivation(1)
|
218
|
+
#
|
219
|
+
# # Return a second degree polynomial regression for the durations, clustered by seconds
|
220
|
+
# polynomial_regression(:degree => 2).durations.derivation(0)
|
221
|
+
# polynomial_regression(:degree => 2, :timebase => 1.second).durations.derivation(0)
|
222
|
+
#
|
223
|
+
# # Return the first derivation of the second degree polynomial regression for the durations, clustered by seconds
|
224
|
+
# polynomial_regression(:degree => 2).durations.derivation(0)
|
225
|
+
# polynomial_regression(:degree => 2, :timebase => 1.second).durations.derivation(0)
|
226
|
+
#
|
227
|
+
def polynomial_regression(new_options = {})
|
228
|
+
options = { :degree => 1, :timebase => 1.second }.merge(new_options)
|
229
|
+
degree_and_timebase_and_scope = [options[:degree], options[:timebase].to_i, scope]
|
230
|
+
|
231
|
+
return @@regressions[degree_and_timebase_and_scope] unless @@regressions[degree_and_timebase_and_scope].nil?
|
232
|
+
|
233
|
+
@@regressions[degree_and_timebase_and_scope] ||= AttributeCluster.new(PolynomialRegression, :timebase => options[:timebase], :scope => scope, :degree => options[:degree])
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
# Calculates the default statistic values for a given attribute, like the duration, requests, etc.
|
238
|
+
class Statistics
|
239
|
+
include Environment
|
240
|
+
|
241
|
+
attr_reader :x
|
242
|
+
attr_reader :y
|
243
|
+
|
244
|
+
def initialize x, y, degree
|
245
|
+
@x, @y = x, y
|
246
|
+
end
|
247
|
+
|
248
|
+
# The maximum of the attribute
|
249
|
+
def max
|
250
|
+
@y.max
|
251
|
+
end
|
252
|
+
|
253
|
+
# The minimum of the attribute
|
254
|
+
def min
|
255
|
+
@y.min
|
256
|
+
end
|
257
|
+
|
258
|
+
# The mean or average of the attribute
|
259
|
+
def mean
|
260
|
+
@y.average
|
261
|
+
end
|
262
|
+
alias_method :average, :mean
|
263
|
+
|
264
|
+
# The standard deviation or sd of the attribute
|
265
|
+
def standard_deviation
|
266
|
+
u = mean
|
267
|
+
@y.inject(0){ |result, element| result + (element - u).abs }.to_f / @y.size.to_f
|
268
|
+
end
|
269
|
+
alias_method :sd, :standard_deviation
|
270
|
+
|
271
|
+
# The squared deviation or variance of the attribute
|
272
|
+
def squared_deviation
|
273
|
+
u = mean
|
274
|
+
@y.inject(0){ |result, element| result + (element - u) ** 2 }.to_f / @y.size.to_f
|
275
|
+
end
|
276
|
+
alias_method :variance, :squared_deviation
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# Calculates a gaussion normal distribution with the mean a variance of the supplied y values
|
281
|
+
class NormalDistribution
|
282
|
+
include Environment
|
283
|
+
|
284
|
+
attr_reader :mean
|
285
|
+
attr_reader :sd
|
286
|
+
attr_reader :x
|
287
|
+
attr_reader :formula
|
288
|
+
|
289
|
+
def initialize x, y, degree
|
290
|
+
@mean, @x = y.average, x
|
291
|
+
@sd = y.inject(0){ |result, element| result + (element - @mean).abs }.to_f / y.size.to_f
|
292
|
+
|
293
|
+
# Setup functions that map the guassian normal distribution
|
294
|
+
@distribution = lambda { |x| 1 / Math.sqrt(2 * Math::PI * @sd) * Math::E**( -0.5 * (x - @mean)**2 / @sd) }
|
295
|
+
|
296
|
+
# Setup x to match the center mean
|
297
|
+
@x = []
|
298
|
+
upper_limit = @mean + (6 * @sd)
|
299
|
+
lower_limit = @mean - (6 * @sd)
|
300
|
+
delta_limit = upper_limit - lower_limit
|
301
|
+
steps = 120
|
302
|
+
step_size = delta_limit / steps
|
303
|
+
steps.times{ |step| @x << (step * step_size) + lower_limit }
|
304
|
+
|
305
|
+
# Store formula for printing
|
306
|
+
@formula = "1 / sqrt(2 * pi * #{@sd}) * e**( -0.5 * (x - #{@mean})**2 / #{@sd})"
|
307
|
+
end
|
308
|
+
|
309
|
+
# Returns an array with the distribution values like this:
|
310
|
+
#
|
311
|
+
# [0.0, 0.1, 1.2, 4.5, 10.8, 4.5, 1.2, 0.1, 0.0]
|
312
|
+
#
|
313
|
+
def y
|
314
|
+
@x.map{ |x| @distribution.call(x).round(3) }
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
318
|
+
|
319
|
+
# Clusters the trackings in the specified timebase. By default everything is clustered by seconds.
|
320
|
+
class Cluster
|
321
|
+
include Environment
|
322
|
+
|
323
|
+
# Allows the registering of multi dimensioned attributes and 0s out values that aren't present
|
324
|
+
#
|
325
|
+
# attr_multi_dimension_reader :methods
|
326
|
+
#
|
327
|
+
def self.attr_multi_dimension_reader(attribute)
|
328
|
+
define_method(attribute) do |appearance|
|
329
|
+
variable = instance_variable_get("@#{attribute}".to_sym)
|
330
|
+
variable.key?(appearance.to_s) ? variable[appearance.to_s] : timesteps.dup.fill(0)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
attr_reader :timesteps
|
335
|
+
attr_reader :durations
|
336
|
+
attr_reader :requests
|
337
|
+
attr_multi_dimension_reader :methods
|
338
|
+
attr_multi_dimension_reader :statuses
|
339
|
+
attr_multi_dimension_reader :paths
|
340
|
+
|
341
|
+
def initialize(timebase = 1.second, scope = :all)
|
342
|
+
@timesteps, @durations, @durations_array, @requests, @methods, @statuses, @paths, @scope = [], [], [], [], {}, {}, {}, scope
|
343
|
+
|
344
|
+
# Single dimension attributes
|
345
|
+
steps = BigBench.config.duration.to_i / timebase
|
346
|
+
(0..steps).to_a.each do |timestep|
|
347
|
+
@timesteps[timestep] = timestep
|
348
|
+
@durations_array[timestep] = []
|
349
|
+
@requests[timestep] = 0
|
350
|
+
end
|
351
|
+
|
352
|
+
# Multi dimension attributes
|
353
|
+
[:methods, :statuses, :paths].each do |attribute|
|
354
|
+
appearing.send(attribute).each do |appearance|
|
355
|
+
variable = instance_variable_get("@#{attribute}".to_sym)
|
356
|
+
variable[appearance.to_s] = []
|
357
|
+
@timesteps.each { |timestep| variable[appearance.to_s][timestep] = 0 }
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# Cluster trackings
|
362
|
+
trackings.each do |tracking|
|
363
|
+
next if !(tracking[:benchmark] == scope or scope == :all)
|
364
|
+
|
365
|
+
timestep = tracking[:elapsed].to_i / timebase
|
366
|
+
|
367
|
+
@durations_array[timestep] << tracking[:duration]
|
368
|
+
@requests[timestep] += 1
|
369
|
+
@methods[tracking[:method].to_s][timestep] += 1
|
370
|
+
@statuses[tracking[:status].to_s][timestep] += 1
|
371
|
+
@paths[tracking[:path].to_s][timestep] += 1
|
372
|
+
end
|
373
|
+
|
374
|
+
# Compute mean of durations
|
375
|
+
@timesteps.each do |timestep|
|
376
|
+
@durations[timestep] = @durations_array[timestep].average
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
end
|
381
|
+
|
382
|
+
# Lists the appearing attributes for a scope
|
383
|
+
class Appearings
|
384
|
+
include Environment
|
385
|
+
|
386
|
+
attr_reader :methods
|
387
|
+
attr_reader :statuses
|
388
|
+
attr_reader :paths
|
389
|
+
|
390
|
+
def initialize scope = :all
|
391
|
+
@methods, @statuses, @paths = [], [], []
|
392
|
+
|
393
|
+
trackings.each do |tracking|
|
394
|
+
next if !(tracking[:benchmark] == scope or scope == :all)
|
395
|
+
|
396
|
+
# Add appearing attributes
|
397
|
+
@methods << tracking[:method]
|
398
|
+
@statuses << tracking[:status]
|
399
|
+
@paths << tracking[:path]
|
400
|
+
end
|
401
|
+
|
402
|
+
# Unique attributes
|
403
|
+
@methods.uniq!
|
404
|
+
@statuses.uniq!
|
405
|
+
@paths.uniq!
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
409
|
+
|
410
|
+
|
411
|
+
# This class performs the actual regression for a specfied degree and timebase. As x it returns the timebase
|
412
|
+
# values for the corresponding timebase - e.g. the seconds - and as y it returns the corresponding regression
|
413
|
+
# values. Additionally it offers the derivations for the regressions with the <tt>deviation</tt> method.
|
414
|
+
class PolynomialRegression
|
415
|
+
|
416
|
+
# An array with the seconds in the timebase
|
417
|
+
#
|
418
|
+
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
419
|
+
#
|
420
|
+
attr_reader :x
|
421
|
+
attr_reader :degree
|
422
|
+
|
423
|
+
# Returns the coefficients calculated for the regression and the degree like
|
424
|
+
#
|
425
|
+
# [3.428734, 1.176235]
|
426
|
+
#
|
427
|
+
attr_reader :coefficients
|
428
|
+
|
429
|
+
def initialize x, y, degree
|
430
|
+
@x, @degree, @derivations, @formulas = x, degree, [], []
|
431
|
+
raise "Regression is not possible for a single time value, choose a smaller timebase" if @x.size <= 1
|
432
|
+
|
433
|
+
# Perform regression
|
434
|
+
x_data = x.map { |xi| (0..degree).map { |pow| (xi**pow).to_f } }
|
435
|
+
mx = ::Matrix[*x_data]
|
436
|
+
my = ::Matrix.column_vector(y)
|
437
|
+
|
438
|
+
|
439
|
+
# Calculate coefficients
|
440
|
+
@coefficients = ((mx.t * mx).inv * mx.t * my).transpose.to_a[0]
|
441
|
+
|
442
|
+
# Setup functions that map the actual polynom
|
443
|
+
@derivations << lambda { |x| (0..@degree).to_a.inject(0) { |result, d| result + (@coefficients[d] * x**d) }}
|
444
|
+
@derivations << lambda { |x| (1..@degree).to_a.inject(0) { |result, d| result + (d * (@coefficients[d] * x**(d-1))) }}
|
445
|
+
|
446
|
+
# Store formulas for printing
|
447
|
+
@formulas << (0..@degree).to_a.map { |d| d == 0 ? @coefficients[d] : "#{@coefficients[d]}x^#{d}" }
|
448
|
+
@formulas << (1..@degree).to_a.map { |d| d == 1 ? @coefficients[d] : "#{d}*#{@coefficients[d]}x^#{d-1}" }
|
449
|
+
end
|
450
|
+
|
451
|
+
# Returns an array with the computed y-values for the corresponding derivation. The default derivation is the
|
452
|
+
# 0. derivation which is the original regression that is equal to the <tt>y</tt> method. The result looks like this:
|
453
|
+
#
|
454
|
+
# [2.5, 2.6, 2.7, 2.8, 3.0, 3.2, 3.4, 3.8, 4.0, 4.9]
|
455
|
+
#
|
456
|
+
def derivation(derivation = 0)
|
457
|
+
@x.map{ |x| @derivations[derivation].call(x) }
|
458
|
+
end
|
459
|
+
|
460
|
+
# Returns an array with the regression values like this:
|
461
|
+
#
|
462
|
+
# [2.5, 2.6, 2.7, 2.8, 3.0, 3.2, 3.4, 3.8, 4.0, 4.9]
|
463
|
+
#
|
464
|
+
def y
|
465
|
+
derivation(0)
|
466
|
+
end
|
467
|
+
|
468
|
+
# Returns the printed formula of this polynom
|
469
|
+
def formula(derivation = 0)
|
470
|
+
@formulas[derivation].join " + "
|
471
|
+
end
|
472
|
+
|
473
|
+
end
|
474
|
+
|
475
|
+
|
476
|
+
# Creates attribute clusters for all available attributes
|
477
|
+
class AttributeCluster
|
478
|
+
include Environment
|
479
|
+
|
480
|
+
def initialize klass, options = {}
|
481
|
+
@options = { :degree => 0, :timebase => 1.second, :scope => :all }.merge(options)
|
482
|
+
@degree, @klass, @cluster = @options[:degree], klass, cluster(@options[:timebase], @options[:scope])
|
483
|
+
|
484
|
+
cluster_attribute :durations
|
485
|
+
cluster_attribute :requests
|
486
|
+
cluster_attribute_with_options :methods
|
487
|
+
cluster_attribute_with_options :statuses
|
488
|
+
cluster_attribute_with_options :paths
|
489
|
+
cluster_attribute_with_options :benchmarks
|
490
|
+
end
|
491
|
+
|
492
|
+
private
|
493
|
+
|
494
|
+
# Allows the attribute reader definition for attribues without options like this:
|
495
|
+
#
|
496
|
+
# cluster_attribute :durations
|
497
|
+
#
|
498
|
+
def cluster_attribute(name)
|
499
|
+
attribute_symbol = "@#{name}".to_sym
|
500
|
+
instance_variable_set(attribute_symbol, @klass.new(@cluster.timesteps, @cluster.send(name.to_sym), @degree))
|
501
|
+
self.class.class_eval do
|
502
|
+
attr_reader name
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
# Allows the attribute reader definition for attribues with options like this:
|
507
|
+
#
|
508
|
+
# cluster_attribute_with_options :statuses
|
509
|
+
#
|
510
|
+
def cluster_attribute_with_options(name)
|
511
|
+
attribute_symbol = "@#{name}".to_sym
|
512
|
+
instance_variable_set attribute_symbol, {}
|
513
|
+
self.class.class_eval do
|
514
|
+
define_method(name) do |option|
|
515
|
+
attribute = instance_variable_get(attribute_symbol)
|
516
|
+
attribute[option.to_s] ||= @klass.new(@cluster.timesteps, @cluster.send(name.to_sym, option), @degree)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
end
|
522
|
+
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|