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.
Files changed (74) hide show
  1. data/.DS_Store +0 -0
  2. data/README.textile +254 -4
  3. data/Rakefile +43 -9
  4. data/doc/Array.html +288 -0
  5. data/doc/BigBench.html +32 -6
  6. data/doc/BigBench/Benchmark.html +24 -0
  7. data/doc/BigBench/Benchmark/Benchmark.html +24 -0
  8. data/doc/BigBench/Benchmark/Looper.html +24 -0
  9. data/doc/BigBench/Bot.html +24 -0
  10. data/doc/BigBench/Configuration.html +24 -0
  11. data/doc/BigBench/Configuration/Config.html +24 -0
  12. data/doc/BigBench/Configuration/InvalidOptions.html +24 -0
  13. data/doc/BigBench/Executor.html +33 -3
  14. data/doc/BigBench/Executor/InvalidCommand.html +25 -1
  15. data/doc/BigBench/Fragment.html +24 -0
  16. data/doc/BigBench/Fragment/Fragment.html +24 -0
  17. data/doc/BigBench/Output.html +24 -0
  18. data/doc/BigBench/PostProcessor.html +33 -6
  19. data/doc/BigBench/PostProcessor/Environment.html +489 -2
  20. data/doc/BigBench/PostProcessor/Environment/Appearings.html +327 -0
  21. data/doc/BigBench/PostProcessor/Environment/AttributeCluster.html +275 -0
  22. data/doc/BigBench/PostProcessor/Environment/BenchmarkNotFound.html +293 -0
  23. data/doc/BigBench/PostProcessor/Environment/Cluster.html +387 -0
  24. data/doc/BigBench/PostProcessor/Environment/NormalDistribution.html +383 -0
  25. data/doc/BigBench/PostProcessor/Environment/PolynomialRegression.html +438 -0
  26. data/doc/BigBench/PostProcessor/Environment/Statistics.html +568 -0
  27. data/doc/BigBench/PostProcessor/Graphs.html +270 -0
  28. data/doc/BigBench/PostProcessor/Graphs/LineGraph.html +403 -0
  29. data/doc/BigBench/PostProcessor/Graphs/PieGraph.html +396 -0
  30. data/doc/BigBench/PostProcessor/InvalidProcessor.html +25 -1
  31. data/doc/BigBench/PostProcessor/Processor.html +59 -7
  32. data/doc/BigBench/PostProcessor/Statistics.html +26 -2
  33. data/doc/BigBench/PostProcessor/Test.html +26 -2
  34. data/doc/BigBench/Runner.html +24 -0
  35. data/doc/BigBench/Runner/NoBenchmarksDefined.html +24 -0
  36. data/doc/BigBench/Store.html +24 -0
  37. data/doc/BigBench/Tracker.html +24 -0
  38. data/doc/BigBench/Tracker/Tracker.html +24 -0
  39. data/doc/EventMachineLoop.html +24 -0
  40. data/doc/Float.html +24 -0
  41. data/doc/Gemfile.html +24 -0
  42. data/doc/Helpers.html +78 -0
  43. data/doc/Object.html +29 -0
  44. data/doc/README_rdoc.html +803 -0
  45. data/doc/Rakefile.html +66 -10
  46. data/doc/created.rid +46 -40
  47. data/doc/index.html +667 -1
  48. data/doc/js/search_index.js +1 -1
  49. data/doc/lib/bigbench/help/executor_txt.html +32 -2
  50. data/doc/rdoc.css +4 -0
  51. data/doc/table_of_contents.html +179 -23
  52. data/doc/test_rdoc.html +159 -0
  53. data/lib/bigbench.rb +2 -0
  54. data/lib/bigbench/executor.rb +17 -1
  55. data/lib/bigbench/help/executor.txt +5 -0
  56. data/lib/bigbench/post_processor.rb +16 -32
  57. data/lib/bigbench/post_processor/environment.rb +525 -0
  58. data/lib/bigbench/post_processor/graphs.rb +209 -0
  59. data/lib/bigbench/post_processor/statistics.rb +29 -49
  60. data/lib/bigbench/version.rb +1 -1
  61. data/spec/executor_spec.rb +35 -0
  62. data/spec/helpers.rb +15 -1
  63. data/spec/post_processor_spec.rb +19 -4
  64. data/spec/post_processors/environment_spec.rb +412 -0
  65. data/spec/post_processors/graphs_spec.rb +23 -0
  66. data/spec/post_processors/statistics_spec.rb +3 -2
  67. data/spec/tests/local.rb +1 -1
  68. data/spec/tests/sample_results_big.ljson +51925 -0
  69. data/spec/tests/sample_results_small.ljson +3875 -0
  70. data/spec/tests/with_post_processor.ljson +43 -0
  71. data/spec/tests/with_post_processor.rb +12 -0
  72. data/spec/tmp/.DS_Store +0 -0
  73. data/spec/tracker_spec.rb +8 -8
  74. 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
 
@@ -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
- PostProcessor::Environment.module_exec(&@proc)
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}".constantize
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