Skip to content
Marko Topolnik edited this page Aug 9, 2017 · 8 revisions
  1. Create a project in Eclipse. Any project type will do, for example a simple "Project";

  2. create a file, name it example.js, and copy this into it:

function init() {
  test();
}

function test() {
  req('example').get('http://www.example.org').go(spy);
}

Replace the URL with something that will give a succesful response. WARNING: choose the URL wisely, you don't want to get blacklisted as a DoS attacker of a public service!

  1. right-click on the editor area, choose Run As -> RequestAge Script (or use the Run shortcut: Ctrl-F11 or ⌘⇧F11);

  2. the RequestAge and RequestAge History views should appear;

  3. in the RequestAge view, find the slider to the left: this is the throttle. Apply it gently and watch what happens on the histogram.

Script explained

The presented script will stress-test an HTTP GET request to wwww.example.org. The req() function returns the request builder, whose go() method makes the request execute. go(spy) means that the spy function will be called with the response. spy is a built-in function which logs its argument at the DEBUG level. The logging level is configured to DEBUG only during the initial call to init(), then switches to INFO, so this way you'll get just the first result in the logs and the main stress testing session will not inundate the logs. For exceptional cases where you want logging to remain on during the stress test, there is also the infoSpy function.

Histograms explained

The following picture shows a typical histogram for high, but bearable, server load. The server is able to keep up, but some requests are taking a longer time to serve:

Histogram-normal load

Note the following elements:

  • throttle is the slider all the way to the left;
  • the blue bar next to it is a reading of the current requests per second, shown numerically below it (354);
  • the scale is logarithmic, with ticks at 2-4-6-8-10-20-40-...;
  • on the logarithmic scale itself, below the 10 marking, note a blue notch surrounded by a grey range: this visualizes the reciprocal of the average response time (the unit is 1/sec) and the associated confidence interval. We can read out 1/6th of a second average response and the confidence interval extending from approx. 1/5th to 1/8th of a second;
  • the main part is occupied by the histogram: for a given age in seconds, it shows how many pending requests there are of that age. The histogram does not refer to the logarithmic scale: it is linear, each pixel representing one request;
  • in this snapshot, the cluster just to the right of the zero mark shows that most requests are up to a tenth of a second old and the tiny dots more to the right show that a few requests are waiting for up to a half second;
  • all the way to the right is the sum total of pending requests, presented as a number (62) and graphically, as the tan line. The line will grow into a fat rectangle if the server goes into overload.

On the second snapshot we see a situation typical for server overload:

Histogram-overload

About 8 seconds ago we raised the load to 812 requests per second and there are 4498 outstanding requests (and growing). The histogram is sloping to the right, meaning that the server is still serving many requests, giving preference to the older ones, but it is not able to keep up with the pressure. Note the yellow bar just to the right of the blue requests-per-second bar: it shows the difference between requests and responses per second. It extends from the top of the blue bar down, towards the point which marks the current responses-per-second rate: 200. It did not appear in the first histogram, where the requests and responses were balanced. Here we can see that there is an imbalance, resulting in the constant growth of the pending request count.

The third snapshot shows what happened after we first dropped the load to zero for a few seconds, then reduced it back to the initial level:

Histogram-recovery

Notice the small green stub extending upwards from the blue request-per-second bar: this shows there's a surplus of responses over requests: the server is catching up with old requests. Also notice the red bar extending from the bottom of the scale: this indicates failures per second. By double-clicking the histogram we can get a full stack trace of the exception which indicated the most recent failure: in this case it's a timeout at the client side (the default timeout is 20 seconds).

History view and snapshot report

In the history view we can review the history of certain quantities throughout a stress-testing session:

RequestAge history

History is kept for the following quantities:

  • total pending requests;
  • response time;
  • requests per second;
  • failures per second;
  • response time scatter chart: each dot means "there was at least one request made at time X, which took Y time to receive a response (or failure).

Up in the RequestAge view's title bar, next to the stop button, there is a button which will open a report window:

RequestAge view title bar

The report documents the current value of a number of tracked quantities:

RequestAge report

The quantities shown for each tracked request are as follows:

  • request rate (requests per second);
  • pending requests;
  • successful response rate (responses per second);
  • failure rate (failures per second);
  • average of the response time over the last 2 seconds (or longer, a minimum of 50 responses is sampled)
  • standard deviation of the response time;
  • size of the response in bytes;
  • network bandwidth occupied by the responses.

The last row shows the total for all tracked requests.

More scripting

We'll extend our basic script a bit and show a good idiom for chained requests.

If we need to extract data from the response, we should provide our own callback function. RequestAge has built-in functions to parse the response as JSON and XML; otherwise you may obtain the response body as a String, binary data, or a Java InputStream. The response object passed to the callback has the native com.ning.http.client.Response instance as its prototype, so all its methods are supported.

Let's say we receive an XML response:

function init() { test(); }
function test() {
  req('example').get('http://www.example.org').go(function(r) {
    var id = xpath('/resp/user/id/text()').evaluateFirst(r.xmlBody());
  });
}

r.xmlBody() returns a JDOM2 document tree reflecting the parsed XML response. This is passed to Jaxen's XPath evaluator returned by the xpath built-in function. Evaluators are cached to avoid the cost of repeated compiling of the same XPath expression.

If you want to place another request based on the retrieved ID, you'll make it from inside the callback:

function init() { test(); }
function test() {
  req('example').get('http://www.example.org').go(function(r) {
    var id = xpath('/resp/user/id/text()').evaluateFirst(r.xmlBody());
    req('getList').get(url('http://www.example.org').s(id, 'list')).go(function(r) {
      var firstItem = xpath('/resp/items/item[1]/text()').evaluateFirst(r.xmlBody());
    }
  });
}

Note that we now build our URL using the url function, which returns the URL builder object. Its method s adds path segments to the URL (you can read the method as "slash", if you like). So for example our URL will be http://www.example.org/2343/list. Again we supply a callback and extract the first list item.

Now the main pattern of the asynchronous coding style is emerging: each successive HTTP request requires another callback to process it. If all the functions are defined in-place as above, the nesting level gets ever deeper. The preferred style, well-supported by the semantics of JavaScript's function scope, would be to define each function directly inside the test function:

function init() { test(); }
function test() {
  var id, firstItem;
  function step1() {
    req('example').get('http://www.example.org').go(step2);
  }
  function step2(r) {
    id = xpath('/resp/user/id/text()').evaluateFirst(r.xmlBody());
    req('getList').get(url('http://www.example.org').s(id, 'list')).go(step3);
  }
  function step3(r) {
    firstItem = xpath('/resp/items/item[1]/text()').evaluateFirst(r.xmlBody());
  }
  step1();
}

The most convenient form is to put all the functions into an array and refer to them not by name, but by index in the array. The advantage of this approach is that each step can now easily be commented out without changing anything else:

function test() {
   var id, firstItem;
   var i = 0;
   var steps=
      [
       function() { req('example').get('http://www.example.org').go(steps[i++]); },
       function(r) {
          id = xpath('/resp/user/id/text()').evaluateFirst(r.xmlBody());
          req('getList').get(url('http://www.example.org').s(id, 'list')).go(steps[i++]);
       },
       function(r) {
          firstItem = xpath('/resp/items/item[1]/text()').evaluateFirst(r.xmlBody());
       }
   ];
   steps[i++]();
 }