Class: Speedtest::Test

Inherits:
Object
  • Object
show all
Defined in:
lib/speedtest.rb

Defined Under Namespace

Classes: FailedTransfer

Constant Summary collapse

HTTP_PING_TIMEOUT =
5

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Test

Returns a new instance of Test.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/speedtest.rb', line 17

def initialize(options = {})
  @min_transfer_secs = options[:min_transfer_secs] || 10
  @num_threads   = options[:num_threads]           || 4
  @ping_runs = options[:ping_runs]                 || 4
  @download_size = options[:download_size]         || 4000
  @upload_size = options[:upload_size]             || 1_000_000
  @logger = options[:logger]
  @num_transfers_padding = options[:num_transfers_padding] || 5

  if @num_transfers_padding > @num_threads
    @num_transfers_padding = @num_threads
  end
  @ping_runs = 2 if @ping_runs < 2
end

Instance Method Details

#downloadObject



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/speedtest.rb', line 74

def download
  log "\nstarting download tests:"

  start_time = Time.now
  futures_ring = Ring.new(@num_threads + @num_transfers_padding)
  download_url = download_url(@server_root)
  pool = TransferWorker.pool(size: @num_threads, args: [download_url, @logger])
  1.upto(@num_threads + @num_transfers_padding).each do |i|
    futures_ring.append(pool.future.download)
  end

  total_downloaded = 0
  while (future = futures_ring.pop) do
    status = future.value
    raise FailedTransfer.new("Download failed.") if status.error == true
    total_downloaded += status.size

    if Time.now - start_time < @min_transfer_secs
      futures_ring.append(pool.future.download)
    end
  end

  total_time = Time.new - start_time
  log "Took #{total_time} seconds to download #{total_downloaded} bytes in #{@num_threads} threads\n"

  [ total_downloaded * 8, total_time ]
end

#download_url(server_root) ⇒ Object



70
71
72
# File 'lib/speedtest.rb', line 70

def download_url(server_root)
  "#{server_root}/speedtest/random#{@download_size}x#{@download_size}.jpg"
end

#error(msg) ⇒ Object



66
67
68
# File 'lib/speedtest.rb', line 66

def error(msg)
  @logger.error msg if @logger
end

#log(msg) ⇒ Object



62
63
64
# File 'lib/speedtest.rb', line 62

def log(msg)
  @logger.debug msg if @logger
end

#pick_serverObject



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/speedtest.rb', line 142

def pick_server
  page = HTTParty.get("http://www.speedtest.net/speedtest-config.php")
  ip,lat,lon = page.body.scan(/<client ip="([^"]*)" lat="([^"]*)" lon="([^"]*)"/)[0]
  orig = GeoPoint.new(lat, lon)
  log "Your IP: #{ip}\nYour coordinates: #{orig}\n"

  page = HTTParty.get("http://www.speedtest.net/speedtest-servers.php")
  sorted_servers=page.body.scan(/<server url="([^"]*)" lat="([^"]*)" lon="([^"]*)/).map { |x| {
    :distance => orig.distance(GeoPoint.new(x[1],x[2])),
    :url => x[0].split(/(http:\/\/.*)\/speedtest.*/)[1]
  } }
  .reject { |x| x[:url].nil? } # reject 'servers' without a domain
  .sort_by { |x| x[:distance] }

  # sort the nearest 10 by download latency
  latency_sorted_servers = sorted_servers[0..9].map { |x|
    {
    :latency => ping(x[:url]),
    :url => x[:url]
    }}.sort_by { |x| x[:latency] }


  selected = latency_sorted_servers.detect { |s| validate_server_transfer(s[:url]) }
  log "Automatically selected server: #{selected[:url]} - #{selected[:latency]} ms"

  selected
end

#ping(server) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/speedtest.rb', line 186

def ping(server)
  times = []
  1.upto(@ping_runs) {
    start = Time.new
    begin
      page = HTTParty.get("#{server}/speedtest/latency.txt", timeout: HTTP_PING_TIMEOUT)
      times << Time.new - start
    rescue Timeout::Error, Net::HTTPNotFound, Errno::ENETUNREACH => e
      log "ping error: #{e.class} [#{e}] for #{server}"
      times << 999999
    end
  }
  times.sort
  times[1, @ping_runs].inject(:+) * 1000 / @ping_runs # average in milliseconds
end

#pretty_speed(speed) ⇒ Object



52
53
54
55
56
57
58
59
60
# File 'lib/speedtest.rb', line 52

def pretty_speed(speed)
  units = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]
  i = 0
  while speed > 1024
    speed /= 1024
    i += 1
  end
  "%.2f #{units[i]}" % speed
end

#randomString(alphabet, size) ⇒ Object



102
103
104
# File 'lib/speedtest.rb', line 102

def randomString(alphabet, size)
  (1.upto(size)).map { alphabet[rand(alphabet.length)] }.join
end

#runObject



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/speedtest.rb', line 32

def run()
  server = pick_server
  @server_root = server[:url]
  log "Server #{@server_root}"

  latency = server[:latency]

  download_size, download_time = download
  download_rate = download_size / download_time
  log "Download: #{pretty_speed download_rate}"

  upload_size, upload_time = upload
  upload_rate = upload_size / upload_time
  log "Upload: #{pretty_speed upload_rate}"

  Result.new(:server => @server_root, :latency => latency,
    download_size: download_size, download_time: download_time,
    upload_size: upload_size, upload_time: upload_time)
end

#uploadObject



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/speedtest.rb', line 110

def upload
  log "\nstarting upload tests:"

  data = randomString(('A'..'Z').to_a, @upload_size)

  start_time = Time.now

  futures_ring = Ring.new(@num_threads + @num_transfers_padding)
  upload_url = upload_url(@server_root)
  pool = TransferWorker.pool(size: @num_threads, args: [upload_url, @logger])
  1.upto(@num_threads + @num_transfers_padding).each do |i|
    futures_ring.append(pool.future.upload(data))
  end

  total_uploaded = 0
  while (future = futures_ring.pop) do
    status = future.value
    raise FailedTransfer.new("Upload failed.") if status.error == true
    total_uploaded += status.size

    if Time.now - start_time < @min_transfer_secs
      futures_ring.append(pool.future.upload(data))
    end
  end

  total_time = Time.new - start_time
  log "Took #{total_time} seconds to upload #{total_uploaded} bytes in #{@num_threads} threads\n"

  # bytes to bits / time = bps
  [ total_uploaded * 8, total_time ]
end

#upload_url(server_root) ⇒ Object



106
107
108
# File 'lib/speedtest.rb', line 106

def upload_url(server_root)
  "#{server_root}/speedtest/upload.php"
end

#validate_server_transfer(server_root) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/speedtest.rb', line 170

def validate_server_transfer(server_root)
  downloader = TransferWorker.new(download_url(server_root), @logger)
  status = downloader.download
  raise RuntimeError if status.error

  uploader = TransferWorker.new(upload_url(server_root), @logger)
  data = randomString(('A'..'Z').to_a, @upload_size)
  status = uploader.upload(data)
  raise RuntimeError if status.error || status.size < @upload_size

  true
rescue => e
  log "Rejecting #{server_root}"
  false
end