Current section

Files

Jump to
toolshed lib_src core speed_test.ex
Raw

lib_src/core/speed_test.ex

defmodule Toolshed.Core.SpeedTest do
@typedoc "Options for speed_test/1"
@type speed_test_options :: [
duration: pos_integer(),
ifname: String.t(),
url: String.t() | URI.t()
]
@doc """
Perform a download speed test
Calling this with no options measures the download speed of a small test
file. The test file may not be large enough or close enough to you to
produce a good measurement. To fix this, pass a `:url` to a better file. To
change the default, add the following to your application environment:
```elixir
config :toolshed, speed_test_url: "http://my_company.com/speed_test_file.bin"
```
Commercial users are encouraged to specify their own files to minimize our
bandwidth costs.
Please be aware that this function is somewhat simplistic in how it measures
download performance.
Options:
* `:duration` - Maximum duration in milliseconds (defaults to 5 seconds)
* `:ifname` - Interface to use (e.g., `"wwan0"` or `"eth0"`)
* `:url` - File to download for the test
"""
@spec speed_test(speed_test_options()) :: :ok
def speed_test(options \\ []) do
default_url = Application.get_env(:toolshed, :speed_test_url)
url = Keyword.get(options, :url, default_url) |> URI.parse()
duration = Keyword.get(options, :duration, 5000)
request = [
["GET ", url.path, " HTTP/1.1\r\n"],
["Host: ", url.host, "\r\n"],
"User-Agent: Mozilla/5.0\r\n",
"\r\n",
"\r\n"
]
connect_options =
case Keyword.fetch(options, :ifname) do
{:ok, ifname} ->
[ip: ifname_to_ip(ifname, :inet)]
:error ->
[]
end
case :gen_tcp.connect(
String.to_charlist(url.host),
url.port,
[:binary, {:active, true} | connect_options],
min(5000, duration)
) do
{:ok, s} ->
:ok = :gen_tcp.send(s, request)
wait_for_connect(s, duration)
:gen_tcp.close(s)
{:error, reason} ->
IO.puts("Can't connect to #{url.host}: #{inspect(reason)}")
end
end
defp wait_for_connect(s, duration) do
receive do
{:tcp, ^s, response} ->
length = get_length_from_headers(response)
start_us = System.monotonic_time(:microsecond)
state = %{s: s, start_us: start_us, end_us: start_us + duration * 1000, length: length}
loop(state, 0, start_us)
other ->
IO.inspect(other)
after
5000 ->
IO.puts("Can't connect to download site")
end
end
defp get_length_from_headers(data) do
# Split off the beginning of the content
[header, content] = String.split(data, "\r\n\r\n", parts: 2)
case Regex.run(~r/Content-Length: (\d+)/, header) do
[_, content_length] -> String.to_integer(content_length) - byte_size(content)
_ -> 0
end
end
defp loop(%{length: length} = state, received, _next_status_us) when received >= length do
now = System.monotonic_time(:microsecond)
print_report(state, received, now)
end
defp loop(state, received, next_status_us) do
s = state.s
receive do
{:tcp, ^s, data} ->
new_received = received + byte_size(data)
now = System.monotonic_time(:microsecond)
next_status_us = update_status(state, new_received, now, next_status_us)
if now < state.end_us do
loop(state, new_received, next_status_us)
else
print_report(state, new_received, now)
end
{:tcp_closed, ^s} ->
now = System.monotonic_time(:microsecond)
print_report(state, received, now)
other ->
IO.inspect(other)
after
5000 ->
IO.puts("Timed out waiting for a response")
end
end
defp print_report(state, received, now) do
delta_s = max(now - state.start_us, 1) / 1_000_000
bps = received * 8 / delta_s
IO.puts([
"\r#{received} bytes received in #{Float.round(delta_s, 2)} s\n",
"Download speed: ",
format(bps)
])
end
defp update_status(_state, _received, now, next_status_us) when now < next_status_us do
next_status_us
end
defp update_status(state, received, now, next_status_us) do
delta_us = max(now - state.start_us, 1)
bps = received * 8 * 1.0e6 / delta_us
["\r--> ", format(bps), " "] |> IO.write()
next_status_us + 1_000_000
end
defp format(speed) when speed > 1.0e9, do: "#{Float.round(speed / 1.0e9, 2)} Gbps"
defp format(speed) when speed > 1.0e6, do: "#{Float.round(speed / 1.0e6, 2)} Mbps"
defp format(speed) when speed > 1.0e3, do: "#{Float.round(speed / 1.0e3, 2)} Kbps"
defp format(speed), do: "#{round(speed)} bps"
end