Class: Speedtest::Test

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

Defined Under Namespace

Classes: FailedTransfer

Constant Summary collapse

HTTP_PING_TIMEOUT =
5

Instance Method Summary collapse

Methods included from Logging

#error, #log

Constructor Details

#initialize(options = {}) ⇒ Test

Returns a new instance of Test.



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

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]
  @skip_servers = options[:skip_servers]           || []
  @skip_latency_min_ms = options[:skip_latency_min_ms] || 0

  @ping_runs = 2 if @ping_runs < 2
end

Instance Method Details

#downloadObject



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/speedtest.rb', line 68

def download
  log "\nstarting download tests:"

  start_time = Time.now
  ring_size = @num_threads * 2
  futures_ring = Ring.new(ring_size)
  download_url = download_url(@server_root)
  pool = TransferWorker.pool(size: @num_threads, args: [download_url, @logger])
  1.upto(ring_size).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



64
65
66
# File 'lib/speedtest.rb', line 64

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

#pick_serverObject



138
139
140
141
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/speedtest.rb', line 138

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..20].map { |x|
    {
    :latency => ping(x[:url]),
    :url => x[:url]
    }
  }.sort_by { |x| x[:latency] }

  latency_sorted_servers.reject! do |s|
    skip = false

    if s[:latency] < @skip_latency_min_ms
      log "Skipping #{s} because latency (#{s[:latency]}) is below threshold (#{@skip_latency_min_ms})"
      skip = true
    end

    if @skip_servers.include?(s[:url])
      log "Skipping #{s} because url in skip list"
      skip = true
    end

    skip
  end

  selected = latency_sorted_servers.detect { |s| validate_server_transfer(s[:url]) }
  if selected
    log "Automatically selected server: #{selected[:url]} - #{selected[:latency]} ms"
  else
    error "Cannot find any server matching the requirements"
  end

  selected
end

#ping(server) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/speedtest.rb', line 202

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, Net::OpenTimeout, Errno::ENETUNREACH, Errno::EADDRNOTAVAIL, Errno::ECONNREFUSED => 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



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

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



97
98
99
# File 'lib/speedtest.rb', line 97

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

#runObject

Raises:



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

def run()
  server = pick_server
  raise FailedTransfer, "Failed to find a suitable server" unless 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



105
106
107
108
109
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
# File 'lib/speedtest.rb', line 105

def upload
  log "\nstarting upload tests:"

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

  start_time = Time.now

  ring_size = @num_threads * 2
  futures_ring = Ring.new(ring_size)
  upload_url = upload_url(@server_root)
  pool = TransferWorker.pool(size: @num_threads, args: [upload_url, @logger])
  1.upto(ring_size).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



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

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

#validate_server_transfer(server_root) ⇒ Object



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

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