sidekiq-benchmark 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sidekiq-benchmark.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Konstantin Kosmatov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Sidekiq::Benchmark
2
+
3
+ Adds benchmarking methods to Sidekiq workers, keeps metrics and adds tab to Web UI to let you browse them.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sidekiq-benchmark'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ ## Requirements
16
+
17
+ Redis 2.6.0 or newer required
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ class SampleWorker
23
+ include Sidekiq::Worker
24
+ include Sidekiq::Benchmark::Worker
25
+
26
+ def perform(id)
27
+ benchmark do |bm|
28
+ bm.some_metric do
29
+ 100500.times do
30
+ end
31
+ end
32
+
33
+ bm.other_metric do
34
+ something_code
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ ```
41
+ ## Web UI
42
+ ![Web UI](https://github.com/kosmatov/sidekiq-benchmark/raw/master/examples/web-ui.png)
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ task default: :test
4
+
5
+ Rake::TestTask.new :test do |test|
6
+ test.libs << 'test'
7
+ test.pattern = 'test/**/*_test.rb'
8
+ end
Binary file
@@ -0,0 +1,12 @@
1
+ require 'sidekiq/web'
2
+ require 'sidekiq-benchmark/web'
3
+
4
+ module Sidekiq
5
+ module Benchmark
6
+ autoload :Worker, 'sidekiq-benchmark/worker'
7
+ autoload :Version, 'sidekiq-benchmark/version'
8
+ end
9
+ end
10
+
11
+ Sidekiq::Web.register Sidekiq::Benchmark::Web
12
+ Sidekiq::Web.tabs["Benchmarks"] = "benchmarks"
@@ -0,0 +1,5 @@
1
+ module Sidekiq
2
+ module Benchmark
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,61 @@
1
+ require 'sinatra/assetpack'
2
+ require 'chartkick'
3
+
4
+ module Sidekiq
5
+ module Benchmark
6
+ module Web
7
+ def self.registered(app)
8
+ web_dir = File.expand_path("../../../web", __FILE__)
9
+ js_dir = File.join(web_dir, "assets", "javascripts")
10
+
11
+ app.helpers Chartkick::Helper
12
+ app.register Sinatra::AssetPack
13
+
14
+ app.assets {
15
+ serve '/js', from: js_dir
16
+
17
+ js 'chartkick', ['/js/chartkick.js']
18
+ }
19
+
20
+ app.get "/benchmarks" do
21
+ @charts = {}
22
+
23
+ Sidekiq.redis do |conn|
24
+ @types = conn.smembers "benchmark:types"
25
+ @types.each do |type|
26
+ @charts[type] = { total: [], stats: [] }
27
+
28
+ total_keys = conn.hkeys("benchmark:#{type}:total") -
29
+ ['start_time', 'job_time', 'finish_time']
30
+
31
+ total_time = conn.hget "benchmark:#{type}:total", :job_time
32
+ total_time = total_time.to_f
33
+ total_keys.each do |key|
34
+ value = conn.hget "benchmark:#{type}:total", key
35
+ @charts[type][:total] << [key, value.to_f.round(2)]
36
+ end
37
+
38
+ stats = conn.hgetall "benchmark:#{type}:stats"
39
+ stats.each do |key, value|
40
+ @charts[type][:stats] << [key.to_f, value.to_i]
41
+ end
42
+ end
43
+ end
44
+
45
+ view_path = File.join(web_dir, "views", "benchmarks.slim")
46
+ template = File.read view_path
47
+ render :slim, template
48
+ end
49
+
50
+ app.post "/benchmarks/remove" do
51
+ Sidekiq.redis do |conn|
52
+ keys = conn.keys "benchmark:*"
53
+ conn.del keys
54
+ end
55
+
56
+ redirect "#{root_path}benchmarks"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,85 @@
1
+ module Sidekiq
2
+ module Benchmark
3
+ module Worker
4
+
5
+ def benchmark(options = {})
6
+ bm = Benchmark.new Time.now
7
+
8
+ yield(bm)
9
+
10
+ bm.finish_time = Time.now
11
+ bm.save benchmark_redis_base_key, options
12
+
13
+ bm.set_redis_key benchmark_redis_type_key
14
+ bm
15
+ end
16
+
17
+ def benchmark_redis_type_key
18
+ @benchmark_redis_type_key ||= self.class.name.gsub('::', '_').downcase
19
+ end
20
+
21
+ def benchmark_redis_base_key
22
+ @benchmark_redis_base_key ||= "benchmark:#{benchmark_redis_type_key}"
23
+ end
24
+
25
+ class Benchmark
26
+ attr_reader :metrics, :start_time, :finish_time
27
+
28
+ def initialize(start_time)
29
+ @metrics = {}
30
+ @start_time = start_time.to_f
31
+ end
32
+
33
+ def finish_time=(value)
34
+ @finish_time = value.to_f
35
+ @metrics[:job_time] = @finish_time - start_time
36
+ end
37
+
38
+ def method_missing(name, *args)
39
+ if block_given?
40
+ start_time = Time.now
41
+
42
+ yield
43
+
44
+ finish_time = Time.now
45
+ value = finish_time.to_f - start_time.to_f
46
+ else
47
+ value = args[0].to_f
48
+ end
49
+
50
+ @metrics[name] = value
51
+ end
52
+
53
+ def set_redis_key(key)
54
+ Sidekiq.redis do |conn|
55
+ conn.sadd "benchmark:types", key
56
+ end
57
+ end
58
+
59
+ def save(redis_base_key, options = {})
60
+ options.merge! start_time: start_time, finish_time: finish_time
61
+ options.merge! @metrics
62
+
63
+ job_time_key = @metrics[:job_time].round(1)
64
+
65
+ Sidekiq.redis do |conn|
66
+ conn.multi do
67
+ # Isn't usefull at this moment
68
+ #conn.lpush "#{redis_base_key}:jobs", Sidekiq.dump_json(options)
69
+
70
+ @metrics.each do |key, value|
71
+ conn.hincrbyfloat "#{redis_base_key}:total", key, value
72
+ conn.hincrby "#{redis_base_key}:stats", job_time_key, 1
73
+ end
74
+
75
+ conn.hsetnx "#{redis_base_key}:total", "start_time", start_time
76
+ conn.hincrbyfloat "#{redis_base_key}:total", "job_time", @metrics[:job_time]
77
+ conn.hset "#{redis_base_key}:total", "finish_time", finish_time
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sidekiq-benchmark/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "sidekiq-benchmark"
8
+ gem.version = Sidekiq::Benchmark::VERSION
9
+ gem.authors = ["Konstantin Kosmatov"]
10
+ gem.email = ["key@kosmatov.ru"]
11
+ gem.description = %q{Benchmarks for Sidekiq}
12
+ gem.summary = %q{Adds benchmarking methods to Sidekiq workers, keeps metrics and adds tab to Web UI to let you browse them.}
13
+ gem.homepage = "https://github.com/kosmatov/sidekiq-benchmark/"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "chartkick"
21
+ gem.add_dependency "sinatra-assetpack"
22
+
23
+ gem.add_development_dependency "sidekiq"
24
+ gem.add_development_dependency "slim"
25
+ gem.add_development_dependency "sinatra"
26
+ gem.add_development_dependency "rake"
27
+ gem.add_development_dependency "rack-test"
28
+ gem.add_development_dependency "minitest", "~> 5"
29
+ end
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ module Sidekiq
4
+ module Benchmark
5
+ module Test
6
+ describe "Web extention" do
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ @app ||= Sidekiq::Web
11
+ end
12
+
13
+ before do
14
+ Test.flush_db
15
+ end
16
+
17
+ it "should display index without stats" do
18
+ get '/benchmarks'
19
+ last_response.status.must_equal 200
20
+ end
21
+
22
+ it "should display index with stats" do
23
+ WorkerMock.new
24
+
25
+ get '/benchmarks'
26
+ last_response.status.must_equal 200, last_response.body
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ module Sidekiq
4
+ module Benchmark
5
+ module Test
6
+
7
+ class WorkerTest < Minitest::Spec
8
+ include Sidekiq::Benchmark::Worker
9
+
10
+ before do
11
+ Test.flush_db
12
+ @worker = WorkerMock.new
13
+ end
14
+
15
+ it "should collect metrics" do
16
+ metrics = @worker.bm_obj.metrics
17
+
18
+ @worker.metric_names.each do |metric_name|
19
+ assert metrics[metric_name]
20
+ end
21
+
22
+ assert @worker.bm_obj.start_time
23
+ assert @worker.bm_obj.finish_time
24
+ end
25
+
26
+ it "should save metrics to redis" do
27
+ Sidekiq.redis do |conn|
28
+ total_time = conn.hget("#{@worker.benchmark_redis_base_key}:total", :job_time)
29
+ assert total_time, "Total time: #{total_time.inspect}"
30
+
31
+ metrics = conn.hkeys("#{@worker.benchmark_redis_base_key}:stats")
32
+ assert metrics.any?, "Metrics: #{metrics.inspect}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ require 'minitest/pride'
2
+
3
+ require 'bundler/setup'
4
+ require 'rack/test'
5
+
6
+ require 'sidekiq'
7
+ require 'sidekiq/util'
8
+ require 'sidekiq-benchmark'
9
+
10
+ REDIS = Sidekiq::RedisConnection.create url: "redis://localhost/15", namespace: "testy"
11
+
12
+ Bundler.require
13
+
14
+ module Sidekiq
15
+ module Benchmark
16
+ module Test
17
+
18
+ class WorkerMock
19
+ include Sidekiq::Worker
20
+ include Sidekiq::Benchmark::Worker
21
+
22
+ attr_reader :bm_obj, :metric_names
23
+
24
+ def initialize
25
+ @bm_obj = benchmark do |bm|
26
+ bm.test_metric do
27
+ 2.times do |i|
28
+ bm.send("nested_test_metric_#{i}") do
29
+ 100500.times do |i|
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ @metric_names = [:test_metric, :nested_test_metric_1, :job_time]
37
+ end
38
+ end
39
+
40
+ def self.flush_db
41
+ Sidekiq.redis = REDIS
42
+ Sidekiq.redis do |conn|
43
+ conn.flushdb
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,536 @@
1
+ /*jslint browser: true, indent: 2, plusplus: true */
2
+ /*global google, $*/
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
8
+ function clone(obj) {
9
+ var copy, i, attr, len;
10
+
11
+ // Handle the 3 simple types, and null or undefined
12
+ if (null === obj || "object" !== typeof obj) {
13
+ return obj;
14
+ }
15
+
16
+ // Handle Date
17
+ if (obj instanceof Date) {
18
+ copy = new Date();
19
+ copy.setTime(obj.getTime());
20
+ return copy;
21
+ }
22
+
23
+ // Handle Array
24
+ if (obj instanceof Array) {
25
+ copy = [];
26
+ for (i = 0, len = obj.length; i < len; i++) {
27
+ copy[i] = clone(obj[i]);
28
+ }
29
+ return copy;
30
+ }
31
+
32
+ // Handle Object
33
+ if (obj instanceof Object) {
34
+ copy = {};
35
+ for (attr in obj) {
36
+ if (obj.hasOwnProperty(attr)) {
37
+ copy[attr] = clone(obj[attr]);
38
+ }
39
+ }
40
+ return copy;
41
+ }
42
+
43
+ throw new Error("Unable to copy obj! Its type isn't supported.");
44
+ }
45
+
46
+ // https://github.com/Do/iso8601.js
47
+ var ISO8601_PATTERN = /(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([\+\-])(\d\d)(:)?(\d\d)?)/i;
48
+ var DECIMAL_SEPARATOR = String(1.5).charAt(1);
49
+
50
+ function parseISO8601(input) {
51
+ var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year;
52
+ type = Object.prototype.toString.call(input);
53
+ if (type === '[object Date]') {
54
+ return input;
55
+ }
56
+ if (type !== '[object String]') {
57
+ return;
58
+ }
59
+ if (matches = input.match(ISO8601_PATTERN)) {
60
+ year = parseInt(matches[1], 10);
61
+ month = parseInt(matches[3], 10) - 1;
62
+ day = parseInt(matches[5], 10);
63
+ hour = parseInt(matches[7], 10);
64
+ minutes = matches[9] ? parseInt(matches[9], 10) : 0;
65
+ seconds = matches[11] ? parseInt(matches[11], 10) : 0;
66
+ milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0;
67
+ result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds);
68
+ if (matches[13] && matches[14]) {
69
+ offset = matches[15] * 60;
70
+ if (matches[17]) {
71
+ offset += parseInt(matches[17], 10);
72
+ }
73
+ offset *= matches[14] === '-' ? -1 : 1;
74
+ result -= offset * 60 * 1000;
75
+ }
76
+ return new Date(result);
77
+ }
78
+ }
79
+ // end iso8601.js
80
+
81
+ function negativeValues(series) {
82
+ var i, j, data;
83
+ for (i = 0; i < series.length; i++) {
84
+ data = series[i].data;
85
+ for (j = 0; j < data.length; j++) {
86
+ if (data[j][1] < 0) {
87
+ return true;
88
+ }
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax) {
95
+ return function(series, opts) {
96
+ var options = clone(defaultOptions);
97
+
98
+ // hide legend
99
+ if (series.length === 1) {
100
+ hideLegend(options);
101
+ }
102
+
103
+ // min
104
+ if ("min" in opts) {
105
+ setMin(options, opts.min);
106
+ }
107
+ else if (!negativeValues(series)) {
108
+ setMin(options, 0);
109
+ }
110
+
111
+ // max
112
+ if ("max" in opts) {
113
+ setMax(options, opts.max);
114
+ }
115
+
116
+ return options;
117
+ };
118
+ }
119
+
120
+ // only functions that need defined specific to charting library
121
+ var renderLineChart, renderPieChart, renderColumnChart;
122
+
123
+ if ("Highcharts" in window) {
124
+
125
+ var defaultOptions = {
126
+ xAxis: {
127
+ labels: {
128
+ style: {
129
+ fontSize: "12px"
130
+ }
131
+ }
132
+ },
133
+ yAxis: {
134
+ title: {
135
+ text: null
136
+ },
137
+ labels: {
138
+ style: {
139
+ fontSize: "12px"
140
+ }
141
+ }
142
+ },
143
+ title: {
144
+ text: null
145
+ },
146
+ credits: {
147
+ enabled: false
148
+ },
149
+ legend: {
150
+ borderWidth: 0
151
+ },
152
+ tooltip: {
153
+ style: {
154
+ fontSize: "12px"
155
+ }
156
+ }
157
+ };
158
+
159
+ var hideLegend = function(options) {
160
+ options.legend.enabled = false;
161
+ };
162
+
163
+ var setMin = function(options, min) {
164
+ options.yAxis.min = min;
165
+ };
166
+
167
+ var setMax = function(options, max) {
168
+ options.yAxis.max = max;
169
+ };
170
+
171
+ var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax);
172
+
173
+ renderLineChart = function(element, series, opts) {
174
+ var options = jsOptions(series, opts), data, i, j;
175
+ options.xAxis.type = "datetime";
176
+ options.chart = {type: "spline"};
177
+
178
+ for (i = 0; i < series.length; i++) {
179
+ data = series[i].data;
180
+ for (j = 0; j < data.length; j++) {
181
+ data[j][0] = data[j][0].getTime();
182
+ }
183
+ series[i].marker = {symbol: "circle"};
184
+ }
185
+ options.series = series;
186
+ $(element).highcharts(options);
187
+ };
188
+
189
+ renderPieChart = function(element, series, opts) {
190
+ var options = clone(defaultOptions);
191
+ options.series = [{
192
+ type: "pie",
193
+ name: "Value",
194
+ data: series
195
+ }];
196
+ $(element).highcharts(options);
197
+ };
198
+
199
+ renderColumnChart = function(element, series, opts) {
200
+ var options = jsOptions(series, opts), i, j, s, d, rows = [];
201
+ options.chart = {type: "column"};
202
+
203
+ for (i = 0; i < series.length; i++) {
204
+ s = series[i];
205
+
206
+ for (j = 0; j < s.data.length; j++) {
207
+ d = s.data[j];
208
+ if (!rows[d[0]]) {
209
+ rows[d[0]] = new Array(series.length);
210
+ }
211
+ rows[d[0]][i] = d[1];
212
+ }
213
+ }
214
+
215
+ var categories = [];
216
+ for (i in rows) {
217
+ if (rows.hasOwnProperty(i)) {
218
+ categories.push(i);
219
+ }
220
+ }
221
+ options.xAxis.categories = categories;
222
+
223
+ var newSeries = [];
224
+ for (i = 0; i < series.length; i++) {
225
+ d = [];
226
+ for (j = 0; j < categories.length; j++) {
227
+ d.push(rows[categories[j]][i]);
228
+ }
229
+
230
+ newSeries.push({
231
+ name: series[i].name,
232
+ data: d
233
+ });
234
+ }
235
+ options.series = newSeries;
236
+
237
+ $(element).highcharts(options);
238
+ };
239
+ } else if ("google" in window) { // Google charts
240
+ // load from google
241
+ var loaded = false;
242
+ google.setOnLoadCallback(function() {
243
+ loaded = true;
244
+ });
245
+ google.load("visualization", "1.0", {"packages": ["corechart"]});
246
+
247
+ var waitForLoaded = function(callback) {
248
+ google.setOnLoadCallback(callback); // always do this to prevent race conditions (watch out for other issues due to this)
249
+ if (loaded) {
250
+ callback();
251
+ }
252
+ };
253
+
254
+ // Set chart options
255
+ var defaultOptions = {
256
+ fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",
257
+ pointSize: 6,
258
+ legend: {
259
+ textStyle: {
260
+ fontSize: 12,
261
+ color: "#444"
262
+ },
263
+ alignment: "center",
264
+ position: "right"
265
+ },
266
+ curveType: "function",
267
+ hAxis: {
268
+ textStyle: {
269
+ color: "#666",
270
+ fontSize: 12
271
+ },
272
+ gridlines: {
273
+ color: "transparent"
274
+ },
275
+ baselineColor: "#ccc"
276
+ },
277
+ vAxis: {
278
+ textStyle: {
279
+ color: "#666",
280
+ fontSize: 12
281
+ },
282
+ baselineColor: "#ccc",
283
+ viewWindow: {}
284
+ },
285
+ tooltip: {
286
+ textStyle: {
287
+ color: "#666",
288
+ fontSize: 12
289
+ }
290
+ }
291
+ };
292
+
293
+ var hideLegend = function(options) {
294
+ options.legend.position = "none";
295
+ };
296
+
297
+ var setMin = function(options, min) {
298
+ options.vAxis.viewWindow.min = min;
299
+ };
300
+
301
+ var setMax = function(options, max) {
302
+ options.vAxis.viewWindow.max = max;
303
+ };
304
+
305
+ var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax);
306
+
307
+ // cant use object as key
308
+ var createDataTable = function(series, columnType) {
309
+ var data = new google.visualization.DataTable();
310
+ data.addColumn(columnType, "");
311
+
312
+ var i, j, s, d, key, rows = [];
313
+ for (i = 0; i < series.length; i++) {
314
+ s = series[i];
315
+ data.addColumn("number", s.name);
316
+
317
+ for (j = 0; j < s.data.length; j++) {
318
+ d = s.data[j];
319
+ key = (columnType === "datetime") ? d[0].getTime() : d[0];
320
+ if (!rows[key]) {
321
+ rows[key] = new Array(series.length);
322
+ }
323
+ rows[key][i] = toFloat(d[1]);
324
+ }
325
+ }
326
+
327
+ var rows2 = [];
328
+ for (i in rows) {
329
+ if (rows.hasOwnProperty(i)) {
330
+ rows2.push([(columnType === "datetime") ? new Date(toFloat(i)) : i].concat(rows[i]));
331
+ }
332
+ }
333
+ data.addRows(rows2);
334
+
335
+ return data;
336
+ };
337
+
338
+ renderLineChart = function(element, series, opts) {
339
+ waitForLoaded(function() {
340
+ var options = jsOptions(series, opts);
341
+ var data = createDataTable(series, "datetime");
342
+ var chart = new google.visualization.LineChart(element);
343
+ chart.draw(data, options);
344
+ });
345
+ };
346
+
347
+ renderPieChart = function(element, series, opts) {
348
+ waitForLoaded(function() {
349
+ var options = clone(defaultOptions);
350
+ options.chartArea = {
351
+ top: "10%",
352
+ height: "80%"
353
+ };
354
+
355
+ var data = new google.visualization.DataTable();
356
+ data.addColumn("string", "");
357
+ data.addColumn("number", "Value");
358
+ data.addRows(series);
359
+
360
+ var chart = new google.visualization.PieChart(element);
361
+ chart.draw(data, options);
362
+ });
363
+ };
364
+
365
+ renderColumnChart = function(element, series, opts) {
366
+ waitForLoaded(function() {
367
+ var options = jsOptions(series, opts);
368
+ var data = createDataTable(series, "string");
369
+ var chart = new google.visualization.ColumnChart(element);
370
+ chart.draw(data, options);
371
+ });
372
+ };
373
+ } else { // no chart library installed
374
+ renderLineChart = renderPieChart = renderColumnChart = function() {
375
+ throw new Error("Please install Google Charts or Highcharts");
376
+ };
377
+ }
378
+
379
+ function setText(element, text) {
380
+ if (document.body.innerText) {
381
+ element.innerText = text;
382
+ } else {
383
+ element.textContent = text;
384
+ }
385
+ }
386
+
387
+ function chartError(element, message) {
388
+ setText(element, "Error Loading Chart: " + message);
389
+ element.style.color = "#ff0000";
390
+ }
391
+
392
+ function getJSON(element, url, success) {
393
+ $.ajax({
394
+ dataType: "json",
395
+ url: url,
396
+ success: success,
397
+ error: function(jqXHR, textStatus, errorThrown) {
398
+ var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
399
+ chartError(element, message);
400
+ }
401
+ });
402
+ }
403
+
404
+ function errorCatcher(element, data, opts, callback) {
405
+ try {
406
+ callback(element, data, opts);
407
+ } catch (err) {
408
+ chartError(element, err.message);
409
+ throw err;
410
+ }
411
+ }
412
+
413
+ function fetchDataSource(element, dataSource, opts, callback) {
414
+ if (typeof dataSource === "string") {
415
+ getJSON(element, dataSource, function(data, textStatus, jqXHR) {
416
+ errorCatcher(element, data, opts, callback);
417
+ });
418
+ } else {
419
+ errorCatcher(element, dataSource, opts, callback);
420
+ }
421
+ }
422
+
423
+ // helpers
424
+
425
+ function isArray(variable) {
426
+ return Object.prototype.toString.call(variable) === "[object Array]";
427
+ }
428
+
429
+ // type conversions
430
+
431
+ function toStr(n) {
432
+ return "" + n;
433
+ }
434
+
435
+ function toFloat(n) {
436
+ return parseFloat(n);
437
+ }
438
+
439
+ function toDate(n) {
440
+ if (typeof n !== "object") {
441
+ if (typeof n === "number") {
442
+ n = new Date(n * 1000); // ms
443
+ } else { // str
444
+ // try our best to get the str into iso8601
445
+ // TODO be smarter about this
446
+ var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z");
447
+ n = parseISO8601(str) || new Date(n);
448
+ }
449
+ }
450
+ return n;
451
+ }
452
+
453
+ function toArr(n) {
454
+ if (!isArray(n)) {
455
+ var arr = [], i;
456
+ for (i in n) {
457
+ if (n.hasOwnProperty(i)) {
458
+ arr.push([i, n[i]]);
459
+ }
460
+ }
461
+ n = arr;
462
+ }
463
+ return n;
464
+ }
465
+
466
+ // process data
467
+
468
+ function sortByTime(a, b) {
469
+ return a[0].getTime() - b[0].getTime();
470
+ }
471
+
472
+ function processSeries(series, time) {
473
+ var i, j, data, r, key;
474
+
475
+ // see if one series or multiple
476
+ if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) {
477
+ series = [{name: "Value", data: series}];
478
+ }
479
+
480
+ // right format
481
+ for (i = 0; i < series.length; i++) {
482
+ data = toArr(series[i].data);
483
+ r = [];
484
+ for (j = 0; j < data.length; j++) {
485
+ key = data[j][0];
486
+ key = time ? toDate(key) : toStr(key);
487
+ r.push([key, toFloat(data[j][1])]);
488
+ }
489
+ if (time) {
490
+ r.sort(sortByTime);
491
+ }
492
+ series[i].data = r;
493
+ }
494
+
495
+ return series;
496
+ }
497
+
498
+ function processLineData(element, data, opts) {
499
+ renderLineChart(element, processSeries(data, true), opts);
500
+ }
501
+
502
+ function processColumnData(element, data, opts) {
503
+ renderColumnChart(element, processSeries(data, false), opts);
504
+ }
505
+
506
+ function processPieData(element, data, opts) {
507
+ var perfectData = toArr(data), i;
508
+ for (i = 0; i < perfectData.length; i++) {
509
+ perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])];
510
+ }
511
+ renderPieChart(element, perfectData, opts);
512
+ }
513
+
514
+ function setElement(element, data, opts, callback) {
515
+ if (typeof element === "string") {
516
+ element = document.getElementById(element);
517
+ }
518
+ fetchDataSource(element, data, opts || {}, callback);
519
+ }
520
+
521
+ // define classes
522
+
523
+ var Chartkick = {
524
+ LineChart: function(element, dataSource, opts) {
525
+ setElement(element, dataSource, opts, processLineData);
526
+ },
527
+ ColumnChart: function(element, dataSource, opts) {
528
+ setElement(element, dataSource, opts, processColumnData);
529
+ },
530
+ PieChart: function(element, dataSource, opts) {
531
+ setElement(element, dataSource, opts, processPieData);
532
+ }
533
+ };
534
+
535
+ window.Chartkick = Chartkick;
536
+ })();
@@ -0,0 +1,18 @@
1
+ script src="//www.google.com/jsapi"
2
+ ==js :chartkick
3
+
4
+ header.row
5
+ .span5
6
+ h3 Benchmarks
7
+
8
+ section
9
+ - @types.each do |type|
10
+ section
11
+ h4= type
12
+ figure.row
13
+ .span5
14
+ h5 Count jobs by execution time
15
+ == column_chart @charts[type][:stats]
16
+ .span4
17
+ h5 Average execution time
18
+ == pie_chart @charts[type][:total]
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-benchmark
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Konstantin Kosmatov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: chartkick
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra-assetpack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sidekiq
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: slim
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: sinatra
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rack-test
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: minitest
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '5'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '5'
142
+ description: Benchmarks for Sidekiq
143
+ email:
144
+ - key@kosmatov.ru
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - .gitignore
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - examples/web-ui.png
155
+ - lib/sidekiq-benchmark.rb
156
+ - lib/sidekiq-benchmark/version.rb
157
+ - lib/sidekiq-benchmark/web.rb
158
+ - lib/sidekiq-benchmark/worker.rb
159
+ - sidekiq-benchmark.gemspec
160
+ - test/lib/web_test.rb
161
+ - test/lib/worker_test.rb
162
+ - test/test_helper.rb
163
+ - web/assets/javascripts/chartkick.js
164
+ - web/views/benchmarks.slim
165
+ homepage: https://github.com/kosmatov/sidekiq-benchmark/
166
+ licenses: []
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ! '>='
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ! '>='
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 1.8.25
186
+ signing_key:
187
+ specification_version: 3
188
+ summary: Adds benchmarking methods to Sidekiq workers, keeps metrics and adds tab
189
+ to Web UI to let you browse them.
190
+ test_files:
191
+ - test/lib/web_test.rb
192
+ - test/lib/worker_test.rb
193
+ - test/test_helper.rb
194
+ has_rdoc: