// benchmark.ts benchmarks the proxy. It should be running on localhost:9999
///
///
////
import request = require('request');
import util = require('util');
import Agent = require('../../../third_party/socks5-http-client/agent');
var shttpagent : Agent.Socks5ClientHttpAgent = require('socks5-http-client/lib/Agent');
export module Benchmark {
export class Bucket {
upperLimit : number;
count : number;
constructor(up : number, cnt : number) {
this.upperLimit = up;
this.count = cnt;
}
};
export class BasicStats {
max : number;
min : number;
mean : number;
median : number;
count: number;
constructor(values: number[]) {
this.min = values[0];
this.max = values[0];
var sum = 0;
var n = values.length;
this.count = n;
for (var i = 0; i < n; i++) {
sum += values[i];
if (values[i] < this.min) {
this.min = values[i];
}
if (values[i] > this.max) {
this.max = values[i];
}
}
this.median = this.calcMedian(values);
this.mean = sum / n;
}
private calcMedian(values : number[]) : number {
var sorted = values.sort();
var len = values.length;
if (len < 2) {
return values[0];
}
if (len % 2) {
// Odd number of elements, choose center.
return sorted[ Math.floor((len/2) + 1) ];
} else {
// Even number of elements, average two middle ones.
return (sorted[len/2] + sorted[(len/2)+1]) / 2;
}
}
private fmtNumber(n: number) : string {
if (n) {
return n.toFixed(2);
} else {
return "undef!";
}
}
public summary () : string {
return util.format("[total: %d, min: %d, med: %d, mean: %d, max: %d]",
this.count, this.min, this.fmtNumber(this.median),
this.fmtNumber(this.mean), this.max);
}
};
export class Histogram {
private buckets_ : Bucket[];
private count_ : number;
constructor(nbuckets:number, max : number) {
this.buckets_ = new Array();
this.count_ = 0;
var step = max / nbuckets;
for (var i = 0; i < nbuckets - 1; i++) {
this.buckets_[i] = new Bucket(step * i, 0);
}
this.buckets_[nbuckets - 1] = new Bucket(Number.MAX_VALUE, 0);
}
public addValue(num : number) : void {
// This isn't fast.
for (var i = 0;i < this.buckets_.length; i++) {
if (num < this.buckets_[i].upperLimit) {
this.buckets_[i].count++;
return;
}
}
}
public addValues(nums : number[]) : Histogram {
for (var i = 0; i < nums.length; i++) {
this.addValue(nums[i]);
}
return this;
}
public getValues() : Bucket[] {
return this.buckets_;
}
public getPoints() : number[][] {
var buckets = new Array();
for (var i = 0; i < this.buckets_.length; i++) {
var prev = 0;
if (i > 0) { prev = this.buckets_[i-1].upperLimit; }
buckets.push([prev, this.buckets_[i].count]);
}
return buckets;
}
};
// Request result. We separate out timeouts from the general
// class of failure, as it probably indicates a bug in the SUT's
// implementation.
export enum Result {
RES_SUCCESS,
RES_FAILURE,
RES_TIMEOUT
};
// A container for raw latency values for a single kind of test.
// Keeps separate value lists for each result.
export class DataVector {
public values : Array[];
constructor() {
this.values = [
new Array(), // RES_SUCCESS
new Array(), // RES_FAILURE
new Array(), // RES_TIMEOUT
];
}
addValue(latency: number, result: Result) {
this.values[result].push(latency);
}
addValues(latencies: number[], result: Result) {
for (var i = 0; i < latencies.length; i++) {
this.values[result].push(latencies[i]);
}
}
};
// A result for testing a single 'kind' of URL.
export class TestResult {
public requestSize : number;
public raw : DataVector;
public histogram : Histogram[];
constructor(size: number, successes: number[], failures: number[],
timeouts: number[], nbuckets: number, max: number) {
this.requestSize = size;
this.raw = new DataVector();
this.histogram = new Array();
var suc_hist = new Histogram(nbuckets, max);
var fail_hist = new Histogram(nbuckets, max);
var to_hist = new Histogram(nbuckets, max);
suc_hist.addValues(successes);
fail_hist.addValues(failures);
to_hist.addValues(timeouts);
this.histogram.push(suc_hist);
this.histogram.push(fail_hist);
this.histogram.push(to_hist);
this.raw.addValues(successes, Result.RES_SUCCESS);
this.raw.addValues(failures, Result.RES_FAILURE);
this.raw.addValues(timeouts, Result.RES_TIMEOUT);
}
};
// Data maintained about an in-flight HTTP request.
class Request {
public requestSize : number;
public requestSizeIndex : number;
public requestTime : number;
public requestNum : number;
public url : string
constructor(url : string, sz : number, idx: number, num : number) {
this.requestTime = Date.now();
this.requestSizeIndex = idx;
this.requestSize = sz;
this.url = url;
this.requestNum = num;
}
};
export class RequestManager {
// TODO: Size concurrency for underlying runtime.
private concurrency = 1;
private histoNumBuckets = 16;
private histoMax = 100;
private kTimeoutMS = 30000; // 30 sec timeout.
private kMaxTimeouts = 10; // max number of timeouts before
// aborting.
private kWatchdogInterval = 500; // check 2 times/sec.
private latencies_ : DataVector[];
private sizes_: number[];
private request_queue_ : number[];
private running_requests_ : Request[];
// When waiting for all concurrent requests to finish, keep
// count of how many already have.
// 'this.concurrency - this.finished_concurrent_requests_'
// is how many more we have to wait for.
private finished_concurrent_requests_ : number;
private result_callback_ : Function;
private timeout_count_ : number;
private request_counter_ : number;
private verbosity_: number;
constructor(sizes: number[]) {
this.latencies_ = new Array();
this.sizes_ = sizes;
this.request_queue_ = [];
this.request_counter_ = 0;
this.result_callback_ = null;
this.running_requests_ = new Array();
for (var i = 0; i < sizes.length; i++) {
this.latencies_.push(new DataVector);
}
}
public configureDefaults(conc: number,
nbuckets: number,
max: number,
verbosity: number,
max_timeouts: number) {
this.concurrency = conc;
this.histoNumBuckets = nbuckets;
this.histoMax = max;
this.verbosity_ = verbosity;
this.kMaxTimeouts = max_timeouts;
}
private finishRequest(requestIndex: number, err: any, response: any, body: any) {
var request_in_error : boolean = err != null;
var req = this.running_requests_[requestIndex];
if (req == null) {
console.log("Getting a result back for a request that no longer " +
"exists. Race between timeouts?");
return;
}
var result_time = Date.now();
var latency_ms = result_time - req.requestTime;
// first verify that the body is fully-formed
if (!request_in_error && (!body || !body.length || body.length != req.requestSize)) {
request_in_error = true;
}
if (req.requestTime < 0) {
this.latencies_[req.requestSizeIndex].addValue(this.kTimeoutMS,
Result.RES_TIMEOUT);
} else if (request_in_error) {
// TODO: Look up error codes for this.
this.latencies_[req.requestSizeIndex].addValue(latency_ms,
Result.RES_FAILURE);
var body_length = -1;
if (body) {
body_length = body.length;
}
console.log("--> finishRequest: got err: " + err + ", body length was "
+ body.length + ", wanted size: " + req.requestSize +
", on url " + req.url);
} else {
this.latencies_[req.requestSizeIndex].addValue(latency_ms,
Result.RES_SUCCESS);
if (this.verbosity_ > 0) {
process.stdout.write("[" + latency_ms + " ms]\t");
}
}
this.runATest(requestIndex);
}
public startRequest(requestIndex: number, sizeIndex: number) : void {
var size = this.sizes_[sizeIndex];
var self = this;
var url = 'http://localhost:8080/' + size;
this.request_counter_++;
this.running_requests_[requestIndex] = new Request(url, size, sizeIndex,
this.request_counter_);
request({
url: 'http://localhost:8080/' + size,
agent: new shttpagent({
socksHost: 'localhost',
socksPort: 9999
})
}, function (err, response, body) {
self.finishRequest(requestIndex, err, response, body);
});
}
public runATest(requestIndex: number) : void {
if (this.timeout_count_ > 0 && this.request_queue_.length > 0) {
var queue_head = this.request_queue_[0];
this.request_queue_.shift();
this.startRequest(requestIndex, queue_head);
} else if (this.finished_concurrent_requests_ < this.concurrency) {
this.finished_concurrent_requests_++;
this.running_requests_[requestIndex] = null;
}
if (this.finished_concurrent_requests_ == this.concurrency) {
if (this.verbosity_ > 0) {
console.log("\nTest run complete. Generating results");
}
var results = new Array();
for (var sz = 0; sz < this.sizes_.length; sz++) {
results.push(new TestResult(this.sizes_[sz],
this.latencies_[sz].values[0],
this.latencies_[sz].values[1],
this.latencies_[sz].values[2],
this.histoNumBuckets,
this.histoMax));
}
if (this.result_callback_ != null) {
var cb = this.result_callback_;
this.result_callback_ = null;
cb(results);
}
}
}
// Scan the running requests for anything that's timed out.
private watchDog() {
var now = Date.now();
var num_completed_requests = 0;
for (var r = 0; r < this.running_requests_.length; r++) {
var req = this.running_requests_[r];
if (req != null) {
if (now - req.requestTime > this.kWatchdogInterval) {
req.requestTime = -1; // mark as timed out
console.log("*ruff! Timing out request " + req.requestNum
+ " on slot " + r);
this.finishRequest(r, null, null, null);
this.timeout_count_--;
}
} else {
num_completed_requests++;
}
}
// This setTimeout also keeps the node.js process running.
if (!this.running_requests_.length ||
num_completed_requests < this.running_requests_.length) {
var self = this;
setTimeout(function() { self.watchDog(); }, this.kWatchdogInterval);
}
}
private initForTestRun() {
this.timeout_count_ = this.kMaxTimeouts;
this.running_requests_ = new Array();
this.finished_concurrent_requests_ = 0;
this.watchDog();
}
public runTests (numPerSize : number, callback: Function) {
this.initForTestRun();
this.result_callback_ = callback;
// Queue the tests.
for (var sz = 0; sz < this.sizes_.length; sz++) {
for (var run = 0; run < numPerSize; run++) {
this.request_queue_.push(sz);
}
}
// Start them.
for (var c = 0; c < this.concurrency; c++) {
this.runATest(c);
}
}
};
export interface BenchmarkStrategy {
configure(requestManager: RequestManager) : void;
run() : void;
};
} // module Benchmark