blob: 32888a7edbfe304faf00ad8de08092b61ce7ec67 [file] [log] [blame]
{/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/}
/**
* Creates an array of arrays of elements that return the same value from the
* given function.
*
* @template T
* @param {Array<T>} a The array to group.
* @param {(e: T) => string} f The grouping function; values in `a` that return
* the same value from this function will be grouped together.
* @returns {Array<Array<T>>} All of the elements of `a`, grouped by `f`.
*/
function groupBy(a, f) {
/** @type Record<string, Array<T>> */
const groups = {};
for (const element of a) {
const group = f(element);
if (group in groups) {
groups[group].push(element);
} else {
groups[group] = [element];
}
}
return Object.keys(groups).map(g=>groups[g]);
}
/**
* Gets the unique elements of an Array as a new Array. Does not modify Arrays
* in-place.
*
* @template T
* @param {Array<T>} a The array to be unique'd.
* @returns {Array<T>} A new array with all of the values in `a`, but without
* any duplicate values. Duplicates are determined with `===`.
*/
function unique(a) {
return Array.from(new Set(a));
}
var CdnTestForm = React.createClass({
getInitialState: function() {
return {
cdn: "none",
numHttp: 10,
numHttps: 10,
txCount: 1000,
connections: 10,
caFile: './ca.crt'
}
},
handleCdnChange: function(e) {
this.setState({cdn: e.target.value})
},
handleNumHttpChange: function(e) {
this.setState({numHttp: e.target.value})
},
handleNumHttpsChange: function(e) {
this.setState({numHttps: e.target.value})
},
handleTxCountChange: function(e) {
this.setState({txCount: e.target.value})
},
handleConnectionsChange: function(e) {
this.setState({connections: e.target.value})
},
handleCaFileChange: function(e) {
this.setState({caFile: e.target.value})
},
handleSubmit: function(e) {
e.preventDefault();
var cdn = this.state.cdn.trim();
var caFile = this.state.caFile.trim();
var txCount = this.state.txCount;
var connections = this.state.connections;
var numHttp = this.state.numHttp;
var numHttps = this.state.numHttps;
var testCdn = this.props.cdns.find(function (item) { return item.name == cdn});
var blah = testCdn.deliveryServices[0].exampleURLs[0];
blah = blah.substring(blah.indexOf(testCdn.name) + testCdn.name.length);
testCdn.name = testCdn.name + blah;
var httpDsList = testCdn.deliveryServices.filter(function (item) {
return item.type.toLowerCase().includes("dns") == false && item.protocol == 0;
});
var randomIndices = [];
for (var i = 0; i < numHttp; i++) {
randomIndices.push(Math.floor((Math.random() * httpDsList.length)))
}
var httpDeliveryServices = [];
for (var i = 0; i < randomIndices.length; i++) {
var u = httpDsList[randomIndices[i]].exampleURLs[0];
if (u.indexOf("edge") != -1) {
console.log("????! " + u);
}
var id = u.substring(u.indexOf("ccr") + 4, u.indexOf(testCdn.name) - 1);
httpDeliveryServices.push(id);
}
var httpsDsList = testCdn.deliveryServices.filter(function (item) {
return item.type.toLowerCase().includes("dns") == false && item.protocol > 0;
});
randomIndices = [];
for (var i = 0; i < numHttps; i++) {
randomIndices.push(Math.floor((Math.random() * httpsDsList.length)))
}
var httpsDeliveryServices = [];
for (var i = 0; i < randomIndices.length; i++) {
var u = httpsDsList[randomIndices[i]].exampleURLs[0];
var id = u.substring(u.indexOf("ccr") + 4, u.indexOf(testCdn.name) - 1);
httpsDeliveryServices.push(id);
}
this.props.onLoadTestSubmit({
cdn: testCdn.name,
httpDeliveryServices: httpDeliveryServices,
httpsDeliveryServices: httpsDeliveryServices,
txCount: txCount,
connections: connections,
caFile: caFile
});
},
render: function() {
var cdnOptions = this.props.cdns.map(function (cdn) {
return (
<option key={cdn.name} value={cdn.name}>{cdn.name}</option>
)
});
return (
<div className="cdnList">
<h3>3. Run Load Test</h3>
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="testCdn">CDN</label>
<select id="testCdnInput" className="form-control" value={this.state.cdn} onChange={this.handleCdnChange}>
{cdnOptions}
</select>
</div>
<div className="form-group">
<label htmlFor="numHttpInput"># of Http DS to exercise</label>
<input id="numHttpInput" type="text" className="form-control" value={this.state.numHttp} onChange={this.handleNumHttpChange} placeholder="0"/>
</div>
<div className="form-group">
<label htmlFor="numHttpsInput"># of HTTPS DS to exercise</label>
<input id="numHttpsInput" type="text" className="form-control" value={this.state.numHttps} onChange={this.handleNumHttpsChange}/>
</div>
<div className="form-group">
<label htmlFor="txCountInput"># of transactions per Delivery Services</label>
<input id="txCountInput" type="text" className="form-control" value={this.state.txCount} onChange={this.handleTxCountChange}/>
</div>
<div className="form-group">
<label htmlFor="connectionsInput"># of concurrent requests per Delivery Services</label>
<input id="connectionsInput" type="text" className="form-control" value={this.state.connections} onChange={this.handleConnectionsChange} />
</div>
<div className="form-group">
<label htmlFor="caFileInput">CA file path on server</label>
<input id="caFileInput" className="form-control" type="text" value={this.state.caFile} onChange={this.handleCaFileChange} placeholder="./ca.crt"/>
</div>
<button type="submit" className="btn btn-info">Run Test</button>
</form>
</div>
)
}
});
var CdnList = React.createClass({
render: function() {
var cdnRows = this.props.cdns.map(function (cdn) {
var name = cdn.name;
var httpDsList = cdn.deliveryServices.filter(function (ds) {
return ds.type.toLowerCase().includes("dns") == false;
});
var httpCount = httpDsList.filter(function (ds) { return ds.protocol == 0; }).length;
var httpsCount = httpDsList.filter(function (ds) { return ds.protocol > 0; }).length;
return (
<div key={cdn.name} className="row strong">
<div className="col-sm-4 ">{name}</div>
<div className="col-sm-4">{httpCount}</div>
<div className="col-sm-4">{httpsCount}</div>
</div>
)
});
return (
<div className="well">
<h4>Current Lab Data</h4>
<div className="container">
<div className="row">
<div className="col-sm-4">Name</div>
<div className="col-sm-4"># of Http Delivery Services</div>
<div className="col-sm-4"># of HTTPS delivery services</div>
</div>
{cdnRows}
</div>
</div>
)
}
});
var LabBox = React.createClass({
getInitialState: function() {
return {deliveryServices: [], opsHost: ""}
},
handleOpsHostChange: function(e) {
this.setState({opsHost: e.target.value})
},
loadResultsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadResultsFromServer();
setInterval(this.loadResultsFromServer, this.props.pollInterval);
},
handleOpsAuthSubmit: function(formData) {
console.log(formData);
var params = jQuery.param({
opsHost: formData.opsHost.trim()
});
this.opsHost = formData.opsHost.trim();
$.ajax({
url: "http://localhost:8888/api/4.0/user/login?" + params,
dataType: 'json',
type: 'POST',
crossDomain: true,
data: JSON.stringify(formData.credentials)//,
});
},
handleGetDeliveryServicesSubmit: function(data) {
return $.ajax({
url: "http://localhost:8888/api/4.0/deliveryservices.json?opsHost=" + this.opsHost,
dataType: 'json',
type: 'GET',
success: function(data) {
console.log("Got deliveryservices in ajax call");
this.deliveryServices = data.response;
}.bind(this)
});
},
handleLoadTestSubmit: function(formData) {
this.averageLatency = NaN;
this.minLatency = NaN;
this.maxLatency = NaN;
this.medianLatency = NaN;
this.startTime = moment();
this.runningTime = 0;
this.setState({latencies: []});
formData.txCount = parseInt(formData.txCount);
formData.connections = parseInt(formData.connections);
this.totalRequests = formData.txCount * (formData.httpDeliveryServices.length * formData.httpsDeliveryServices.length);
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: JSON.stringify(formData),
success: function(stuff) {
console.log("posted request to start load test");
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleSubmit: function(e) {
e.preventDefault();
console.log("handle submit");
var opsHost = this.state.opsHost.trim();
console.log("http://localhost:8888/api/4.0/deliveryservices.json?opsHost=" + opsHost);
$.ajax({
url: "http://localhost:8888/api/4.0/deliveryservices.json?opsHost=" + opsHost,
dataType: 'json',
type: 'GET',
success: function(data) {
console.log("Got deliveryservices in ajax call");
this.deliveryServices = data.response;
}.bind(this)
});
},
render: function() {
this.cdns = [];
if (this.deliveryServices != null) {
var cdnNames = unique(this.deliveryServices.map(function (item) { return item.cdnName}))
.filter(function(item) { return item.toLowerCase() != 'all'});
cdnNames.forEach(function (cdnName) {
var dsList = this.deliveryServices.filter(function (item) {
return item.cdnName.toLowerCase() == cdnName;
});
var cdn = {
name : cdnName,
deliveryServices: dsList
};
this.cdns.push(cdn);
}.bind(this));
}
if (this.state.data != null && this.state.data.length > 0) {
var sortedData = this.state.data.sort(function (result1, result2) {
return result1.latency - result2.latency;
});
this.minLatency = (sortedData[0].latency / 1000.000).toFixed(3);
this.maxLatency = (sortedData[sortedData.length - 1].latency / 1000.000).toFixed(3);
var s = 0.000;
for (var i = 0; i < sortedData.length; i++) {
var l = (sortedData[i].latency / 1000.0).toFixed(3);
if (this.medianLatency == NaN && (i > this.state.data.length / 2)) {
this.medianLatency = l;
}
s = +s + +l;
}
this.averageLatency = (s / this.state.data.length).toFixed(3);
if (this.state.data.length < this.totalRequests) {
this.runningTime = moment.duration(moment(new Date()).diff(this.startTime));
}
this.tps = NaN;
if (this.runningTime != null && this.state.data.length > 0) {
this.tps = (this.state.data.length / this.runningTime.asSeconds()).toFixed(3);
}
this.startTimePretty = "";
if (this.startTime != null) {
this.startTimePretty = this.startTime.format();
}
this.runningTimePretty = "";
if (this.runningTime != null) {
this.runningTimePretty = this.runningTime.asSeconds();
}
}
return (
<div className="well">
<LoginBox onOpsAuthSubmit={this.handleOpsAuthSubmit}/>
<h3>2. Fetch current data from lab</h3>
<form className="form-inline" onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="opsHostInput">Traffic Ops Host</label>
<input id="opsHostInput" className="form-control" type="text" value={this.state.opsHost} onChange={this.handleOpsHostChange}/>
</div>
<button type="submit" className="btn btn-info">Get Lab Data</button>
</form>
<br/>
<CdnList cdns={this.cdns}/>
<CdnTestForm cdns={this.cdns} onLoadTestSubmit={this.handleLoadTestSubmit}/>
<h1>Results</h1>
<dl className="dl-horizontal">
<dt>Start Time</dt><dd>{this.startTimePretty}</dd>
{/*<dt>Run Time</dt><dd>{this.runningTimePretty}</dd>*/}
{/*<dt>TPS</dt><dd>{this.tps}</dd>*/}
{/*<dt>Median Latency</dt><dd>{this.medianLatency}</dd>*/}
{/*<dt>Average Latency</dt><dd>{this.averageLatency}</dd>*/}
{/*<dt>Max Latency</dt><dd>{this.maxLatency}</dd>*/}
{/*<dt>Min Latency</dt><dd>{this.minLatency}</dd>*/}
</dl>
<div id="asdf"></div>
<ResultsList data={this.state.data}/>
</div>
);
}
});
var LoginBox = React.createClass({
getInitialState: function() {
return {opsUser: "guest", opsPassword: "foo", opsHost: "traffic-ops.example.com", opsCookie: "peanutbutter"}
},
handleOpsHostChange: function(e) {
this.setState({opsHost: e.target.value})
},
handleOpsUserChange: function(e) {
this.setState({opsUser: e.target.value})
},
handleOpsPasswordChange: function(e) {
this.setState({opsPassword: e.target.value})
},
handleSubmit: function(e) {
e.preventDefault();
var opsUser = this.state.opsUser.trim();
var opsPassword = this.state.opsPassword.trim();
var opsHost = this.state.opsHost.trim();
this.props.onOpsAuthSubmit({opsHost: opsHost, credentials: {u: opsUser, p: opsPassword}});
},
render: function() {
return(
<div className="loginBox">
<h3>1. Sign In</h3>
<form className="form-inline" onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="opsHost">Traffic Ops Host</label>
<input id="opsHost" className="form-control" type="text" value={this.state.opsHost} onChange={this.handleOpsHostChange}/>
</div>
<div className="form-group">
<label htmlFor="opsUser">User</label>
<input id="opsUser" className="form-control" type="text" value={this.state.opsUser} onChange={this.handleOpsUserChange}/>
</div>
<div className="form-group">
<label htmlFor="opsPassword">Password</label>
<input id="opsPassword" className="form-control" type="password" value={this.state.opsPassword} onChange={this.handleOpsPasswordChange} />
</div>
<button type="submit" className="btn btn-info">Authenticate</button>
</form>
</div>
)
}
});
var SubResult = React.createClass({
render: function() {
return (
<div className="subresult row">
<div className="col-sm-3">{this.props.subresult.requestTime}</div>
<div className="col-sm-3">{this.props.subresult.latency}</div>
<div className="col-sm-3">{this.props.subresult.error}</div>
<div className="col-sm-3">{this.props.subresult.httpStatus}</div>
</div>
)
}
});
var SubresultsList = React.createClass({
render: function() {
for (var i = 0; i < this.props.subresults.length; i++) {
this.props.subresults[i].id = i;
}
var resultNodes = this.props.subresults.map(function (subresult) {
return (
<SubResult key={subresult.id} subresult={subresult}/>
)
});
return (
<div className="SubResultsList">
{resultNodes}
</div>
)
}
});
var Result = React.createClass({
render: function () {
this.props.result.hid = "#" + this.props.result.id;
return (
<li className="list-group-item">
<h4>
<button type="button" className="btn btn-info" data-toggle="collapse" data-target={this.props.result.hid}>Details</button>
{this.props.result.avgLatency} mSec avg latency for {this.props.result[0].host}
</h4>
<div id={this.props.result.id} className="collapse">
<div className="row grid-table-header">
<div className="col-sm-3">Request Time</div>
<div className="col-sm-3">Latency uSec</div>
<div className="col-sm-3">Error</div>
<div className="col-sm-3">Http Status</div>
</div>
<SubresultsList subresults={this.props.result}/>
</div>
</li>
)
}
});
var ResultsList = React.createClass({
render: function() {
if (this.props.data == null) {
return (
<div>Waiting for results</div>
)
}
histogram(this.props.data.map(function (r, i) { return (r.latency / 1000.0).toFixed(3)}));
var groupedResults = groupBy(this.props.data, result => JSON.stringify([result.host]));
for (var i = 0; i < groupedResults.length; i++) {
groupedResults[i].host = groupedResults[i][0].host;
groupedResults[i].id = i;
var sum = 0;
for (var j = 0; j < groupedResults[i].length; j++) {
sum += groupedResults[i][j].latency;
}
groupedResults[i].avgLatency = ((sum / groupedResults[i].length) / 1000.0).toFixed(3);
}
var resultNodes = groupedResults.map(function(result) {
return (
<Result key={result.id} result={result}/>
)
});
return (
<ul className="list-group">
{resultNodes}
</ul>
)
}
});
ReactDOM.render(
<LabBox url="http://localhost:8888/loadtest" pollInterval={500} />,
document.getElementById('loadtest')
);
var histogram = function(latencies) {
var formatCount = d3.format(",.000f");
var margin = {top: 10, right: 30, bottom: 30, left: 30},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLog()
.domain([latencies[0], latencies[latencies.length -1]])
.rangeRound([0, width]);
var bins = d3.histogram()
.domain(x.domain())
.thresholds(x.ticks(20))
(latencies);
var y = d3.scaleLinear()
.domain([0, d3.max(bins, function(d) { return d.length; })])
.range([height, 0]);
d3.select("#asdf > svg").remove();
var svg = d3.select("#asdf").append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bar = svg.selectAll(".bar")
.data(bins)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d.x0) + "," + y(d.length) + ")"; });
bar.append("rect")
.attr("x", 1)
.attr("width", x(bins[0].x1) - x(bins[0].x0) - 1)
.attr("height", function(d) { return height - y(d.length); });
bar.append("text")
.attr("dy", "-0.75em")
.attr("y", 6)
.attr("x", (x(bins[0].x1) - x(bins[0].x0)) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d.length); });
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.format(",d")));
svg.append("text")
.attr("text-anchor", "middle")
.attr("transform", "translate(" + (width/4) + ",0)")
.text("Latency in msec");
svg.append("text")
.attr("text-anchor", "middle")
.attr("transform", "translate(0," + (height/2) + ")rotate(-90)")
.text("# of Requests");
};