You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

282 lines
7.9 KiB

import _ from 'lodash';
import runSimulation from './run-simulation';
import displayTimeline from './display-timeline';
import Config from '../../src/config';
import defaultTrace from './network_logs/default.js';
import { $ } from './fn';
import { appendToSimulations, summarizeSimulations } from './aggregate-simulations';
/* Data */
// Contains list of strings representing network traces uploaded
const networkTraces = [defaultTrace];
const defaultResults = {
startTimes: [],
rebufferRatios: [],
rebufferCounts: [],
indicatedBitrates: []
};
const createResults = () => _.cloneDeep(defaultResults);
const results = [];
/* DOM elements */
// a dynamic number of time-bandwidth pairs may be defined to drive the simulation
const networkTimeline = $('.network-timeline');
const local = $('#local');
const saveReport = $('#save-report');
const clearReport = $('#clear-report');
const runButton = $('#run-simulation');
const fuzzInputs = $('#fuzz-inputs');
const waitingNote = $('#running-simulation');
const finishedNote = $('#finished-simulation');
const secondaryInputs = [
$('#goal-buffer-length-secondary'),
$('#buffer-low-water-line-secondary')
// $('#bandwidth-variance-secondary')
];
waitingNote.style.display = 'none';
finishedNote.style.display = 'none';
/* Listeners */
// clear the file path to allow for reload
local.addEventListener('click', () => local.value = '');
local.addEventListener('change', function() {
// clear out previously loaded traces
networkTraces.length = 0;
});
saveReport.addEventListener('click', function(){
const rows = results.map(({ inputs, summary }) =>({ inputs, summary }));
const data = new Blob([JSON.stringify(rows, null, 2)], {type: 'text/plain'});
let textFile = window.URL.createObjectURL(data);
let link = document.createElement('a');
link.setAttribute('download', 'report.json');
link.href = textFile;
document.body.appendChild(link);
window.requestAnimationFrame(function () {
let event = new MouseEvent('click');
link.dispatchEvent(event);
document.body.removeChild(link);
window.URL.revokeObjectURL(textFile);
});
});
fuzzInputs.addEventListener('change', function() {
secondaryInputs.forEach((el) => {
el.style.display = fuzzInputs.checked ? 'block' : 'none';
});
});
runButton.addEventListener('click', function() {
// clear previous simulation before starting a new one
results.length = 0;
waitingNote.style.display = 'block';
finishedNote.style.display = 'none';
// Setup the simulation inputs
// [ [GoalBufferLength, BandwidthVariance], ... ]
const simulationInputs = setupSimulationInputs();
// This gets REALLY SLOW
const runs = simulationInputs.map((inputs, index) => {
return runSimulations(index, inputs);
});
Promise.all(runs).then(() => {
finishedNote.style.display = 'block';
waitingNote.style.display = 'none';
// If only one simulation was run over one trace, display the timeline graph of the
// results and add the text results to the display
if (results.length === 1 && networkTraces.length === 1) {
$('#result').style.display = 'none';
displayTimeline(results[0].error, results[0].raw_result);
// hide JSON summary if there's only one simulation run
} else {
$('#result').style.display = 'block';
$('#result').innerText = JSON.stringify(results);
}
});
});
/* Functions */
const loadFiles = () => {
const files = local.files;
// do nothing if no file was chosen or if we already have networkTraces loaded
// networkTraces is cleared whenever the FilePicker changes, so the having a trace
// means we've already loaded a file
if (!files || networkTraces.length) {
return Promise.resolve();
}
const filePromises = Array.from(files).map((file) => readFile(file));
return Promise.all(filePromises);
};
const readFile = function(file) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onloadend = function() {
networkTraces.push(reader.result);
resolve();
};
reader.readAsText(file);
})
};
const setupSimulationInputs = function() {
let result = [];
// First create arrays for each fuzzed input
const fuzz = fuzzInputs.checked;
// Goal Buffer Length
const GBLValues = fuzzInput('goal-buffer-length', fuzz, 1, 1);
// Buffer Low-Water Line
const BLWLValues = fuzzInput('buffer-low-water-line', fuzz, 0, 1);
// Bandwidth Variance
const BVValues = fuzzInput('bandwidth-variance', false, 0.1, 0.1);
const values = [GBLValues, BLWLValues, BVValues];
const merger = function(arr, type) {
for (let i = 0, l = values[type].length; i < l; i++) {
const clone = arr.slice(0);
clone.push(values[type][i]);
if (type === values.length - 1) {
result.push(clone);
} else {
merger(clone, type + 1);
}
}
};
merger([], 0);
return result;
};
const fuzzInput = function(input, useMax, inputMin, inputStepMin) {
const result = [];
let value = Math.max(inputMin, Number($(`#${input}`).value));
let step = Math.max(inputStepMin, Number($(`#${input}-step`).value));
let max = value;
if (useMax) {
max = Math.max(max, Number($(`#${input}-max`).value));
}
for (; value <= max; value += step) {
result.push(value);
}
return result;
}
// collect the simulation parameters
const parameters = function(trace, inputs) {
let networkTrace = trace
.trim()
.split('\n')
.map((line) => line.split(' ').slice(-2).map(Number));
let playlists = $('#bitrates').value
.trim()
.split('\n')
.map((line) => {
let t = line.split(/[,\s]+/).map(Number);
return [t[0], t[1] || t[0]];
});
let segments = {};
try {
segments = JSON.parse($('#segments').value);
} catch(e) {
console.log('Invalid JSON');
}
// inputs: [goalBufferLength, bufferLowWaterLine, bandwidthVariance]
return {
goalBufferLength: inputs[0],
bufferLowWaterLine: inputs[1],
bandwidthVariance: inputs[2],
playlists,
segments,
networkTrace
};
};
const simulationDone = function(promise, simulation, err, res) {
// create global summary if it doesn't exist
if (!results[simulation].results) {
results[simulation].results = createResults();
}
// recalculate summary with new result
results[simulation].results = appendToSimulations(results[simulation].results, res);
results[simulation].summary = summarizeSimulations(results[simulation].results);
// Store the raw results and error of the simulation
// This is only useful in the event that a single simulation was done over a single
// network trace, otherwise these values will only be valid for the last network trace
// used for the given simulation
results[simulation].raw_result = res;
results[simulation].error = err;
promise.resolve();
}
const runSimulations = (simulation, inputs) => loadFiles().then(() => {
// create the simulation object if it does not already exist
if (!results[simulation]) {
results[simulation] = {
inputs
};
}
// network traces are loaded into `networkTraces`
// now run the simulation through each one
const simulationPromises = networkTraces.map((trace) => {
return new Promise((resolve, reject) => {
runSimulation(parameters(trace, inputs), simulationDone.bind(null, { resolve, reject }, simulation));
});
});
return Promise.all(simulationPromises);
});
/* Run */
// Does this even do anything?
// apply any simulation parameters that were set in the fragment identifier
if (window.location.hash) {
// time periods are specified as t<seconds>=<bitrate>
// e.g. #t15=450560&t150=65530
let params = window.location.hash.substring(1)
.split('&')
.map(function(param) {
return ((/t(\d+)=(\d+)/i).exec(param) || [])
.map(window.parseFloat).slice(1);
}).filter(function(pair) {
return pair.length === 2;
});
networkTimeline.innerHTML = '';
}
// initially hide the JSON summary output
$('#result').style.display = 'none';
// runButton.click();