Added optional telemetry + example; Updated documentation; Minor changes and fixes

This commit is contained in:
dosse91 2017-08-24 15:28:28 +02:00
parent 6e1a199a31
commit ca779a5ae2
7 changed files with 319 additions and 23 deletions

View file

@ -10,9 +10,16 @@ This is a very lightweight Speedtest implemented in Javascript, using XMLHttpReq
## Compatibility
Only modern browsers are supported (IE11, latest Edge, latest Chrome, latest Firefox, latest Safari)
## Features
* Download
* Upload
* Ping
* Jitter
* IP Address
* Telemetry (optional)
## Requirements
- A reasonably fast web server. PHP is optional but recommended (see doc.md for details)
- Some way to generate garbage data (PHP script included, see doc.md for other solutions)
- A reasonably fast web server with PHP (see doc.md for details and use without PHP)
- Your server must accept large POST requests (up to 20 Megabytes), otherwise the upload test will fail
- It's also better if your server does not use compression, but it's not mandatory

50
doc.md
View file

@ -1,7 +1,7 @@
# HTML5 Speedtest
> by Federico Dossena
> Version 4.2.9, July 19 2017
> Version 4.3, August 24 2017
> [https://github.com/adolfintel/speedtest/](https://github.com/adolfintel/speedtest/)
@ -35,14 +35,16 @@ To install the test on your server, upload the following files:
* `garbage.php`
* `getIP.php`
* `empty.php`
* one of the examples
You may also want to upload one of the examples to test it.
Later we'll see how to use the test without PHP.
Later we'll see how to use the test without PHP, and how to configure the telemetry if you want to use it.
__Important:__ keep all the files together; all paths are relative to the js file
## Usage
You can modify one of the examples or start from scratch. If you are just editing one of the example, skip to the "test parameters" section.
To run the test, you need to do 3 things:
* Create the worker
@ -246,15 +248,49 @@ You need to start the test with your replacements like this:
```js
w.postMessage('start {"url_dl": "newGarbageURL", "url_ul": "newEmptyURL", "url_ping": "newEmptyURL", "url_getIp": "newIpURL"}')
```
## Telemetry
Telemetry currently requires PHP and MySQL.
To set up the telemetry, we need to do 4 things:
* copy `telemetry.php`
* edit `telemetry.php` to add your database access credentials
* create the database
* enable telemetry
### Creating the database
At the moment, only MySQL is supported.
Log into your database using phpMyAdmin or a similar software and import `telemetry.sql` into an empty database.
If you see a table called `speedtest_users`, empty, you did it right.
### Configuring `telemetry.php`
Open telemetry.php with notepad or a similar text editor, and insert your database access credentials
```
$MySql_username="USERNAME"; //your database username
$MySql_password="PASSWORD"; //your database password
$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost\
$MySql_databasename="DB_NAME"; //the name of the database where you loaded telemetry.sql
```
### Enabling telemetry
Edit your test page; where you start the worker, you need to specify the `telemetry_level`.
There are 3 levels:
* `none`: telemetry is disabled (default)
* `basic`: telemetry collects IP, User Agent, Preferred language, Test results
* `full`: same as above, but also collects a log (10-150 Kb each, not recommended)
Example:
```
w.postMessage('start {"telemetry_level":"basic"}')
```
Also, see example8_telemetry.html
### See the results
At the moment there is no front-end to see the telemetry data; you can connect to the database and see the collected results in the `speedtest_users` table.
## Known bugs and limitations
* The ping/jitter test is measured by seeing how long it takes for an empty XHR to complete. It is not an acutal ICMP ping
* __Chrome:__ high CPU usage from XHR requests with very fast connections (like gigabit).
For this reason, the test may report inaccurate results if your CPU is too slow. (Does not affect most computers)
* __IE11:__ the upload test is not precise on very fast connections
* __IE11, Edge:__ the upload test is not precise on very fast connections
* __IE11:__ the upload test may not work over HTTPS
* __Safari:__ works, but needs more testing and tweaking for very fast connections
* __Firefox:__ on some Linux systems with hardware acceleration turned off, the page rendering makes the browser lag, reducing the accuracy of the ping/jitter test
## Making changes

105
example8_telemetry.html Normal file
View file

@ -0,0 +1,105 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<title>Speedtest</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet" />
<style type="text/css">
.st-block {
text-align: center;
}
.st-btn {
margin-top: -0.5rem;
margin-left: 1.5rem;
}
.st-value>span:empty::before {
content: "0.00";
color: #636c72;
}
#st-ip:empty::before {
content: "___.___.___.___";
color: #636c72;
}
</style>
</head>
<body class="my-4">
<div class="container">
<div class="row">
<div class="col-sm-12 mb-3">
<p class="h1">
Speedtest
<button id="st-start" class="btn btn-outline-primary st-btn" onclick="startTest()">Start</button>
<button id="st-stop" class="btn btn-danger st-btn" onclick="stopTest()" hidden="true">Stop</button>
<p>Basic telemetry is active; results will be saved in your database. If the results don't appear, check the settings in telemetry.php</p>
</p>
<p class="lead">
Your IP: <span id="st-ip"></span>
</p>
</div>
<div class="col-lg-3 col-md-6 mb-3 st-block">
<h3>Download</h3>
<p class="display-4 st-value"><span id="st-download"></span></p>
<p class="lead">Mbit/s</p>
</div>
<div class="col-lg-3 col-md-6 mb-3 st-block">
<h3>Upload</h3>
<p class="display-4 st-value"><span id="st-upload"></span></p>
<p class="lead">Mbit/s</p>
</div>
<div class="col-lg-3 col-md-6 mb-3 st-block">
<h3>Ping</h3>
<p class="display-4 st-value"><span id="st-ping"></span></p>
<p class="lead">ms</p>
</div>
<div class="col-lg-3 col-md-6 mb-3 st-block">
<h3>Jitter</h3>
<p class="display-4 st-value"><span id="st-jitter"></span></p>
<p class="lead">ms</p>
</div>
</div>
</div>
<script type="text/javascript">
var worker = null
function startTest() {
document.getElementById('st-start').hidden = true
document.getElementById('st-stop').hidden = false
worker = new Worker('speedtest_worker.js')
var interval = setInterval(function () { worker.postMessage('status') }, 100)
worker.onmessage = function (event) {
var download = document.getElementById('st-download')
var upload = document.getElementById('st-upload')
var ping = document.getElementById('st-ping')
var jitter = document.getElementById('st-jitter')
var ip = document.getElementById('st-ip')
var data = event.data.split(';')
var status = Number(data[0])
if (status >= 4) {
clearInterval(interval)
document.getElementById('st-start').hidden = false
document.getElementById('st-stop').hidden = true
w = null
}
if (status === 5) {
// speedtest cancelled, clear output data
data = []
}
download.textContent = (status==1&&data[1]==0)?"Starting":data[1]
upload.textContent = (status==3&&data[2]==0)?"Starting":data[2]
ping.textContent = data[3]
ip.textContent = data[4]
jitter.textContent = data[5]
}
worker.postMessage('start {"telemetry_level":"basic"}')
}
function stopTest() {
if (worker) worker.postMessage('abort')
}
</script>
</body>
</html>

View file

@ -1,5 +1,5 @@
/*
HTML5 Speedtest v4.2.8
HTML5 Speedtest v4.3
by Federico Dossena
https://github.com/adolfintel/speedtest/
GNU LGPLv3 License
@ -13,6 +13,10 @@ var pingStatus = '' // ping in milliseconds with 2 decimal digits
var jitterStatus = '' // jitter in milliseconds with 2 decimal digits
var clientIp = '' // client's IP address as reported by getIP.php
var log="" //telemetry log
function tlog(s){log+=Date.now()+": "+s+"\n"}
function twarn(s){log+=Date.now()+" WARN: "+s+"\n"; console.warn(s);}
// test settings. can be overridden by sending specific values with the start command
var settings = {
time_ul: 15, // duration of upload test in seconds
@ -30,7 +34,9 @@ var settings = {
xhr_dlUseBlob: false, // if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream)
garbagePhp_chunkSize: 20, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
overheadCompensationFactor: 1048576/925000 //compensation for HTTP+TCP+IP+ETH overhead. 925000 is how much data is actually carried over 1048576 (1mb) bytes downloaded/uploaded. This default value assumes HTTP+TCP+IPv4+ETH with typical MTUs over the Internet. You may want to change this if you're going through your local network with a different MTU or if you're going over IPv6 (see doc.md for some other values)
overheadCompensationFactor: 1048576/925000, //compensation for HTTP+TCP+IP+ETH overhead. 925000 is how much data is actually carried over 1048576 (1mb) bytes downloaded/uploaded. This default value assumes HTTP+TCP+IPv4+ETH with typical MTUs over the Internet. You may want to change this if you're going through your local network with a different MTU or if you're going over IPv6 (see doc.md for some other values)
telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results+log)
url_telemetry: 'telemetry.php' // path to the script that adds telemetry data to the database
}
var xhr = null // array of currently active xhr requests
@ -88,13 +94,13 @@ this.addEventListener('message', function (e) {
if (/Edge.(\d+\.\d+)/i.test(ua)) {
// edge more precise with 3 download streams
settings.xhr_dlMultistream = 3
settings.forceIE11Workaround = true //Edge 15 introduced a bug that causes onprogress events to not get fired, so for Edge, we have to use the "small chunks" workaround that reduces accuracy
}
if (/Chrome.(\d+)/i.test(ua) && (!!self.fetch)) {
// chrome more precise with 5 streams
settings.xhr_dlMultistream = 5
}
}
if (typeof s.count_ping !== 'undefined') settings.count_ping = s.count_ping // number of pings for ping test
if (typeof s.xhr_dlMultistream !== 'undefined') settings.xhr_dlMultistream = s.xhr_dlMultistream // number of download streams
if (typeof s.xhr_ulMultistream !== 'undefined') settings.xhr_ulMultistream = s.xhr_ulMultistream // number of upload streams
@ -104,19 +110,24 @@ this.addEventListener('message', function (e) {
if (typeof s.time_dlGraceTime !== 'undefined') settings.time_dlGraceTime = s.time_dlGraceTime // dl test grace time before measuring
if (typeof s.time_ulGraceTime !== 'undefined') settings.time_ulGraceTime = s.time_ulGraceTime // ul test grace time before measuring
if (typeof s.overheadCompensationFactor !== 'undefined') settings.overheadCompensationFactor = s.overheadCompensationFactor //custom overhead compensation factor (default assumes HTTP+TCP+IP+ETH with typical MTUs)
} catch (e) { console.warn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e) }
if (typeof s.telemetry_level !== 'undefined') settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : 0; // telemetry level
if (typeof s.url_telemetry !== 'undefined') settings.url_telemetry = s.url_telemetry // url to telemetry.php
} catch (e) { twarn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e) }
// run the tests
console.log(settings)
getIp(function () { dlTest(function () { testStatus = 2; pingTest(function () { testStatus = 3; ulTest(function () { testStatus = 4 }) }) }) })
tlog(JSON.stringify(settings))
getIp(function () { dlTest(function () { testStatus = 2; pingTest(function () { testStatus = 3; ulTest(function () { testStatus = 4; sendTelemetry() }) }) }) })
}
if (params[0] === 'abort') { // abort command
tlog("manually aborted");
clearRequests() // stop all xhr activity
if (interval) clearInterval(interval) // clear timer if present
testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
if (settings.telemetry_level > 1) sendTelemetry()
testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
}
})
// stops all XHR activity, aggressively
function clearRequests () {
tlog("stopping pending XHRs");
if (xhr) {
for (var i = 0; i < xhr.length; i++) {
if (useFetchAPI) try { xhr[i].cancelRequested = true } catch (e) { }
@ -130,13 +141,16 @@ function clearRequests () {
}
// gets client's IP using url_getIp, then calls the done function
function getIp (done) {
tlog("getIp");
if (settings.url_getIp == "-1") {done(); return}
xhr = new XMLHttpRequest()
xhr.onload = function () {
tlog("IP: "+xhr.responseText);
clientIp = xhr.responseText
done()
}
xhr.onerror = function () {
tlog("getIp failed");
done()
}
xhr.open('GET', settings.url_getIp + url_sep(settings.url_getIp) + 'r=' + Math.random(), true)
@ -145,6 +159,7 @@ function getIp (done) {
// download test, calls done function when it's over
var dlCalled = false // used to prevent multiple accidental calls to dlTest
function dlTest (done) {
tlog("dlTest")
if (dlCalled) return; else dlCalled = true // dlTest already called?
if (settings.url_dl == "-1") {done(); return}
var totLoaded = 0.0, // total number of loaded bytes
@ -156,10 +171,12 @@ function dlTest (done) {
var testStream = function (i, delay) {
setTimeout(function () {
if (testStatus !== 1) return // delayed stream ended up starting after the end of the download test
tlog("dl test stream started "+i+" "+delay)
var prevLoaded = 0 // number of bytes loaded last time onprogress was called
var x = new XMLHttpRequest()
xhr[i] = x
xhr[i].onprogress = function (event) {
tlog("dl stream progress event "+i+" "+event.loaded);
if (testStatus !== 1) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the download test
// progress event, add number of new loaded bytes to totLoaded
var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
@ -169,11 +186,13 @@ function dlTest (done) {
}.bind(this)
xhr[i].onload = function () {
// the large file has been loaded entirely, start again
tlog("dl stream finished "+i)
try { xhr[i].abort() } catch (e) { } // reset the stream data to empty ram
testStream(i, 0)
}.bind(this)
xhr[i].onerror = function () {
// error
tlog("dl stream failed "+i);
if (settings.xhr_ignoreErrors === 0) failed=true //abort
try { xhr[i].abort() } catch (e) { }
delete (xhr[i])
@ -191,6 +210,7 @@ function dlTest (done) {
}
// every 200ms, update dlStatus
interval = setInterval(function () {
tlog("DL: "+dlStatus+(graceTimeDone?"":" (in grace time)"))
var t = new Date().getTime() - startT
if (t < 200) return
if (!graceTimeDone){
@ -208,6 +228,7 @@ function dlTest (done) {
if (failed || isNaN(dlStatus)) dlStatus = 'Fail'
clearRequests()
clearInterval(interval)
tlog("dlTest finished "+dlStatus)
done()
}
}
@ -227,6 +248,7 @@ reqsmall.push(r)
reqsmall = new Blob(reqsmall)
var ulCalled = false // used to prevent multiple accidental calls to ulTest
function ulTest (done) {
tlog("ulTest");
if (ulCalled) return; else ulCalled = true // ulTest already called?
if (settings.url_ul == "-1") {done(); return}
var totLoaded = 0.0, // total number of transmitted bytes
@ -238,24 +260,29 @@ function ulTest (done) {
var testStream = function (i, delay) {
setTimeout(function () {
if (testStatus !== 3) return // delayed stream ended up starting after the end of the upload test
tlog("ul test stream started "+i+" "+delay)
var prevLoaded = 0 // number of bytes transmitted last time onprogress was called
var x = new XMLHttpRequest()
xhr[i] = x
var ie11workaround
try {
xhr[i].upload.onprogress
ie11workaround = false
} catch (e) {
ie11workaround = true
if (settings.forceIE11Workaround) ie11workaround = true; else {
try {
xhr[i].upload.onprogress
ie11workaround = false
} catch (e) {
ie11workaround = true
}
}
if (ie11workaround) {
// IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
xhr[i].onload = function () {
tlog("ul stream progress event (ie11wa)")
totLoaded += 262144
testStream(i, 0)
}
xhr[i].onerror = function () {
// error, abort
tlog("ul stream failed (ie11wa)")
if (settings.xhr_ignoreErrors === 0) failed = true //abort
try { xhr[i].abort() } catch (e) { }
delete (xhr[i])
@ -267,6 +294,7 @@ function ulTest (done) {
} else {
// REGULAR version, no workaround
xhr[i].upload.onprogress = function (event) {
tlog("ul stream progress event "+i+" "+event.loaded);
if (testStatus !== 3) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the upload test
// progress event, add number of new loaded bytes to totLoaded
var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
@ -276,9 +304,11 @@ function ulTest (done) {
}.bind(this)
xhr[i].upload.onload = function () {
// this stream sent all the garbage data, start again
tlog("ul stream finished "+i);
testStream(i, 0)
}.bind(this)
xhr[i].upload.onerror = function () {
tlog("ul stream failed "+i);
if (settings.xhr_ignoreErrors === 0) failed=true //abort
try { xhr[i].abort() } catch (e) { }
delete (xhr[i])
@ -297,6 +327,7 @@ function ulTest (done) {
}
// every 200ms, update ulStatus
interval = setInterval(function () {
tlog("UL: "+ulStatus+(graceTimeDone?"":" (in grace time)"))
var t = new Date().getTime() - startT
if (t < 200) return
if (!graceTimeDone){
@ -314,6 +345,7 @@ function ulTest (done) {
if (failed || isNaN(ulStatus)) ulStatus = 'Fail'
clearRequests()
clearInterval(interval)
tlog("ulTest finished "+ulStatus)
done()
}
}
@ -322,6 +354,7 @@ function ulTest (done) {
// ping+jitter test, function done is called when it's over
var ptCalled = false // used to prevent multiple accidental calls to pingTest
function pingTest (done) {
tlog("pingTest");
if (ptCalled) return; else ptCalled = true // pingTest already called?
if (settings.url_ping == "-1") {done(); return}
var prevT = null // last time a pong was received
@ -332,10 +365,12 @@ function pingTest (done) {
xhr = []
// ping function
var doPing = function () {
tlog("ping");
prevT = new Date().getTime()
xhr[0] = new XMLHttpRequest()
xhr[0].onload = function () {
// pong
tlog("pong");
if (i === 0) {
prevT = new Date().getTime() // first pong
} else {
@ -350,10 +385,12 @@ function pingTest (done) {
pingStatus = ping.toFixed(2)
jitterStatus = jitter.toFixed(2)
i++
tlog("PING: "+pingStatus+" JITTER: "+jitterStatus)
if (i < settings.count_ping) doPing(); else done() // more pings to do?
}.bind(this)
xhr[0].onerror = function () {
// a ping failed, cancel test
tlog("ping failed");
if (settings.xhr_ignoreErrors === 0) { //abort
pingStatus = 'Fail'
jitterStatus = 'Fail'
@ -361,7 +398,6 @@ function pingTest (done) {
done()
}
if (settings.xhr_ignoreErrors === 1) doPing() //retry ping
if (settings.xhr_ignoreErrors === 2){ //ignore failed ping
i++
if (i < settings.count_ping) doPing(); else done() // more pings to do?
@ -373,3 +409,26 @@ function pingTest (done) {
}.bind(this)
doPing() // start first ping
}
// telemetry
function sendTelemetry(){
if (settings.telemetry_level < 1) return
xhr = new XMLHttpRequest();
xhr.onload = function () { console.log("TELEMETRY OL "+xhr.responseText) }
xhr.onerror = function () { console.log("TELEMETRY ERROR "+xhr) }
xhr.open('POST', settings.url_telemetry+"?r="+Math.random(), true);
try{
var fd = new FormData()
fd.append('dl', dlStatus)
fd.append('ul', ulStatus)
fd.append('ping', pingStatus)
fd.append('jitter', jitterStatus)
fd.append('log', settings.telemetry_level>1?log:"")
xhr.send(fd)
}catch(ex){
var postData = "dl="+encodeURIComponent(dlStatus)+"&ul="+encodeURIComponent(ulStatus)+"&ping="+encodeURIComponent(pingStatus)+"&jitter="+encodeURIComponent(jitterStatus)+"&log="+encodeURIComponent(settings.telemetry_level>1?log:"")
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
xhr.send(postData)
}
}

File diff suppressed because one or more lines are too long

23
telemetry.php Normal file
View file

@ -0,0 +1,23 @@
<?php
$MySql_username="USERNAME";
$MySql_password="PASSWORD";
$MySql_hostname="DB_HOSTNAME";
$MySql_databasename="DB_NAME";
$ip=($_SERVER['REMOTE_ADDR']);
$ua=($_SERVER['HTTP_USER_AGENT']);
$lang=($_SERVER['HTTP_ACCEPT_LANGUAGE']);
$dl=($_POST["dl"]);
$ul=($_POST["ul"]);
$ping=($_POST["ping"]);
$jitter=($_POST["jitter"]);
$log=($_POST["log"]);
$conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename) or die("1");
$stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?)") or die("2");
$stmt->bind_param("ssssssss",$ip,$ua,$lang,$dl,$ul,$ping,$jitter,$log) or die("3");
$stmt->execute() or die("4");
$stmt->close() or die("5");
$conn->close() or die("6");
?>

66
telemetry.sql Normal file
View file

@ -0,0 +1,66 @@
-- phpMyAdmin SQL Dump
-- version 4.7.0
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Generation Time: Aug 24, 2017 at 02:16 PM
-- Server version: 10.1.25-MariaDB
-- PHP Version: 7.1.7
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `speedtest_telemetry`
--
-- --------------------------------------------------------
--
-- Table structure for table `speedtest_users`
--
CREATE TABLE `speedtest_users` (
`id` int(11) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` text NOT NULL,
`ua` text NOT NULL,
`lang` text NOT NULL,
`dl` text,
`ul` text,
`ping` text,
`jitter` text,
`log` longtext
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `speedtest_users`
--
ALTER TABLE `speedtest_users`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `speedtest_users`
--
ALTER TABLE `speedtest_users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;