mutalisk 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d5a04080381afb1c9f0aaa2339636135dda6bbb0
4
+ data.tar.gz: 3840193f635d2110303b9ff00fa73d408e70f63d
5
+ SHA512:
6
+ metadata.gz: d3624024cb5aad3a074e039b701c6dfca3733870a36aa0a083560e2fae3fe0ee23f42d796642f4779397776cc671f24f12d6df4720ba94bcea66ae20fff9d5b4
7
+ data.tar.gz: 5f8a8ced9370289f4761c2c68214ccf5315db92e726c981711443057b00bb52537f6600f1d16b782aaf8d76e431f688b7799425d18cd7ea11f0fc1003972664b
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://ruby.taobao.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Mutalisk
2
+
3
+ [Mutalisk](http://starcraft.wikia.com/wiki/Mutalisk_(StarCraft)) is a simple yet powerful HTTP API benchmarking tool.
4
+
5
+ Mutalisk emulates [wrk](https://github.com/wg/wrk), combines the power of multi-threading and IO-multiplexing.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'mutalisk'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install mutalisk
22
+
23
+ ## Usage
24
+
25
+ Do it in [wrk](https://github.com/wg/wrk) style:
26
+
27
+ $ mutalisk -d 5 -t 2 -c 50 --latency http://v2.same.com/channel/1070566/senses
28
+
29
+ Sample output:
30
+
31
+ ```
32
+ Running 5s test @ http://v2.same.com/channel/1070566/senses
33
+ 2 threads and 50 connections
34
+ +----------+------------+---------------+--------------------+------------------------------------------------+
35
+ | time | bytes_read | requests_sent | responses_received | errors |
36
+ +----------+------------+---------------+--------------------+------------------------------------------------+
37
+ | 5.012713 | 27876713 | 1225 | 1175 | {:read=>0, :write=>0, :connect=>0, :status=>0} |
38
+ +----------+------------+---------------+--------------------+------------------------------------------------+
39
+ Latency Distribution
40
+ 50% 159.21ms
41
+ 75% 217.85ms
42
+ 90% 280.15ms
43
+ 95% 344.61ms
44
+ 99% 523.09ms
45
+ 1175 requests in 5.01s, 26.58MB read
46
+ Requests/sec: 234.40
47
+ Transfer/sec: 5.30MB
48
+ ```
49
+
50
+ Side by side comparison with `wrk`:
51
+
52
+ $ wrk -d 5 -t 2 -c 50 --latency http://v2.same.com/channel/1070566/senses
53
+
54
+ ```
55
+ Running 5s test @ http://v2.same.com/channel/1070566/senses
56
+ 2 threads and 50 connections
57
+ Thread Stats Avg Stdev Max +/- Stdev
58
+ Latency 238.27ms 370.89ms 2.07s 96.00%
59
+ Req/Sec 131.30 9.78 142.00 70.00%
60
+ Latency Distribution
61
+ 50% 154.93ms
62
+ 75% 197.59ms
63
+ 90% 321.57ms
64
+ 99% 2.07s
65
+ 1241 requests in 5.01s, 28.00MB read
66
+ Requests/sec: 247.51
67
+ Transfer/sec: 5.58MB
68
+ ```
69
+
70
+ ## Development
71
+
72
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
73
+
74
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and pull requests are welcome on GitHub at https://github.com/forresty/mutalisk.
79
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mutalisk"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/mutalisk ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mutalisk'
4
+
5
+ Mutalisk::CLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/lib/mutalisk.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative 'mutalisk/version'
2
+ require_relative 'mutalisk/errors'
3
+ require_relative 'mutalisk/stats'
4
+ require_relative 'mutalisk/connection'
5
+ require_relative 'mutalisk/ae'
6
+ require_relative 'mutalisk/request'
7
+ require_relative 'mutalisk/response'
8
+ require_relative 'mutalisk/runner_thread'
9
+ require_relative 'mutalisk/runner'
10
+ require_relative 'mutalisk/cli'
@@ -0,0 +1,22 @@
1
+ module Mutalisk
2
+ class AE
3
+ SELECT_TIMEOUT = 0.01
4
+
5
+ def self.readable(connections, pipe)
6
+ sockets = connections.map(&:socket)
7
+
8
+ readable, _ = IO.select(sockets + [pipe], nil, nil, SELECT_TIMEOUT)
9
+
10
+ if readable
11
+ readable.map do |io|
12
+ if io == pipe
13
+ pipe
14
+ else
15
+ socket = io
16
+ socket.instance_variable_get(:@_mutalisk_connection)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,155 @@
1
+ require 'optparse'
2
+ require 'addressable/uri'
3
+
4
+ module Mutalisk
5
+ class CLI
6
+ def initialize(args)
7
+ @args = args
8
+ end
9
+
10
+ def start
11
+ Stats.reset
12
+ @options = { headers: {} }
13
+
14
+ parser = OptionParser.new do |opts|
15
+ opts.banner = "Usage: mutalisk <options> <url>\n Options:"
16
+ opts.version = Mutalisk::VERSION
17
+
18
+ opts.on("-c", "--connections N", Integer, "<N> Connections to keep open") do |n|
19
+ @options[:connections] = n
20
+ end
21
+
22
+ opts.on("-d", "--duration T", Integer, "<T> Duration of test") do |t|
23
+ @options[:seconds] = t
24
+ end
25
+
26
+ opts.on("-t", "--threads N", Integer, "<N> Number of threads to use") do |n|
27
+ @options[:threads] = n
28
+ end
29
+
30
+ opts.on('-H', '--header H', String, "<H> Add header to request") do |h|
31
+ if h =~ /^(.+): (.+)$/
32
+ @options[:headers][$1] = $2
33
+ end
34
+ end
35
+
36
+ opts.on("--latency", " Print latency statistics") do |l|
37
+ @options[:print_latency] = l
38
+ end
39
+
40
+ opts.on("-v", "--[no-]verbose", " Run verbosely") do |v|
41
+ @options[:verbose] = v
42
+ end
43
+
44
+ opts.on("-h", "--help", " Prints this help") do
45
+ puts opts
46
+ exit
47
+ end
48
+ end
49
+ parser.parse!(@args)
50
+
51
+ if url = @args.shift
52
+ uri = Addressable::URI.parse(url)
53
+ if uri.scheme =~ /^https?/
54
+ @options[:uri] = uri
55
+
56
+ runner = Runner.new(@options)
57
+ print_header(runner)
58
+ runner.start
59
+ print_results(runner)
60
+ else
61
+ puts "invalid URL: `#{url}', only HTTP/HTTPS endpoints are supported at the moment"
62
+ end
63
+ else
64
+ puts parser
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def print_header(runner)
71
+ seconds, uri, threads, connections = runner.options.values_at(:seconds, :uri, :threads, :connections)
72
+
73
+ puts "Running #{seconds}s test @ #{uri}"
74
+ puts " #{threads} threads and #{connections} connections"
75
+ end
76
+
77
+ def print_errors(errors)
78
+ read, write, connect, status = errors.values_at(:read, :write, :connect, :status)
79
+
80
+ if read > 0 || write > 0 || connect > 0
81
+ puts " Socket errors: connect #{connect}, read #{read}, write #{write}, timeout UNKNOWN"
82
+ end
83
+
84
+ if status > 0
85
+ puts " Non-2xx or 3xx responses: #{status}"
86
+ end
87
+ end
88
+
89
+ def print_results(runner)
90
+ require 'ttable'
91
+ puts Terminal::Table.new(runner.results)
92
+ completed_requests = runner.results[:responses_received]
93
+ total_time = runner.results[:time]
94
+ bytes_read = runner.results[:bytes_read]
95
+
96
+ if @options[:print_latency]
97
+ puts " Latency Distribution"
98
+ [50, 75, 90, 95, 99].each do |pc|
99
+ puts " #{pc}% #{pretty_time(Stats.percentile(pc))}"
100
+ end
101
+ end
102
+
103
+ puts " #{completed_requests} requests in #{'%.2f' % total_time}s, #{pretty_bytes(bytes_read)} read"
104
+
105
+ qps = '%.2f' % (completed_requests.to_f / total_time)
106
+ bps = pretty_bytes(bytes_read / total_time)
107
+
108
+ print_errors(runner.results[:errors])
109
+
110
+ puts "Requests/sec: #{qps.rjust(8)}"
111
+ puts "Transfer/sec: #{bps.rjust(10)}"
112
+
113
+ if @options[:verbose]
114
+ puts "All finished requests:"
115
+ puts
116
+ puts Terminal::Table.new(Stats.metrics)
117
+ end
118
+
119
+ @results = {
120
+ qps: qps,
121
+ tps: bps,
122
+ uri: @options[:uri]
123
+ }
124
+ end
125
+
126
+ def pretty_time(sec)
127
+ if sec > 1
128
+ ('%.2fs' % sec).rjust(9)
129
+ else
130
+ ('%.2fms' % (sec * 1000)).rjust(10)
131
+ end
132
+ end
133
+
134
+ def pretty_bytes(bytes)
135
+ kb, bytes = bytes.divmod(1024)
136
+
137
+ if kb > 0
138
+ mb, kb = kb.divmod(1024)
139
+
140
+ if mb > 0
141
+ "#{'%.2f' % ((mb*1024 + kb)/1024.0)}MB"
142
+ else
143
+ "#{'%.2f' % ((kb*1024 + bytes)/1024.0)}KB"
144
+ end
145
+
146
+ else
147
+ "#{bytes} bytes"
148
+ end
149
+ end
150
+
151
+ def self.start(args)
152
+ new(args).start
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,124 @@
1
+ require 'socket'
2
+
3
+ module Mutalisk
4
+ class Connection
5
+ attr_reader :socket
6
+ attr_reader :uri
7
+ attr_reader :connection_id
8
+ attr_reader :thread
9
+
10
+ # stats
11
+ attr_reader :bytes_read
12
+ attr_reader :requests_sent
13
+ attr_reader :responses_received
14
+ attr_reader :read_errors
15
+ attr_reader :write_errors
16
+ attr_reader :connect_errors
17
+
18
+ READ_MAXLEN = 100_000
19
+
20
+ def initialize(thread, uri, cid)
21
+ @thread = thread
22
+ @uri = uri
23
+ @connection_id = cid
24
+
25
+ # stats
26
+ @bytes_read = 0
27
+ @requests_sent = 0
28
+ @responses_received = 0
29
+ @read_errors = 0
30
+ @write_errors = 0
31
+ @connect_errors = 0
32
+
33
+ connect
34
+ end
35
+
36
+ def idle?
37
+ !!@idle
38
+ end
39
+
40
+ def send_request(headers=nil)
41
+ raise ConnectionBusyError unless idle?
42
+
43
+ reset_response
44
+
45
+ @request = spawn_request(headers)
46
+ @request.start
47
+
48
+ @idle = false
49
+ @requests_sent += 1
50
+
51
+ @request
52
+ rescue Errno::EPIPE
53
+ @write_errors += 1
54
+ reconnect
55
+ end
56
+
57
+ def receive
58
+ @response ||= Response.new(@request)
59
+
60
+ resp = @socket.readpartial(READ_MAXLEN)
61
+
62
+ @bytes_read += resp.bytesize
63
+
64
+ @response << resp
65
+
66
+ if @response.complete?
67
+ @idle = true
68
+ @responses_received += 1
69
+ end
70
+
71
+ @response
72
+ rescue EOFError
73
+ @idle = true
74
+ @response
75
+ rescue Errno::ECONNRESET => e
76
+ @read_errors += 1
77
+ @idle = true
78
+ reconnect
79
+
80
+ @response
81
+ end
82
+
83
+ private
84
+
85
+ def spawn_request(headers)
86
+ @rid ||= 0
87
+ request = Request.new(self, @rid, headers)
88
+ @rid += 1
89
+
90
+ request
91
+ end
92
+
93
+ def connect
94
+ reconnect
95
+ end
96
+
97
+ def reconnect
98
+ if uri.scheme == 'https'
99
+ sock = TCPSocket.new(uri.host, uri.port || 443)
100
+
101
+ require 'openssl'
102
+ ctx = OpenSSL::SSL::SSLContext.new
103
+
104
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
105
+
106
+ @socket = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
107
+ socket.sync_close = true
108
+ socket.connect
109
+ end
110
+ else
111
+ @socket = TCPSocket.new(uri.host, uri.port || 80)
112
+ end
113
+
114
+ @socket.instance_variable_set(:@_mutalisk_connection, self)
115
+ @idle = true
116
+ rescue SocketError
117
+ @connect_errors += 1
118
+ end
119
+
120
+ def reset_response
121
+ @response = nil
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,6 @@
1
+ module Mutalisk
2
+ class MutaliskError < StandardError; end
3
+ class NotImplementedYet < MutaliskError; end
4
+ class ConnectionBusyError < MutaliskError; end
5
+ class AssertionFailedError < MutaliskError; end
6
+ end
@@ -0,0 +1,66 @@
1
+ module Mutalisk
2
+ class Request
3
+ attr_accessor :uri
4
+ attr_accessor :connection
5
+ attr_accessor :request_id
6
+ attr_accessor :t0
7
+
8
+ def initialize(connection, rid, headers=nil)
9
+ @connection = connection
10
+ @uri = @connection.uri
11
+ @socket = @connection.socket
12
+ @request_id = rid
13
+ @headers = headers
14
+ @done = false
15
+ end
16
+
17
+ def start
18
+ @t0 = Time.now
19
+ path = @uri.path && !@uri.path.empty? ? @uri.path : '/'
20
+
21
+ header_lines = [
22
+ "GET #{path} HTTP/1.1",
23
+ # 'Accept: */*',
24
+ # 'Accept-Encoding: gzip, deflate',
25
+ # 'Connection: keep-alive',
26
+ "Host: #{@uri.host}",
27
+ # "User-Agent: Mutalisk/#{Mutalisk::VERSION}"
28
+ ]
29
+
30
+ if @headers
31
+ @headers.each do |k, v|
32
+ header_lines << "#{k}: #{v}"
33
+ end
34
+ end
35
+
36
+ headers = header_lines.join("\r\n") + "\r\n\r\n"
37
+
38
+ bytes_written = @socket.write(headers)
39
+
40
+ # assume out-going buffer is large enough
41
+ assert bytes_written == headers.bytesize
42
+
43
+ @done = true
44
+ rescue IO::EAGAINWaitWritable
45
+ retry
46
+ end
47
+
48
+ def done?
49
+ !!@done
50
+ end
51
+
52
+ def req_id
53
+ tid = connection.thread.thread_id
54
+ cid = connection.connection_id
55
+ rid = request_id
56
+
57
+ "t#{tid}-c#{cid}-r#{rid}"
58
+ end
59
+
60
+ private
61
+
62
+ def assert(condition)
63
+ raise AssertionFailedError unless condition
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,56 @@
1
+ require 'http/parser'
2
+
3
+ module Mutalisk
4
+ class Response
5
+ attr_reader :code
6
+ attr_reader :headers
7
+ attr_reader :body
8
+ attr_reader :request
9
+ attr_reader :time_spent
10
+
11
+ def initialize(request)
12
+ @request = request
13
+
14
+ @completed = false
15
+ @code = nil
16
+ @body = ''
17
+ @time_spent = nil
18
+
19
+ @parser = Http::Parser.new
20
+
21
+ @parser.on_headers_complete = proc do
22
+ @code = @parser.status_code
23
+ end
24
+
25
+ @parser.on_body = proc do |chunk|
26
+ @body << chunk
27
+ end
28
+
29
+ @parser.on_message_complete = proc do
30
+ @time_spent = Time.now - @request.t0
31
+ @completed = true
32
+ end
33
+ end
34
+
35
+ def <<(data)
36
+ @parser << data
37
+ end
38
+
39
+ def complete?
40
+ !!@completed
41
+ end
42
+
43
+ def success?
44
+ complete? && (@code >= 200 && @code <= 399)
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ req_id: @request.req_id,
50
+ code: @code,
51
+ body_size: @body.bytesize,
52
+ time_spent: @time_spent
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,75 @@
1
+ module Mutalisk
2
+ class Runner
3
+ attr_reader :results
4
+ attr_reader :options
5
+
6
+ DEFAULT_OPTIONS = {
7
+ connections: 10,
8
+ seconds: 10,
9
+ threads: 2
10
+ }
11
+
12
+ def initialize(options={})
13
+ @options = DEFAULT_OPTIONS.merge(options)
14
+ @threads = []
15
+ end
16
+
17
+ def start
18
+ t0 = Time.now
19
+ options_per_thread = @options.dup
20
+ options_per_thread.delete(:threads)
21
+ options_per_thread[:connections] = @options[:connections] / @options[:threads]
22
+
23
+ Thread.abort_on_exception = true
24
+
25
+ @options[:threads].times do |i|
26
+ thread = spawn_thread(options_per_thread, i)
27
+ thread.start
28
+ @threads << thread
29
+ end
30
+
31
+ trap(:INT) {
32
+ puts "INT signal caught, stopping threads..."
33
+ @pipes.each { |writer| writer.write_nonblock('.') }
34
+ }
35
+
36
+ @threads.map(&:join)
37
+
38
+ @results = { time: Time.now - t0 }
39
+ @results = merge_stats(@results, @threads.map(&:results))
40
+ end
41
+
42
+ private
43
+
44
+ def spawn_thread(options_per_thread, i)
45
+ @pipes ||= []
46
+
47
+ reader, writer = create_pipe
48
+
49
+ # selfpipe to pass signals to threads
50
+ @pipes << writer
51
+
52
+ RunnerThread.new(options_per_thread, reader, i)
53
+ end
54
+
55
+ def create_pipe
56
+ IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
57
+ end
58
+
59
+ def merge_stats(dest, array)
60
+ array.each do |hash|
61
+ hash.each do |key, value|
62
+ if value.is_a? Hash
63
+ dest[key] ||= {}
64
+ merge_stats(dest[key], [value])
65
+ else
66
+ dest[key] ||= 0
67
+ dest[key] += value
68
+ end
69
+ end
70
+ end
71
+
72
+ dest
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,94 @@
1
+ module Mutalisk
2
+ class RunnerThread
3
+ attr_reader :results
4
+ attr_reader :thread_id
5
+
6
+ def initialize(options, pipe, id)
7
+ @options = options
8
+ @connections = []
9
+ @responses = []
10
+
11
+ @pipe = pipe
12
+ @interrupted = false
13
+ @thread_id = id
14
+ end
15
+
16
+ def start
17
+ @t0 = Time.now
18
+
19
+ @thread = Thread.new {
20
+ # slow start
21
+ until @connections.size == @options[:connections]
22
+ spawn_connection
23
+ send_requests
24
+ check_response
25
+ end
26
+
27
+ # full speed
28
+ loop do
29
+ send_requests
30
+ check_response
31
+ break if times_up? or @interrupted
32
+ end
33
+
34
+ generate_report
35
+ }
36
+ end
37
+
38
+ def join
39
+ @thread.join
40
+
41
+ Stats.collect(@responses.map(&:to_h))
42
+ end
43
+
44
+ private
45
+
46
+ def spawn_connection
47
+ @cid ||= 0
48
+ connection = Connection.new(self, @options[:uri], @cid)
49
+ @cid += 1
50
+ @connections << connection
51
+ end
52
+
53
+ def send_requests
54
+ @connections.each do |c|
55
+ c.send_request(@options[:headers]) if c.idle?
56
+ end
57
+ end
58
+
59
+ def check_response
60
+ if readable = AE.readable(@connections, @pipe)
61
+ readable.each do |connection|
62
+ if connection == @pipe
63
+ @interrupted = true
64
+ else
65
+ response = connection.receive
66
+
67
+ if response.complete?
68
+ # done with a request
69
+ @responses << response
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def times_up?
77
+ Time.now - @t0 > @options[:seconds]
78
+ end
79
+
80
+ def generate_report
81
+ @results = {
82
+ bytes_read: @connections.map(&:bytes_read).inject(&:+),
83
+ requests_sent: @connections.map(&:requests_sent).inject(&:+),
84
+ responses_received: @connections.map(&:responses_received).inject(&:+),
85
+ errors: {
86
+ read: @connections.map(&:read_errors).inject(&:+),
87
+ write: @connections.map(&:write_errors).inject(&:+),
88
+ connect: @connections.map(&:connect_errors).inject(&:+),
89
+ status: @responses.map(&:code).count { |code| code < 200 || code > 399 }
90
+ }
91
+ }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,22 @@
1
+ module Mutalisk
2
+ class Stats
3
+ def self.collect(metrics)
4
+ @@metrics ||= []
5
+ @@metrics += metrics
6
+ end
7
+
8
+ def self.reset
9
+ @@metrics = []
10
+ end
11
+
12
+ def self.metrics
13
+ @@metrics
14
+ end
15
+
16
+ def self.percentile(pc)
17
+ metrics = @@metrics.sort { |m1, m2| m1[:time_spent] <=> m2[:time_spent] }.reject { |m| m[:code] > 399 || m[:code] < 200 }
18
+
19
+ metrics[pc*metrics.size/100][:time_spent] rescue 0
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Mutalisk
2
+ VERSION = "0.1.0"
3
+ end
data/mutalisk.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mutalisk/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mutalisk"
8
+ spec.version = Mutalisk::VERSION
9
+ spec.authors = ["Forrest Ye"]
10
+ spec.email = ["afu@forresty.com"]
11
+
12
+ spec.summary = %q{A simple yet powerful HTTP API benchmarking tool.}
13
+ spec.homepage = "https://github.com/forresty/mutalisk"
14
+
15
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
16
+ # delete this section to allow pushing this gem to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
21
+ end
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_runtime_dependency 'ttable', '~> 0.0.10'
29
+ spec.add_runtime_dependency 'http_parser.rb', '~> 0.6.0'
30
+ spec.add_runtime_dependency 'addressable', '~> 2.3.8'
31
+
32
+ spec.add_development_dependency 'byebug', '~> 8.2.0'
33
+ spec.add_development_dependency "bundler", "~> 1.10"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec"
36
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mutalisk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Forrest Ye
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ttable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.10
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.10
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: addressable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.3.8
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.3.8
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 8.2.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 8.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - afu@forresty.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - Gemfile
122
+ - README.md
123
+ - Rakefile
124
+ - bin/console
125
+ - bin/mutalisk
126
+ - bin/setup
127
+ - lib/mutalisk.rb
128
+ - lib/mutalisk/ae.rb
129
+ - lib/mutalisk/cli.rb
130
+ - lib/mutalisk/connection.rb
131
+ - lib/mutalisk/errors.rb
132
+ - lib/mutalisk/request.rb
133
+ - lib/mutalisk/response.rb
134
+ - lib/mutalisk/runner.rb
135
+ - lib/mutalisk/runner_thread.rb
136
+ - lib/mutalisk/stats.rb
137
+ - lib/mutalisk/version.rb
138
+ - mutalisk.gemspec
139
+ homepage: https://github.com/forresty/mutalisk
140
+ licenses: []
141
+ metadata:
142
+ allowed_push_host: https://rubygems.org
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.4.5
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: A simple yet powerful HTTP API benchmarking tool.
163
+ test_files: []