380 lines
12 KiB
JavaScript
380 lines
12 KiB
JavaScript
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2015-2021 (original work) Open Assessment Technologies SA ;
|
||
|
*/
|
||
|
define([
|
||
|
'lodash',
|
||
|
'i18n',
|
||
|
'async',
|
||
|
'context',
|
||
|
'taoClientDiagnostic/tools/getConfig',
|
||
|
'taoClientDiagnostic/tools/getLabels',
|
||
|
'taoClientDiagnostic/tools/stats',
|
||
|
'taoClientDiagnostic/tools/fixedDecimals',
|
||
|
'taoClientDiagnostic/tools/getStatus'
|
||
|
], function(_, __, async, context, getConfig, getLabels, stats, fixedDecimals, getStatus) {
|
||
|
'use strict';
|
||
|
|
||
|
/**
|
||
|
* A binary kilo bytes (KiB)
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
const _kilo = 1024;
|
||
|
|
||
|
/**
|
||
|
* A binary mega bytes (MiB)
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
const _mega = _kilo * _kilo;
|
||
|
|
||
|
/**
|
||
|
* Duration of one second (in milliseconds)
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
const _second = 1000;
|
||
|
|
||
|
/**
|
||
|
* A list of thresholds for bandwidth check
|
||
|
* @type {Array}
|
||
|
* @private
|
||
|
*/
|
||
|
const _thresholds = [
|
||
|
{
|
||
|
threshold: 0,
|
||
|
message: __('Low bandwidth'),
|
||
|
type: 'error'
|
||
|
},
|
||
|
{
|
||
|
threshold: 33,
|
||
|
message: __('Average bandwidth'),
|
||
|
type: 'warning'
|
||
|
},
|
||
|
{
|
||
|
threshold: 66,
|
||
|
message: __('Good bandwidth'),
|
||
|
type: 'success'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Default values for the bandwidth tester
|
||
|
* @type {object}
|
||
|
* @private
|
||
|
*/
|
||
|
const _defaults = {
|
||
|
id: 'bandwidth',
|
||
|
|
||
|
// The typical bandwidth needed for a test taker (Mbps)
|
||
|
unit: 0.16,
|
||
|
|
||
|
// The thresholds for optimal bandwidth
|
||
|
ideal: 45,
|
||
|
|
||
|
// Maximum number of test takers to display
|
||
|
max: 100,
|
||
|
|
||
|
// Lowest value that will be used in the global score computation
|
||
|
minimumGlobalPercentage: false,
|
||
|
|
||
|
// A list of thresholds for bandwidth check
|
||
|
feedbackThresholds: _thresholds,
|
||
|
|
||
|
fallbackThreshold: 0.2
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List of descriptors defining the data sets to download.
|
||
|
* - file : path of the file containing the test data
|
||
|
* - size : the given size of the file
|
||
|
* - timeout : the timeout for the download
|
||
|
* - threshold : a bandwidth threshold above which the data set can be downloaded to evaluate a more accurate value
|
||
|
* - nb : number of download iterations
|
||
|
* @type {object}
|
||
|
* @private
|
||
|
*/
|
||
|
const _downloadData = {
|
||
|
'10KB': {
|
||
|
id: '10KB',
|
||
|
file: 'data/bin10KB.data',
|
||
|
size: 10 * _kilo,
|
||
|
timeout: _second,
|
||
|
threshold: 0,
|
||
|
nb: 10
|
||
|
},
|
||
|
'100KB': {
|
||
|
id: '100KB',
|
||
|
file: 'data/bin100KB.data',
|
||
|
size: 100 * _kilo,
|
||
|
timeout: 2 * _second,
|
||
|
threshold: 0,
|
||
|
nb: 5
|
||
|
},
|
||
|
'1MB': {
|
||
|
id: '1MB',
|
||
|
file: 'data/bin1MB.data',
|
||
|
size: _mega,
|
||
|
timeout: 20 * _second,
|
||
|
threshold: 0,
|
||
|
nb: 3
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List of translated texts per level.
|
||
|
* The level is provided through the config as a numeric value, starting from 1.
|
||
|
* @type {object[]}
|
||
|
* @private
|
||
|
*/
|
||
|
const _messages = [
|
||
|
// level 1
|
||
|
{
|
||
|
title: __('Bandwidth'),
|
||
|
status: __('Checking the bandwidth...'),
|
||
|
legend: __('Number of simultaneous test takers the connection can handle'),
|
||
|
bandwidthMin: __('Minimum bandwidth'),
|
||
|
bandwidthMax: __('Maximum bandwidth'),
|
||
|
bandwidthAverage: __('Average bandwidth')
|
||
|
},
|
||
|
// level 2
|
||
|
{
|
||
|
title: __('Media intensive bandwidth'),
|
||
|
status: __('Checking the media intensive bandwidth...'),
|
||
|
legend: __('Number of simultaneous test takers the connection can handle with media intensive'),
|
||
|
bandwidthMin: __('Minimum intensive bandwidth'),
|
||
|
bandwidthMax: __('Maximum intensive bandwidth'),
|
||
|
bandwidthAverage: __('Average intensive bandwidth')
|
||
|
}
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Download a data set as described by the provided descriptor and compute the duration.
|
||
|
* @param {object} data The data set descriptor to use for download
|
||
|
* @param {Function} cb A callback function called at the end of the download.
|
||
|
* This callback is also called if a timeout breaks the download;
|
||
|
* @private
|
||
|
*/
|
||
|
function download(data, cb) {
|
||
|
if (data.threshold && this.bandwidth < data.threshold) {
|
||
|
return cb('threshold');
|
||
|
}
|
||
|
|
||
|
const start = window.performance.now();
|
||
|
const url = `${context['root_url']}taoClientDiagnostic/views/js/tools/bandwidth/${data.file}?${Date.now()}`;
|
||
|
const timeoutId = window.setTimeout(cb, data.timeout, 'timeout');
|
||
|
const request = new XMLHttpRequest();
|
||
|
request.open('GET', url, true);
|
||
|
request.setRequestHeader('Accept', 'application/octet-stream');
|
||
|
|
||
|
request.onload = () => {
|
||
|
const end = window.performance.now();
|
||
|
clearTimeout(timeoutId);
|
||
|
|
||
|
const duration = end - start;
|
||
|
const bytes = data.size;
|
||
|
const seconds = duration / _second;
|
||
|
|
||
|
// speed in Mbps
|
||
|
const speed = (bytes * 8) / seconds / _mega;
|
||
|
|
||
|
this.bandwidth = Math.max(this.bandwidth, speed);
|
||
|
|
||
|
return cb(null, {
|
||
|
id: data.id,
|
||
|
file: data.file,
|
||
|
size: data.size,
|
||
|
duration,
|
||
|
speed
|
||
|
});
|
||
|
};
|
||
|
request.onerror = err => {
|
||
|
clearTimeout(timeoutId);
|
||
|
cb(err);
|
||
|
};
|
||
|
|
||
|
request.onreadystatechange = () => {
|
||
|
if (request.readyState === 4 && request.status !== 200) {
|
||
|
clearTimeout(timeoutId);
|
||
|
cb(request.status);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
request.send();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Performs a bandwidth test by downloading a bunch of data sets with different sizes
|
||
|
*
|
||
|
* @param {object} config - Some optional configs
|
||
|
* @param {string} [config.id] - The identifier of the test
|
||
|
* @param {number} [config.unit] - The typical bandwidth needed for a test taker (Mbps)
|
||
|
* @param {number} [config.ideal] - The thresholds for optimal bandwidth
|
||
|
* @param {number} [config.max] - Maximum number of test takers to display
|
||
|
* @param {string} [config.level] - The intensity level of the test. It will aim which messages list to use.
|
||
|
* @returns {object}
|
||
|
*/
|
||
|
return function bandwidthTester(config) {
|
||
|
const initConfig = getConfig(config, _defaults);
|
||
|
const labels = getLabels(_messages, initConfig.level);
|
||
|
|
||
|
// override the feedback thresholds given by the config in case it is an empty array
|
||
|
if (_.isArray(initConfig.feedbackThresholds) && !initConfig.feedbackThresholds.length) {
|
||
|
initConfig.feedbackThresholds = _thresholds;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
/**
|
||
|
* Performs a bandwidth test, then call a function to provide the result
|
||
|
* @param {Function} done
|
||
|
*/
|
||
|
start(done) {
|
||
|
const tests = [];
|
||
|
|
||
|
_.forEach(_downloadData, data => {
|
||
|
const cb = download.bind(this, data);
|
||
|
let iterations = data.nb || 1;
|
||
|
while (iterations--) {
|
||
|
tests.push(cb);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.bandwidth = 0;
|
||
|
|
||
|
async.series(tests, (err, measures) => {
|
||
|
let duration = 0;
|
||
|
let size = 0;
|
||
|
const decimals = 2;
|
||
|
const resultsBySize = {};
|
||
|
|
||
|
function getValue(value) {
|
||
|
let speed = 0;
|
||
|
|
||
|
if (value) {
|
||
|
duration += value.duration;
|
||
|
size += value.size;
|
||
|
|
||
|
speed = value.speed;
|
||
|
value.speed = fixedDecimals(speed, decimals);
|
||
|
}
|
||
|
|
||
|
return speed;
|
||
|
}
|
||
|
|
||
|
if (err && !measures.length) {
|
||
|
//something went wrong
|
||
|
throw err;
|
||
|
}
|
||
|
const results = stats(measures, getValue, decimals);
|
||
|
|
||
|
_.forEach(_downloadData, (data, key) => {
|
||
|
resultsBySize[key] = stats(
|
||
|
_.filter(measures, o => o.id === key),
|
||
|
getValue,
|
||
|
decimals
|
||
|
);
|
||
|
});
|
||
|
|
||
|
results.duration = fixedDecimals(duration / _second, decimals);
|
||
|
results.size = size;
|
||
|
|
||
|
const summary = this.getSummary(results);
|
||
|
const status = this.getFeedback(results, resultsBySize);
|
||
|
|
||
|
done(status, summary, results);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the labels loaded for the tester
|
||
|
* @returns {object}
|
||
|
*/
|
||
|
get labels() {
|
||
|
return labels;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Builds the results summary
|
||
|
* @param {object} results
|
||
|
* @returns {object}
|
||
|
*/
|
||
|
getSummary(results) {
|
||
|
return {
|
||
|
bandwidthMin: { message: labels.bandwidthMin, value: results.min + ' Mbps' },
|
||
|
bandwidthMax: { message: labels.bandwidthMax, value: results.max + ' Mbps' },
|
||
|
bandwidthAverage: { message: labels.bandwidthAverage, value: results.average + ' Mbps' }
|
||
|
};
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the feedback status for the provided result value
|
||
|
* @param {object} result
|
||
|
* @param {number} result.max
|
||
|
* @param {number} result.min
|
||
|
* @param {number} result.average
|
||
|
* @param {object} resultsBySize - result statistics grouped by size
|
||
|
* @returns {object}
|
||
|
*/
|
||
|
getFeedback(result, resultsBySize) {
|
||
|
const avgResult = result.average;
|
||
|
const bandwidthUnit = initConfig.unit;
|
||
|
const threshold = initConfig.ideal;
|
||
|
const maxTestTakers = initConfig.max;
|
||
|
const max = threshold * bandwidthUnit;
|
||
|
const getStatusOptions = initConfig.minimumGlobalPercentage
|
||
|
? { minimumGlobalPercentage: initConfig.minimumGlobalPercentage }
|
||
|
: {};
|
||
|
const baseBandwidth = avgResult;
|
||
|
let stable = true;
|
||
|
|
||
|
_.forEach(resultsBySize, resultBySize => {
|
||
|
if (resultBySize.min / resultBySize.average < initConfig.fallbackThreshold) {
|
||
|
stable = false;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const status = getStatus((baseBandwidth / max) * 100, initConfig.feedbackThresholds, getStatusOptions);
|
||
|
|
||
|
let nb = Math.floor(baseBandwidth / bandwidthUnit);
|
||
|
|
||
|
if (nb > maxTestTakers) {
|
||
|
nb = '>' + maxTestTakers;
|
||
|
}
|
||
|
|
||
|
status.id = initConfig.id;
|
||
|
status.title = labels.title;
|
||
|
status.feedback.legend = labels.legend;
|
||
|
status.quality.label = nb;
|
||
|
|
||
|
if (!stable) {
|
||
|
status.feedback.type = 'warning';
|
||
|
status.feedback.message = __(
|
||
|
'Unstable bandwidth, temporary fluctuations in connection speed may affect test taker experience.'
|
||
|
);
|
||
|
status.feedback.legend = __('Simultaneous test takers under normal connection conditions.');
|
||
|
}
|
||
|
|
||
|
if (nb.toString().length > 2) {
|
||
|
status.quality.wide = true;
|
||
|
}
|
||
|
|
||
|
return status;
|
||
|
}
|
||
|
};
|
||
|
};
|
||
|
});
|