Wie ermittelt man eigentlich aus dem Browser heraus, wie schnell gerade das WLAN ist? Diese Frage stellte sich uns, bei dem Versuch, über eine Webseite den Nutzern die Möglichkeit einzuräumen, uns Feedback zur FeM-WLAN Qualität und -Empfang zu geben.
Im Wesentlichen gibt es da drei Möglichkeiten, dies mit JavaScript zu machen.
JavaScript bietet sich an, da mit ihm relativ kurze Zeiten (ms) gut gemessen werden können. Und kurze Zeiten bedeuten, dass eine Datenmenge zum Test verwendet werden kann, die für langsame Verbindungen nicht zu langsam (für den Nutzer) und für schnelle Verbindungen nicht zu schnell (wegen der Messgenauigkeit) übertragen wird. Wichtig ist natürlich auch, schlecht komprimierbare Daten zu verwenden, da sonst HTTP oder TLS Datenkompression die Ergebnisse verfälschen könnten.
- Bilder + onLoadEvent
- AJAX-Request + done-Event
- AJAX-Request + progress-Event
Grundsätzlich können alle Messungen als Differenzmessungen ausgelegt werden, um invariable Verarbeitungszeiten (DNS Lookup, Verbindungsaufbau, Übertragung der Header) raus zu rechnen. D.h. man vergleicht die für unterschiedliche Datenmengen benötigten Zeiten und ermittelt die Bandbreite als Quotient aus den Differenzen von Bandbreite und benötigter Zeit.
Bilder
Das Bild-Element wird mittels JavaScript dynamisch erzeugt. Der onLoad Event Handler vergleicht die aktuelle Zeit (als das Bild fertig geladen war) mit der Zeit, als das Bild-Element erzeugt wurde, und schon ist die benötigte Zeit klar. Die Bildgröße muss unabhängig davon bekannt sein.
Der Vorteil an diesem Ansatz ist die sehr gute Unterstützung durch die Browser - das onLoad-Event ist hinreichend alt und verbreitet.
Leider wird das onLoad Event (zumindest bei Firefox) erst ausgeführt, wenn das Bild auch verarbeitet wurde. Und das kann bei großen Bildern leider etwas dauern (300ms). Bei 500KB - was auch für langsame Verbindungen noch akzeptabel ist - geht es bei Gigabit hier aber um etwa 10ms - viel zu wenig bei 300ms Verarbeitungsjitter.
AJAX-Request + done-Event
Das Bild wird mittels JavaScript (AJAX) dynamisch geladen. Der done Event Handler vergleicht die aktuelle Zeit (als der Download fertig war) mit der Zeit, als das AJAX-Request erzeugt wurde, und schon ist die benötigte Zeit klar. Die Bildgröße kann aus dem Ergebnis des AJAX-Requests ermittelt werden.
Der Vorteil ist auch hier die relativ gute Browser-Unterstützung und die prinzipielle Unterstützung von Upload-Messungen. Leider liegen die damit bei Firefox + Gigabit-Netz gemessenen Zeiten relativ weit daneben (6ms vs. 24ms bei 500KB), sodass hier keine brauchbaren Ergebnisse gewonnen werden konnten.
AJAX-Request + progress-Event
Das Bild wird mittels JavaScript (AJAX) dynamisch geladen. Der progress Event Handler speichert bei jedem Aufruf die aktuelle Zeit und die aktuell schon übertragene Datenmenge. Aus der Differenz der Datenmenge und der Zeit aus unterschiedlichen Aufrufen zur gleichen Datenübertragung kann dann die Bandbreite ermittelt werden. Da die Anfrage auch (beispw. nach 1 Sekunde) abgebrochen werden kann, wäre es prinzipiell noch nicht einmal nötig, adaptiv die passende Datenmenge zu ermitteln.
Diese Variante wird zwar nur von relativ neuen Browsern unterstützt (FF ab 30, IE ab 10, Chrome in neu, usw.), dafür liefert sie aber auch relativ genaue Werte sowohl für den Upstream als auch den Downstream. Allerdings kann auch diese Variante natürlich nur messen, was der Browser überträgt - und das kann je nach Browser unterschiedlich performant implementiert sein.
Bei der Umsetzung der Upload-Messung muss beachtet werden, dass die Erzeugung hinreichend großer Datenmengen im Browser nur relativ unperformant realisiert werden kann. Einfacher ist es, die Daten vorher mittels AJAX herunter zu laden. Auch sollte die Größe adaptiv gewählt werden, da zumindest Chrome unter Linux (64Bit) schon bei relativ kleinen Datenmengen (20-40MByte) gelegentlich anfängt abzustürzen. Aber das wird Google hoffentlich bei Gelegenheit korrigieren.
Beispiel:
<script type="text/javascript">
var state;
var timestamp1;
var timestamp2;
var bytes1;
var bytes2;
var scale=10;
var mode = 0; /* 0: download, 1: upload */
var d = '';
var completed = 0;
function randid()
{
var id = "";
var possible = "0123456789-._ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for( var i=0; i < 21; i++ )
id += possible.charAt(Math.floor(Math.random() * possible.length));
return id;
}
function showText(txt,bold) {
elem = $('<tt></tt>');
elem.text(txt);
if (bold) elem.css('font-weight','bold');
elem.css('white-space','pre');
if (!bold)
elem.addClass('progress');
if (bold)
$('.progress').remove();
elem.append($('<br>'));
$('body').append(elem);
}
function handleOnLoad() {
if (completed) return;
completed = 1;
if (state <= 2 || (timestamp2 - timestamp1 < (mode ? 1000 : 1000))) {
scale *= 2;
if (new Date().getTime() - start < 30*1000)
setTimeout(speedtest, 1000);/* wait for the last request to complete */
else
alert('timeout');
return;
} else {
speed = (bytes2 - bytes1) / (timestamp2 - timestamp1);
speedInBitsPerSecond = speed * 8 * 1000;
txt = (mode ? 'upload' : 'download') + ': mega bits per second: ' + Math.round((speedInBitsPerSecond / 1024) / 1024)+'\n';
txt += ' bytes1: ' + Math.round(bytes1) + ' bytes2: ' + Math.round(bytes2)+"\n";
txt += ' timestamp1: ' + Math.round(timestamp1-start) + ' timestamp2: ' + Math.round(timestamp2-start)+"\n";
showText(txt,true);
if (mode == 0) {
scale--;
mode++;
speedtest();
}
}
}
function speedtest() {
showText('start another round');
state = 1;
url = "img.php?stage=2&id=" + randid();
completed = 0;
if (mode == 0) {
url += '&scale=' + scale;
d = '';
} else {
/* generating such a big string in JS is sooo slow */
durl = url + '&scale=' + scale;
showText('upload: fetch ' + (500 * 1024 * scale) + 'bytes');
$.ajax({ "url" : durl,
"async": false,
"dataType": "text",
}).done(function(x, stats, xhr) {
d = x;
showText('upload: fetch ok');
} );
showText('...done, got ' + d.length + ' bytes');
if (d.length == 0) return;
url += '&scale=0';
}
$.ajax({ "url": url,
type: "POST",
data: d,
processData: false,
xhr: function()
{
var xhr = new window.XMLHttpRequest();
//Upload progress
xhr.upload.addEventListener("progress", function(evt){
if (mode == 0) return;
showText('upload: sent ' + evt.loaded + ' of ' + evt.total + ' at ' + Math.round(new Date().getTime() - start), false);
state++;
if (state == 2) {
timestamp1 = new Date().getTime();
bytes1 = evt.loaded;
} else {
timestamp2 = new Date().getTime();
bytes2 = evt.loaded;
if (timestamp2 - timestamp1 > 1000) {
showText('upload: try to abort');
xhr.abort();
}
}
}, false);
//Download progress
xhr.addEventListener("progress", function(evt){
if (mode == 1) return;
showText('download: got ' + evt.loaded + ' of ' + evt.total + ' at ' + Math.round(new Date().getTime() - start), false);
state++;
if (state == 2) {
timestamp1 = new Date().getTime();
bytes1 = evt.loaded;
} else {
timestamp2 = new Date().getTime();
bytes2 = evt.loaded;
if (timestamp2 - timestamp1 > 1000) {
showText('download: try to abort');
xhr.abort();
}
}
}, false);
return xhr;
}
}).always(handleOnLoad);
}
var start = new Date().getTime();
</script>
<body onLoad="speedtest();">
</body>
Trackbacks