Many people are familiar with Speedtest.net, which is used to test a network connection, displaying download speeds, upload speeds, and server latency. Implemented as a Flash-based interface, it can be used from a web browser to verify your internet provider is giving you what you pay for.
You might not be aware that the speedtest-cli project provides a way to check internet speed from the command line in a similar manner.
I thought it might be fun to implement an interface to Speedtest.net using Factor:
Closest Servers
Speedtest provides a list of available servers all over the world that can be used for testing, returned as XML. After parsing the XML document, we use a utility method to extract attributes for each server into an array:
: attr-map ( tag -- attrs ) attrs>> [ [ main>> ] dip ] H{ } assoc-map-as ; : speedtest-servers ( -- servers ) "http://www.speedtest.net/speedtest-servers.php" http-get nip string>xml "server" deep-tags-named [ attr-map ] map ;
Calculating the geographical distance between two points, specified by latitude and longitude:
: radians ( degrees -- radians ) pi * 180 /f ; inline :: geo-distance ( lat1 lon1 lat2 lon2 -- distance ) 6371 :> radius ! km lat2 lat1 - radians :> dlat lon2 lon1 - radians :> dlon dlat 2 / sin sq dlon 2 / sin sq lat1 radians cos lat2 radians cos * * + :> a a sqrt 1 a - sqrt fatan2 2 * :> c radius c * ;
This lets us find the closest server to a given geographic location:
: lat/lon ( assoc -- lat lon ) [ "lat" of ] [ "lon" of ] bi [ string>number ] bi@ ; : server-distance ( server lat lon -- server ) '[ lat/lon _ _ geo-distance "distance" ] keep [ set-at ] keep ; : closest-servers-to ( lat lon -- servers ) [ speedtest-servers ] 2dip '[ _ _ server-distance ] map [ "distance" of ] sort-with ;
The available Speedtest configuration provides our latitude and longitude, allowing us to sort the server list by geographic distance:
TUPLE: config client times download upload ; C: <config> config : speedtest-config ( -- config ) "http://www.speedtest.net/speedtest-config.php" http-get nip string>xml { [ "client" deep-tag-named attr-map ] [ "times" deep-tag-named attr-map ] [ "download" deep-tag-named attr-map ] [ "upload" deep-tag-named attr-map ] } cleave <config> ; : closest-servers ( -- servers ) speedtest-config client>> lat/lon closest-servers-to ;
Best Server
We can calculate latency by downloading a small latency.txt
file and timing how long it takes:
: (server-latency) ( server -- ms ) "url" of >url URL" latency.txt" derive-url [ http-get nip "test=test\n" = ] benchmark 1,000,000 /f 3,600,000 ? ;
After calculating latency, we save it for later use:
: server-latency ( server -- server ) [ (server-latency) "latency" ] keep [ set-at ] keep ;
The "best" server that we will use for testing is the one with the lowest latency, checking the five closest servers to our location:
: best-server ( -- server ) closest-servers 5 short head [ server-latency ] parallel-map [ "latency" of ] sort-with first ;
Upload Speed
To calculate upload speed, we upload several document sizes (filling the content with zeroes) and time how long it takes:
: upload-data ( size -- data ) 9 - CHAR: 0 <string> "content1=" prepend ; : (upload-speed) ( server -- Mbps ) "url" of >url { 250,000 500,000 } [ [ upload-data [ swap http-put 2drop ] keep length ] with map-sum ] benchmark 1,000,000,000 /f / 8 * 1,000,000 / ;
After calculating upload speed, we save it for later use:
: upload-speed ( server -- server ) [ (upload-speed) "upload" ] keep [ set-at ] keep ;
Download Speed
To calculate download speed, we download several files with varying sizes in parallel and time how long it takes:
: download-urls ( server -- urls ) "url" { 350 500 750 1000 } [ dup "random%sx%s.jpg" sprintf >url derive-url ] with map ; : (download-speed) ( server -- Mbps ) download-urls 4 swap <array> [ [ [ http-get nip length ] map-sum ] parallel-map sum ] benchmark 1,000,000,000 /f / 8 * 1,000,000 / ;
After calculating download speed, we save it for later use:
: download-speed ( server -- server ) [ (download-speed) "download" ] keep [ set-at ] keep ;
Text Results
With all of that built, we can build a word to run a Speedtest, printing out the results as text:
: run-speedtest ( -- server ) "Selecting best server based on ping..." print flush best-server dup { [ "sponsor" of ] [ "name" of ] [ "distance" of ] [ "latency" of ] } cleave "Hosted by %s (%s) [%0.2f km]: %s ms\n" printf "Testing download speed" print flush download-speed dup "download" of "Download: %0.2f Mbit/s\n" printf "Testing upload speed" print flush upload-speed dup "upload" of "Upload: %0.2f Mbit/s\n" printf ;
Graphic Results
It would be nice if we could show the reports graphically, and as it turns out, its not too hard. We just have to upload the results to speedtest.net in the same way their Flash application does, and then display the image that is created for you.
: make-result ( server -- result ) [ { [ "download" of 1,000 * >integer "download" ,, ] [ "latency" of >integer "ping" ,, ] [ "upload" of 1,000 * >integer "upload" ,, ] [ drop "" "promo" ,, ] [ drop "pingselect" "startmode" ,, ] [ "id" of "recommendedserverid" ,, ] [ drop "1" "accuracy" ,, ] [ "id" of "serverid" ,, ] [ [ "latency" of ] [ "upload" of 1,000 * ] [ "download" of 1,000 * ] tri "%d-%d-%d-297aae72" sprintf md5 checksum-bytes hex-string "hash" ,, ] } cleave ] { } make ; : submit-result ( server -- result-id ) make-result "http://www.speedtest.net/api/api.php" <post-request> [ [ "http://c.speedtest.net/flash/speedtest.swf" "referer" ] dip header>> set-at ] keep http-request nip query>assoc "resultid" of ;
Speedtest
Putting this all together, we can run the Speedtest, submit the results, then display the test results as an image.
: speedtest ( -- ) run-speedtest submit-result "Share results: " write "http://www.speedtest.net/result/%s.png" sprintf [ dup >url write-object nl ] [ http-image. ] bi ;
Some things that I would like to improve:
- The Speedtest configuration actually specifies the details of download and upload sizes, the amount of parallelism, and the duration of the test, we should use it.
- The http-get word needs an overall timeout so we can scale between very slow and very fast connection speeds.
- The Speedtest graphical result images are "retina" when viewed in the web browser, but are not when downloaded from Factor or wget.
- Factor needs an easier way to create a queue of work that is processed by several worker threads, for convenience I just used one of the concurrent combinators.
The code for this is on my GitHub.
I don't have a solution, but top 5 closest by geography isn't guaranteed to yield the closest server by actual internet topology. For example, from Tel Aviv, Cairo is 230ms away and Rome is only 100ms away.
ReplyDelete@Zeev, Ahh, good point. I'm not sure how the Speedtest.net flash app handles that.
ReplyDeleteThere's a lot of existing solutions dating back at least several years. The best use of this is to actually fake the results, so you appear to have a much higher bandwidth. Some site require proof of adequate bandwidth speed before letting you join (e.g. file sharing).
ReplyDeleteI think Speedtest gives an arrangement of accessible servers everywhere throughout the world that could be utilized for testing, returned as XML. In the wake of parsing the XML record, we utilize an utility system to concentrate traits for every server into an exhibit. You have shared what a superb and informative article.
ReplyDeleteTest Your Internet Speed