benchmark-http 0.2.0 → 0.3.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 +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +4 -3
- data/README.md +79 -0
- data/benchmark-http.gemspec +4 -1
- data/lib/benchmark/http/command.rb +4 -1
- data/lib/benchmark/http/command/spider.rb +150 -0
- data/lib/benchmark/http/links_filter.rb +47 -0
- data/lib/benchmark/http/statistics.rb +91 -18
- data/lib/benchmark/http/version.rb +1 -1
- metadata +34 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: de65883bfb494a1d1da97429666bd4a590361632cc008d1292e11003a00a8acc
         | 
| 4 | 
            +
              data.tar.gz: 2b0a8c9f5d9d59799203e6030669a576c4802d0d95c1c1dd4dcf23bba7a875ce
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: '039a873b982321ada9fcfd8cb3f848e46df8786ee2590a075a68580ad4aa5b3bcee638eb47f28a24e88faabf1349215193a02b65d25f377abe3ad091a36f594f'
         | 
| 7 | 
            +
              data.tar.gz: 584cea3731b38082c4a1fb078d883c7eb8d61edc5b626d0ef5dc0ead41e9fe5338edbcfc6535f6b9a66e9187580fbcd25b73d68336e76502a3c5f8bb9a6b6e72
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/Gemfile
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -20,6 +20,85 @@ Install it yourself: | |
| 20 20 |  | 
| 21 21 | 
             
            You can run `benchmark-http` is a top level tool for invoking specific benchmarks.
         | 
| 22 22 |  | 
| 23 | 
            +
            ### Spider
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            This benchmark spiders a website and generates some statistics on general access time.
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            ```shell
         | 
| 28 | 
            +
            $ benchmark-http spider https://www.oriontransfer.co.nz/welcome/index
         | 
| 29 | 
            +
            HEAD https://www.oriontransfer.co.nz/welcome/index -> HTTP/2.0 404 (unspecified bytes)
         | 
| 30 | 
            +
            GET https://www.oriontransfer.co.nz/welcome/index (depth = 10)
         | 
| 31 | 
            +
            GET https://www.oriontransfer.co.nz/welcome/index -> HTTP/2.0 404 (2263 bytes)
         | 
| 32 | 
            +
            HEAD https://www.oriontransfer.co.nz/products/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 33 | 
            +
            GET https://www.oriontransfer.co.nz/products/index (depth = 9)
         | 
| 34 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 35 | 
            +
            GET https://www.oriontransfer.co.nz/services/index (depth = 9)
         | 
| 36 | 
            +
            HEAD https://www.oriontransfer.co.nz/support/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 37 | 
            +
            GET https://www.oriontransfer.co.nz/support/index (depth = 9)
         | 
| 38 | 
            +
            HEAD https://www.oriontransfer.co.nz/support/contact-us -> HTTP/2.0 307 (unspecified bytes)
         | 
| 39 | 
            +
            Following redirect to https://www.oriontransfer.co.nz/support/contact-us/index...
         | 
| 40 | 
            +
            HEAD https://www.oriontransfer.co.nz/support/terms-of-service -> HTTP/2.0 200 (unspecified bytes)
         | 
| 41 | 
            +
            GET https://www.oriontransfer.co.nz/support/terms-of-service (depth = 9)
         | 
| 42 | 
            +
            GET https://www.oriontransfer.co.nz/products/index -> HTTP/2.0 200 (3469 bytes)
         | 
| 43 | 
            +
            GET https://www.oriontransfer.co.nz/services/index -> HTTP/2.0 200 (2488 bytes)
         | 
| 44 | 
            +
            GET https://www.oriontransfer.co.nz/support/index -> HTTP/2.0 200 (2246 bytes)
         | 
| 45 | 
            +
            HEAD https://www.oriontransfer.co.nz/support/contact-us/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 46 | 
            +
            GET https://www.oriontransfer.co.nz/support/contact-us/index (depth = 8)
         | 
| 47 | 
            +
            GET https://www.oriontransfer.co.nz/support/terms-of-service -> HTTP/2.0 200 (8466 bytes)
         | 
| 48 | 
            +
            HEAD https://www.oriontransfer.co.nz/products/library-inspector/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 49 | 
            +
            GET https://www.oriontransfer.co.nz/products/library-inspector/index (depth = 8)
         | 
| 50 | 
            +
            HEAD https://www.oriontransfer.co.nz/products/truth-tables/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 51 | 
            +
            GET https://www.oriontransfer.co.nz/products/truth-tables/index (depth = 8)
         | 
| 52 | 
            +
            HEAD https://www.oriontransfer.co.nz/products/fingerprint/index -> HTTP/2.0 200 (unspecified bytes)
         | 
| 53 | 
            +
            GET https://www.oriontransfer.co.nz/products/fingerprint/index (depth = 8)
         | 
| 54 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/internet-services -> HTTP/2.0 200 (unspecified bytes)
         | 
| 55 | 
            +
            GET https://www.oriontransfer.co.nz/services/internet-services (depth = 8)
         | 
| 56 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/software-development -> HTTP/2.0 200 (unspecified bytes)
         | 
| 57 | 
            +
            GET https://www.oriontransfer.co.nz/services/software-development (depth = 8)
         | 
| 58 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/systems-administration -> HTTP/2.0 200 (unspecified bytes)
         | 
| 59 | 
            +
            GET https://www.oriontransfer.co.nz/services/systems-administration (depth = 8)
         | 
| 60 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/website-development -> HTTP/2.0 200 (unspecified bytes)
         | 
| 61 | 
            +
            GET https://www.oriontransfer.co.nz/services/website-development (depth = 8)
         | 
| 62 | 
            +
            HEAD https://www.oriontransfer.co.nz/support/contact-us/ -> HTTP/2.0 307 (unspecified bytes)
         | 
| 63 | 
            +
            Following redirect to https://www.oriontransfer.co.nz/support/contact-us/index...
         | 
| 64 | 
            +
            GET https://www.oriontransfer.co.nz/support/contact-us/index -> HTTP/2.0 200 (3094 bytes)
         | 
| 65 | 
            +
            GET https://www.oriontransfer.co.nz/products/library-inspector/index -> HTTP/2.0 200 (5592 bytes)
         | 
| 66 | 
            +
            GET https://www.oriontransfer.co.nz/products/truth-tables/index -> HTTP/2.0 200 (4160 bytes)
         | 
| 67 | 
            +
            GET https://www.oriontransfer.co.nz/products/fingerprint/index -> HTTP/2.0 200 (4414 bytes)
         | 
| 68 | 
            +
            GET https://www.oriontransfer.co.nz/services/internet-services -> HTTP/2.0 200 (3362 bytes)
         | 
| 69 | 
            +
            GET https://www.oriontransfer.co.nz/services/software-development -> HTTP/2.0 200 (3521 bytes)
         | 
| 70 | 
            +
            GET https://www.oriontransfer.co.nz/services/systems-administration -> HTTP/2.0 200 (2979 bytes)
         | 
| 71 | 
            +
            GET https://www.oriontransfer.co.nz/services/website-development -> HTTP/2.0 200 (3943 bytes)
         | 
| 72 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/library-inspector/_screenshots/large/Library%20Inspector%20(Libraries).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 73 | 
            +
            Unsupported content type: image/png
         | 
| 74 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/library-inspector/_screenshots/large/Library%20Inspector%20(Libraries%20Disassembly).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 75 | 
            +
            Unsupported content type: image/png
         | 
| 76 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/library-inspector/_screenshots/large/Library%20Inspector%20(Libraries%20QuickLook).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 77 | 
            +
            Unsupported content type: image/png
         | 
| 78 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/library-inspector/_screenshots/large/Library%20Inspector%20(App).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 79 | 
            +
            Unsupported content type: image/png
         | 
| 80 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/library-inspector/_screenshots/large/Library%20Inspector%20(App%20Headers).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 81 | 
            +
            Unsupported content type: image/png
         | 
| 82 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/truth-tables/_screenshots/large/Reformat%20Expression.png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 83 | 
            +
            Unsupported content type: image/png
         | 
| 84 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/truth-tables/_screenshots/large/Large%20Tables.png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 85 | 
            +
            Unsupported content type: image/png
         | 
| 86 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/truth-tables/_screenshots/large/Tutor.png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 87 | 
            +
            Unsupported content type: image/png
         | 
| 88 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/truth-tables/_screenshots/large/Informative%20Text.png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 89 | 
            +
            Unsupported content type: image/png
         | 
| 90 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/fingerprint/_screenshots/large/Fingerprint%20(3).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 91 | 
            +
            Unsupported content type: image/png
         | 
| 92 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/fingerprint/_screenshots/large/Fingerprint%20(2).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 93 | 
            +
            Unsupported content type: image/png
         | 
| 94 | 
            +
            HEAD https://www.oriontransfer.co.nz/_gallery/products/fingerprint/_screenshots/large/Fingerprint%20(1).png -> HTTP/2.0 200 (unspecified bytes)
         | 
| 95 | 
            +
            Unsupported content type: image/png
         | 
| 96 | 
            +
            HEAD https://www.oriontransfer.co.nz/services/training -> HTTP/2.0 200 (unspecified bytes)
         | 
| 97 | 
            +
            GET https://www.oriontransfer.co.nz/services/training (depth = 7)
         | 
| 98 | 
            +
            GET https://www.oriontransfer.co.nz/services/training -> HTTP/2.0 200 (2994 bytes)
         | 
| 99 | 
            +
            14 samples: 13x 200; 1x 404. 15.124294695167547 requests per second. S/D: 35.69ms.
         | 
| 100 | 
            +
            ```
         | 
| 101 | 
            +
             | 
| 23 102 | 
             
            ### Concurrency
         | 
| 24 103 |  | 
| 25 104 | 
             
            This benchmark determines the optimal level of concurrency (maximise throughput while keeping latency to a minimum).
         | 
    
        data/benchmark-http.gemspec
    CHANGED
    
    | @@ -16,7 +16,10 @@ Gem::Specification.new do |spec| | |
| 16 16 | 
             
            	spec.require_paths = ["lib"]
         | 
| 17 17 |  | 
| 18 18 | 
             
            	spec.add_dependency("async-io", "~> 1.5")
         | 
| 19 | 
            -
            	spec.add_dependency("async-http", "~> 0. | 
| 19 | 
            +
            	spec.add_dependency("async-http", "~> 0.27.0")
         | 
| 20 | 
            +
            	spec.add_dependency("async-await")
         | 
| 21 | 
            +
            	
         | 
| 22 | 
            +
            	spec.add_dependency("trenni-sanitize")
         | 
| 20 23 |  | 
| 21 24 | 
             
            	spec.add_dependency('samovar', "~> 1.3")
         | 
| 22 25 |  | 
| @@ -19,6 +19,8 @@ | |
| 19 19 | 
             
            # THE SOFTWARE.
         | 
| 20 20 |  | 
| 21 21 | 
             
            require_relative 'command/concurrency'
         | 
| 22 | 
            +
            require_relative 'command/spider'
         | 
| 23 | 
            +
             | 
| 22 24 | 
             
            require_relative 'version'
         | 
| 23 25 | 
             
            require 'samovar'
         | 
| 24 26 |  | 
| @@ -39,7 +41,8 @@ module Benchmark | |
| 39 41 | 
             
            				end
         | 
| 40 42 |  | 
| 41 43 | 
             
            				nested '<command>',
         | 
| 42 | 
            -
            					'concurrency' => Concurrency
         | 
| 44 | 
            +
            					'concurrency' => Concurrency,
         | 
| 45 | 
            +
            					'spider' => Spider
         | 
| 43 46 |  | 
| 44 47 | 
             
            				def verbose?
         | 
| 45 48 | 
             
            					@options[:logging] == :verbose
         | 
| @@ -0,0 +1,150 @@ | |
| 1 | 
            +
            # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
         | 
| 2 | 
            +
            # 
         | 
| 3 | 
            +
            # Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            # of this software and associated documentation files (the "Software"), to deal
         | 
| 5 | 
            +
            # in the Software without restriction, including without limitation the rights
         | 
| 6 | 
            +
            # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 7 | 
            +
            # copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            # furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
            # 
         | 
| 10 | 
            +
            # The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            # all copies or substantial portions of the Software.
         | 
| 12 | 
            +
            # 
         | 
| 13 | 
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 16 | 
            +
            # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 17 | 
            +
            # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 18 | 
            +
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 | 
            +
            # THE SOFTWARE.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            require_relative '../seconds'
         | 
| 22 | 
            +
            require_relative '../statistics'
         | 
| 23 | 
            +
            require_relative '../links_filter'
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            require 'async'
         | 
| 26 | 
            +
            require 'async/http/client'
         | 
| 27 | 
            +
            require 'async/http/url_endpoint'
         | 
| 28 | 
            +
            require 'async/await'
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            require 'samovar'
         | 
| 31 | 
            +
            require 'uri'
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            module Benchmark
         | 
| 34 | 
            +
            	module HTTP
         | 
| 35 | 
            +
            		module Command
         | 
| 36 | 
            +
            			class Spider < Samovar::Command
         | 
| 37 | 
            +
            				include Async::Await
         | 
| 38 | 
            +
            				
         | 
| 39 | 
            +
            				self.description = "Spider a website and report on performance."
         | 
| 40 | 
            +
            				
         | 
| 41 | 
            +
            				options do
         | 
| 42 | 
            +
            					option '-d/--depth <count>', "The number of nested URLs to traverse.", default: 10, type: Integer
         | 
| 43 | 
            +
            					option '-h/--headers', "Print out the response headers", default: false
         | 
| 44 | 
            +
            				end
         | 
| 45 | 
            +
            				
         | 
| 46 | 
            +
            				many :urls, "One or more hosts to benchmark"
         | 
| 47 | 
            +
            				
         | 
| 48 | 
            +
            				def log(method, url, response)
         | 
| 49 | 
            +
            					puts "#{method} #{url} -> #{response.version} #{response.status} (#{response.body&.length || 'unspecified'} bytes)"
         | 
| 50 | 
            +
            					
         | 
| 51 | 
            +
            					response.headers.each do |key, value|
         | 
| 52 | 
            +
            						puts "\t#{key}: #{value}"
         | 
| 53 | 
            +
            					end if @options[:headers]
         | 
| 54 | 
            +
            				end
         | 
| 55 | 
            +
            				
         | 
| 56 | 
            +
            				def extract_links(url, response)
         | 
| 57 | 
            +
            					base = url
         | 
| 58 | 
            +
            					
         | 
| 59 | 
            +
            					body = response.read
         | 
| 60 | 
            +
            					
         | 
| 61 | 
            +
            					begin
         | 
| 62 | 
            +
            						filter = LinksFilter.parse(body)
         | 
| 63 | 
            +
            					rescue
         | 
| 64 | 
            +
            						Async.logger.error($!)
         | 
| 65 | 
            +
            						return []
         | 
| 66 | 
            +
            					end
         | 
| 67 | 
            +
            					
         | 
| 68 | 
            +
            					if filter.base
         | 
| 69 | 
            +
            						base = base + filter.base
         | 
| 70 | 
            +
            					end
         | 
| 71 | 
            +
            					
         | 
| 72 | 
            +
            					filter.links.collect do |href|
         | 
| 73 | 
            +
            						begin
         | 
| 74 | 
            +
            							full_url = base + href
         | 
| 75 | 
            +
            							
         | 
| 76 | 
            +
            							if full_url.host == url.host && full_url.kind_of?(URI::HTTP)
         | 
| 77 | 
            +
            								yield full_url
         | 
| 78 | 
            +
            							end
         | 
| 79 | 
            +
            						rescue ArgumentError, URI::InvalidURIError
         | 
| 80 | 
            +
            							puts "Could not fetch #{href}, relative to #{base}."
         | 
| 81 | 
            +
            						end
         | 
| 82 | 
            +
            					end.compact
         | 
| 83 | 
            +
            				end
         | 
| 84 | 
            +
            				
         | 
| 85 | 
            +
            				async def fetch(statistics, client, url, depth = @options[:depth], fetched = Set.new)
         | 
| 86 | 
            +
            					return if fetched.include?(url) or depth == 0
         | 
| 87 | 
            +
            					
         | 
| 88 | 
            +
            					fetched << url
         | 
| 89 | 
            +
            					
         | 
| 90 | 
            +
            					request_uri = url.request_uri
         | 
| 91 | 
            +
            					
         | 
| 92 | 
            +
            					response = timeout(10) do
         | 
| 93 | 
            +
            						client.head(request_uri)
         | 
| 94 | 
            +
            					end
         | 
| 95 | 
            +
            					
         | 
| 96 | 
            +
            					log("HEAD", url, response)
         | 
| 97 | 
            +
            					
         | 
| 98 | 
            +
            					if response.redirection?
         | 
| 99 | 
            +
            						location = url + response.headers['location']
         | 
| 100 | 
            +
            						if location.host == url.host
         | 
| 101 | 
            +
            							puts "Following redirect to #{location}..."
         | 
| 102 | 
            +
            							return fetch(statistics, client, location, depth-1, fetched).wait
         | 
| 103 | 
            +
            						else
         | 
| 104 | 
            +
            							puts "Ignoring redirect to #{location}."
         | 
| 105 | 
            +
            							return
         | 
| 106 | 
            +
            						end
         | 
| 107 | 
            +
            					end
         | 
| 108 | 
            +
            					
         | 
| 109 | 
            +
            					content_type = response.headers['content-type']
         | 
| 110 | 
            +
            					unless content_type&.start_with? 'text/html'
         | 
| 111 | 
            +
            						puts "Unsupported content type: #{content_type}"
         | 
| 112 | 
            +
            						return
         | 
| 113 | 
            +
            					end
         | 
| 114 | 
            +
            					
         | 
| 115 | 
            +
            					response = timeout(20) do
         | 
| 116 | 
            +
            						statistics.measure do
         | 
| 117 | 
            +
            							client.get(request_uri)
         | 
| 118 | 
            +
            						end
         | 
| 119 | 
            +
            					end
         | 
| 120 | 
            +
            					
         | 
| 121 | 
            +
            					log("GET", url, response)
         | 
| 122 | 
            +
            					
         | 
| 123 | 
            +
            					extract_links(url, response) do |href|
         | 
| 124 | 
            +
            						fetch(statistics, client, href, depth - 1, fetched)
         | 
| 125 | 
            +
            					end.each(&:wait)
         | 
| 126 | 
            +
            				rescue Async::TimeoutError
         | 
| 127 | 
            +
            					Async.logger.error("Timeout while fetching #{url}")
         | 
| 128 | 
            +
            				rescue StandardError
         | 
| 129 | 
            +
            					Async.logger.error($!)
         | 
| 130 | 
            +
            				end
         | 
| 131 | 
            +
            				
         | 
| 132 | 
            +
            				async def invoke(parent)
         | 
| 133 | 
            +
            					statistics = Statistics.new
         | 
| 134 | 
            +
            					
         | 
| 135 | 
            +
            					@urls.each do |url|
         | 
| 136 | 
            +
            						endpoint = Async::HTTP::URLEndpoint.parse(url)
         | 
| 137 | 
            +
            						
         | 
| 138 | 
            +
            						Async::HTTP::Client.open(endpoint, endpoint.protocol, connection_limit: 4) do |client|
         | 
| 139 | 
            +
            							fetch(statistics, client, endpoint.url).wait
         | 
| 140 | 
            +
            						end
         | 
| 141 | 
            +
            					end
         | 
| 142 | 
            +
            					
         | 
| 143 | 
            +
            					statistics.print
         | 
| 144 | 
            +
            					
         | 
| 145 | 
            +
            					return statistics
         | 
| 146 | 
            +
            				end
         | 
| 147 | 
            +
            			end
         | 
| 148 | 
            +
            		end
         | 
| 149 | 
            +
            	end
         | 
| 150 | 
            +
            end
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
         | 
| 2 | 
            +
            # 
         | 
| 3 | 
            +
            # Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            # of this software and associated documentation files (the "Software"), to deal
         | 
| 5 | 
            +
            # in the Software without restriction, including without limitation the rights
         | 
| 6 | 
            +
            # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 7 | 
            +
            # copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            # furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
            # 
         | 
| 10 | 
            +
            # The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            # all copies or substantial portions of the Software.
         | 
| 12 | 
            +
            # 
         | 
| 13 | 
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 16 | 
            +
            # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 17 | 
            +
            # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 18 | 
            +
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 | 
            +
            # THE SOFTWARE.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            require 'trenni/sanitize'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            module Benchmark
         | 
| 24 | 
            +
            	module HTTP
         | 
| 25 | 
            +
            		class LinksFilter < Trenni::Sanitize::Filter
         | 
| 26 | 
            +
            			def initialize(*)
         | 
| 27 | 
            +
            				super
         | 
| 28 | 
            +
            				
         | 
| 29 | 
            +
            				@base = nil
         | 
| 30 | 
            +
            				@links = []
         | 
| 31 | 
            +
            			end
         | 
| 32 | 
            +
            			
         | 
| 33 | 
            +
            			attr :base
         | 
| 34 | 
            +
            			attr :links
         | 
| 35 | 
            +
            			
         | 
| 36 | 
            +
            			def filter(node)
         | 
| 37 | 
            +
            				if node.name == 'base'
         | 
| 38 | 
            +
            					@base = node['href']
         | 
| 39 | 
            +
            				elsif node.name == 'a'
         | 
| 40 | 
            +
            					@links << node['href']
         | 
| 41 | 
            +
            				end
         | 
| 42 | 
            +
            				
         | 
| 43 | 
            +
            				node.skip!(TAG)
         | 
| 44 | 
            +
            			end
         | 
| 45 | 
            +
            		end
         | 
| 46 | 
            +
            	end
         | 
| 47 | 
            +
            end
         | 
| @@ -18,23 +18,38 @@ | |
| 18 18 | 
             
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 19 | 
             
            # THE SOFTWARE.
         | 
| 20 20 |  | 
| 21 | 
            +
            require 'async/clock'
         | 
| 22 | 
            +
             | 
| 21 23 | 
             
            module Benchmark
         | 
| 22 24 | 
             
            	module HTTP
         | 
| 23 | 
            -
            		class  | 
| 24 | 
            -
            			def initialize(concurrency)
         | 
| 25 | 
            +
            		class Stopwatch
         | 
| 26 | 
            +
            			def initialize(concurrency = 0)
         | 
| 25 27 | 
             
            				@samples = []
         | 
| 26 | 
            -
            				 | 
| 28 | 
            +
            				
         | 
| 29 | 
            +
            				@total_time = 0
         | 
| 30 | 
            +
            				
         | 
| 31 | 
            +
            				# The number of currently executing measurements:
         | 
| 32 | 
            +
            				@count = 0
         | 
| 27 33 |  | 
| 28 34 | 
             
            				@concurrency = concurrency
         | 
| 35 | 
            +
            				@start_time = nil
         | 
| 29 36 | 
             
            			end
         | 
| 30 37 |  | 
| 38 | 
            +
            			# The individual samples' durations.
         | 
| 31 39 | 
             
            			attr :samples
         | 
| 32 | 
            -
            			attr :duration
         | 
| 33 40 |  | 
| 41 | 
            +
            			# The sequential time of all samples.
         | 
| 42 | 
            +
            			attr :total_time
         | 
| 43 | 
            +
            			
         | 
| 44 | 
            +
            			# The maximum number of executing measurements at any one time.
         | 
| 34 45 | 
             
            			attr :concurrency
         | 
| 35 46 |  | 
| 47 | 
            +
            			def duration
         | 
| 48 | 
            +
            				@samples.sum
         | 
| 49 | 
            +
            			end
         | 
| 50 | 
            +
            			
         | 
| 36 51 | 
             
            			def sequential_duration
         | 
| 37 | 
            -
            				 | 
| 52 | 
            +
            				duration / @concurrency
         | 
| 38 53 | 
             
            			end
         | 
| 39 54 |  | 
| 40 55 | 
             
            			def count
         | 
| @@ -42,11 +57,11 @@ module Benchmark | |
| 42 57 | 
             
            			end
         | 
| 43 58 |  | 
| 44 59 | 
             
            			def per_second
         | 
| 45 | 
            -
            				@samples.count.to_f /  | 
| 60 | 
            +
            				@samples.count.to_f / total_time.to_f
         | 
| 46 61 | 
             
            			end
         | 
| 47 62 |  | 
| 48 63 | 
             
            			def latency
         | 
| 49 | 
            -
            				 | 
| 64 | 
            +
            				duration.to_f / count.to_f
         | 
| 50 65 | 
             
            			end
         | 
| 51 66 |  | 
| 52 67 | 
             
            			def similar?(other, difference = 2.0)
         | 
| @@ -61,13 +76,17 @@ module Benchmark | |
| 61 76 | 
             
            				end
         | 
| 62 77 | 
             
            			end
         | 
| 63 78 |  | 
| 79 | 
            +
            			def valid?
         | 
| 80 | 
            +
            				@samples.count > 1
         | 
| 81 | 
            +
            			end
         | 
| 82 | 
            +
            			
         | 
| 64 83 | 
             
            			# Computes Population Variance, σ^2.
         | 
| 65 84 | 
             
            			def variance
         | 
| 66 | 
            -
            				 | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
            				 | 
| 85 | 
            +
            				if valid?
         | 
| 86 | 
            +
            					average = self.average
         | 
| 87 | 
            +
            					
         | 
| 88 | 
            +
            					return @samples.map{|n| (n - average)**2}.sum / @samples.count
         | 
| 89 | 
            +
            				end
         | 
| 71 90 | 
             
            			end
         | 
| 72 91 |  | 
| 73 92 | 
             
            			# Population Standard Deviation, σ
         | 
| @@ -83,16 +102,37 @@ module Benchmark | |
| 83 102 | 
             
            				end
         | 
| 84 103 | 
             
            			end
         | 
| 85 104 |  | 
| 105 | 
            +
            			def add(duration, result = nil)
         | 
| 106 | 
            +
            				@samples << duration
         | 
| 107 | 
            +
            			end
         | 
| 108 | 
            +
            			
         | 
| 86 109 | 
             
            			def measure
         | 
| 87 | 
            -
            				 | 
| 110 | 
            +
            				@count += 1
         | 
| 111 | 
            +
            				
         | 
| 112 | 
            +
            				if @count > @concurrency
         | 
| 113 | 
            +
            					@concurrency = @count
         | 
| 114 | 
            +
            				end
         | 
| 115 | 
            +
            				
         | 
| 116 | 
            +
            				start_time = Async::Clock.now
         | 
| 117 | 
            +
            				
         | 
| 118 | 
            +
            				unless @start_time
         | 
| 119 | 
            +
            					@start_time = start_time
         | 
| 120 | 
            +
            				end
         | 
| 88 121 |  | 
| 89 122 | 
             
            				result = yield
         | 
| 90 123 |  | 
| 91 | 
            -
            				 | 
| 92 | 
            -
            				 | 
| 93 | 
            -
            				 | 
| 124 | 
            +
            				end_time = Async::Clock.now
         | 
| 125 | 
            +
            				
         | 
| 126 | 
            +
            				self.add(end_time - start_time, result)
         | 
| 94 127 |  | 
| 95 128 | 
             
            				return result
         | 
| 129 | 
            +
            			ensure
         | 
| 130 | 
            +
            				@count -= 1
         | 
| 131 | 
            +
            				
         | 
| 132 | 
            +
            				if @count == 0
         | 
| 133 | 
            +
            					@total_time += end_time - @start_time
         | 
| 134 | 
            +
            					@start_time = nil
         | 
| 135 | 
            +
            				end
         | 
| 96 136 | 
             
            			end
         | 
| 97 137 |  | 
| 98 138 | 
             
            			def sample(confidence_factor, &block)
         | 
| @@ -105,13 +145,46 @@ module Benchmark | |
| 105 145 | 
             
            			end
         | 
| 106 146 |  | 
| 107 147 | 
             
            			def print(out = STDOUT)
         | 
| 108 | 
            -
            				 | 
| 148 | 
            +
            				if self.valid?
         | 
| 149 | 
            +
            					out.puts "#{@samples.count} samples. #{per_second} requests per second. S/D: #{Seconds[standard_deviation]}."
         | 
| 150 | 
            +
            				else
         | 
| 151 | 
            +
            					out.puts "Not enough samples."
         | 
| 152 | 
            +
            				end
         | 
| 109 153 | 
             
            			end
         | 
| 110 154 |  | 
| 111 155 | 
             
            			private
         | 
| 112 156 |  | 
| 113 157 | 
             
            			def confident?(factor)
         | 
| 114 | 
            -
            				 | 
| 158 | 
            +
            				if @samples.count > @concurrency
         | 
| 159 | 
            +
            					return self.standard_error < (self.average * factor)
         | 
| 160 | 
            +
            				end
         | 
| 161 | 
            +
            				
         | 
| 162 | 
            +
            				return false
         | 
| 163 | 
            +
            			end
         | 
| 164 | 
            +
            		end
         | 
| 165 | 
            +
            		
         | 
| 166 | 
            +
            		class Statistics < Stopwatch
         | 
| 167 | 
            +
            			def initialize(*)
         | 
| 168 | 
            +
            				super
         | 
| 169 | 
            +
            				
         | 
| 170 | 
            +
            				# The count of the status codes seen in the responses:
         | 
| 171 | 
            +
            				@responses = Hash.new{|h,k| 0}
         | 
| 172 | 
            +
            			end
         | 
| 173 | 
            +
            			
         | 
| 174 | 
            +
            			def add(duration, result)
         | 
| 175 | 
            +
            				super
         | 
| 176 | 
            +
            				
         | 
| 177 | 
            +
            				@responses[result.status] += 1
         | 
| 178 | 
            +
            			end
         | 
| 179 | 
            +
            			
         | 
| 180 | 
            +
            			def print(out = STDOUT)
         | 
| 181 | 
            +
            				if valid?
         | 
| 182 | 
            +
            					counts = @responses.sort.collect{|status, count| "#{count}x #{status}"}.join("; ")
         | 
| 183 | 
            +
            					
         | 
| 184 | 
            +
            					out.puts "#{@samples.count} samples: #{counts}. #{per_second.round(2)} requests per second. S/D: #{Seconds[standard_deviation]}."
         | 
| 185 | 
            +
            				else
         | 
| 186 | 
            +
            					out.puts "Not enough samples."
         | 
| 187 | 
            +
            				end
         | 
| 115 188 | 
             
            			end
         | 
| 116 189 | 
             
            		end
         | 
| 117 190 | 
             
            	end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: benchmark-http
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.3.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Samuel Williams
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018- | 
| 11 | 
            +
            date: 2018-07-21 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: async-io
         | 
| @@ -30,14 +30,42 @@ dependencies: | |
| 30 30 | 
             
                requirements:
         | 
| 31 31 | 
             
                - - "~>"
         | 
| 32 32 | 
             
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            -
                    version:  | 
| 33 | 
            +
                    version: 0.27.0
         | 
| 34 34 | 
             
              type: :runtime
         | 
| 35 35 | 
             
              prerelease: false
         | 
| 36 36 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 37 | 
             
                requirements:
         | 
| 38 38 | 
             
                - - "~>"
         | 
| 39 39 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            -
                    version:  | 
| 40 | 
            +
                    version: 0.27.0
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: async-await
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: trenni-sanitize
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ">="
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :runtime
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ">="
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 41 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 42 70 | 
             
              name: samovar
         | 
| 43 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -113,6 +141,8 @@ files: | |
| 113 141 | 
             
            - lib/benchmark/http.rb
         | 
| 114 142 | 
             
            - lib/benchmark/http/command.rb
         | 
| 115 143 | 
             
            - lib/benchmark/http/command/concurrency.rb
         | 
| 144 | 
            +
            - lib/benchmark/http/command/spider.rb
         | 
| 145 | 
            +
            - lib/benchmark/http/links_filter.rb
         | 
| 116 146 | 
             
            - lib/benchmark/http/seconds.rb
         | 
| 117 147 | 
             
            - lib/benchmark/http/statistics.rb
         | 
| 118 148 | 
             
            - lib/benchmark/http/version.rb
         |