Write Your Own Load Tester in Under an Hour

by Zack Grossbart on November 25, 2007

Creating web applications can be a complex and daunting task. Web applications need to support multiple users while providing a fast response time. The best way to ensure your application does this is load testing. A load tester will simulate a large number of simultaneous users accessing your application and give you an idea how the application will respond under heavy load. There are many load testing suites available for varying cost. However, many web applications can be tested for free with a custom load testing application. This sample will work especially well with smaller web applications as well as application with well known bottlenecks.

This article will detail the inner workings of a sample load testing application and give you the understanding you need to adapt and expand this application to meet your needs. This application can be a useful tool, but it is also a good introduction to advanced Java topics like threading. This application simulates a web browser and lets you test how your web application will perform in the real world.

This application is written in Java. I have worked with many languages, but I have not found a better solution for writing GUI based multi-platform applications than Java. This sample will run on Windows, Linux, MacOS, and any other platform Java is available on. Most computers already have a Java JDK installed, but if you don’t have one you can download it from http://www.javasoft.com.

The code in this application is free and is released under the Apache 2.0 license. That means you are welcome to use, copy, and change this code as much as you would like. If you find any bugs, have any comments, or make any improvements I would love to hear from you. There are a couple of other programs suggested to run this sample, but they are all free.

Setup

Get the
source code

Get the Source Code

You should start by downloading the source code for this sample. It can be found here.

Other Programs

This program has been setup to be built using Apache Ant. Ant is a very popular build tool for use with Java projects. You are not required to use Ant, but it makes everything a lot easier. You will want to download and install Ant to build the sample code. Once you have installed Ant you can just execute the command ant in the directory where you unzipped the sample code and everything will be built.

This program will generate a WAR file which requires a servlet container to run. The servlet isn’t really part of the application. It is just there so you have something easy to test against. You can use this program to test any web application written in any language.

However, if you want to run the sample code without any changes you will need to run the WAR in a servlet container. I am running my WAR with Jetty. Jetty is an open source web server. It is light, fast, and free. When you are ready to run your WAR you can just copy it to the webapps folder in your Jetty installation and restart your web server.

Build and Run

Once you have installed Ant and Jetty you can run the sample application with the following steps:

  1. Unzip the samples archive to a directory on your machine.
  2. Open a command prompt and change directories to the location you unzipped the sample in.
  3. Execute the command ant (you may need to provide the full path to your Ant installation).
  4. Copy the WAR file dist/loadtest.war to the webapps directory of your Jetty server.
  5. Start the Jetty server. (The start command is java -jar start.jar.)
  6. Run the client with the command java -jar dist/loadtest.jar

Core Technologies

Our program will use the following core technologies:

  • Java Threads
  • HTTP
  • Java Swing
  • Java IO

In addition to being a useful load testing program it is also a good introduction to these technologies. This example will demonstrate a simple thread pool, thread management, simple HTTP connections, a basic Java Swing based application, and use of the Java Input/Output libraries.

How It Works

Once everything is deployed and setup you can use the client program to specify the number of threads you want and the number of times you want them to run. When you press start the client will follow these steps:

loadtest

  1. Create a pool of threads to run each test.
  2. Call each thread to contact the server.
  3. Send the desired parameters to the server.
  4. Parse the results.
  5. Return the success or failure of the test and the time it took to run.
  6. Return the thread to the thread pool.

These steps will run every time you press the start button and can be run multiple times.

Let’s Look at the Code

The client consists of two classes Main.java and RetrieverThread.java. Main.java is the class responsible for the UI and for maintaining the threads and RetrieverThread.java is the class defining each thread. These two classes are used to make a clear separation between each thread running a test and the UI of the application.

RetrieverThread.java

RetrieverThread.java does the work of talking to the server and confirming that the server response was correct. The retriever thread can exist in one of three states: running, waiting, and stopped. The thread will run through one iteration and then wait until it is run again or stopped. Once the thread is stopped then it can not be restarted. Let’s look at the code that manages the state of the thread.

private boolean m_cond = true;1
private boolean m_shouldStop;2

public void doStop()
{
    m_shouldStop = true;
    stopWait();
}

protected synchronized void stopWait()
{
    this.notify();
}

protected synchronized void doWait()
{
    m_cond = true;
    while (m_cond) {
        try {
            this.wait();
            m_cond = false;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public void run()
{
    while (true) {
        if (m_shouldStop)
            return;

        getData();3
        doWait();4
    }
}

There are two variables which store the state of this thread. The first is the 1.m_cond variable. This is used to indicate if the thread is running or waiting. The 2.m_shouldStop variable is used to store when the thread should stop running.

The thread can be stopped with the doStop method, paused with the doWait method, and run again with the stopWait method. When this thread is started the calling class will call the start method. The start method is a special method defined in the java.lang.Thread class which will spawn a new thread and then call the run method in that new thread. The run method will continue in a loop until the thread is stopped. This loop will 3.get the data from the server and then 4.wait until it is called again.

Now that we have examined the thread management code we can look into the communication with the server. Every time this thread runs it will call the getData method. We will look at a simplified version of this method here. For the full version see RetrieverThread.java in the sample code.

private void getData()
{
    m_time = -1;
    m_worked = false;
    m_exception = null;
    m_firstParam = true;1

    HttpURLConnection conn = null;
    try {
        conn = (HttpURLConnection) m_server.openConnection();2

        conn.setRequestMethod("POST");
        conn.setFollowRedirects(false);

        conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
        conn.setDoOutput(true);3

        Writer out = new OutputStreamWriter(conn.getOutputStream());
        for (int i = 0; i < m_paramNames.length; i++) {
            addParameter(out, m_paramNames[i], m_paramVals[i]);4
        }

        out.flush();
        out.close();5

        long startTime = System.nanoTime();6
        conn.connect();7

        InputStream in = conn.getInputStream();
        m_worked = parseSuccess(in);8

        m_time = System.nanoTime() - startTime;9
    } catch (Exception e) {
        e.printStackTrace();
        m_exception = e;
    } finally {
        if (conn != null)
            conn.disconnect();10
    }
}

1. First we want to reset all of our member variables in case this thread has been run before.
2. This will not actually contact the server, but will just setup the connection so we can set the parameters.
3. Set the properties of the connection. This connection will use the POST method, not follow HTTP redirects, and use a form URL encoded content type for the parameters.
4. Add each parameter to the request.
5. Make sure to close our output stream before opening the connection.
6. Now we will get the current time so we can see how long this operation will take.
7. Finally we will actually connect to the server.
8. Now we need to parse the output from the server.
9. Once we are done parsing the output we can get the current time again and figure out how long it took to run this operation.
10. Last but not least we need to make sure to close the connection to the server.

Quick Notes About Multi-threaded Programming

There are many books written about multi-threaded programming. I won’t try to address the entire topic here, However, I have a few general pieces of advice.

  • Keep it simple – Multiple threads produces the opportunity for a lot strange errors. Keeping things simple will help you avoid some of these errors.
  • Keep thread interaction to a minimum – Different threads must interact, but those interactions can be complicated. Keep those interactions to a minimum and make sure to keep them as simple as possible.
  • Use the synchronized keyword sparingly – The synchronized keyword in Java will make sure that only one thread at a time can access a method. This is very useful, but it can also create bottlenecks. If you made all of your methods synchronized you would essentially have a single-threaded application which ran slower because of all the threads.
  • Don’t talk to a server in the UI thread – Talking to the server can take a long time. Maybe the server will be slow to respond or not respond at all. It is important to make sure your application remain responsive during this time by using a background thread to communicate with the server.

Main.java

Main.java extends JPanel and is also the main entry point of this program. Main.java will create a new frame and the UI control needed to gather the data for the load test. It also contains a button to start the load test. Let’s look at the code behind that button.

private void doStart()
{
    try {
        m_threads.clear();1
        int threadCount = ((Integer) m_threadCount.getValue()).intValue();
        m_opCount = ((Integer) m_count.getValue()).intValue() - 1;
        for (int i = 0; i < threadCount; i++) {
            RetrieverThread thread = new RetrieverThread(new URL(m_url.getText()),
                                                         PARAM_NAMES, PARAM_VALUES);2
            m_threads.put(thread, Integer.valueOf(0));3
            thread.start();4
        }
        m_shortTimer = new Timer(100, this);
        m_shortTimer.start();5
      } catch (MalformedURLException e) {
        JOptionPane.showMessageDialog(this, "Invalid URL: " + m_url.getText());
    }
}

1. Clear the thread pool in case this is not the first time we have run.
2. Create as many new RetrieverThread objects as the user indicated they wanted.
3. Add each thread to the thread pool.
4. Start the thread.
5. Start the timer once we are done creating all the threads.

The term thread pool is often used and sounds very technical. In this case our thread pool is just a HashMap named m_threads. This map uses each thread as the key and the number of times that thread has run as the value.

Now that all of our threads are started we need to manage them. Our application will manage the threads using a javax.swing.Timer object. This object will notify our code at given intervals so we can monitor the threads in our thread pool. We started this timer along with the threads. Now lets look at what happens when the timer is triggered.

public void actionPerformed(ActionEvent e)
{
    Object keys[] = m_threads.keySet().toArray();
    for (int i = 0; i < keys.length; i++) {
        RetrieverThread thread = (RetrieverThread) keys[i];
        int currentCount = ((Integer) m_threads.get(thread)).intValue();
        if (!thread.isRunning()1) {
            if (currentCount <= m_opCount2) {
                System.out.println(thread + " (Iteration: " + currentCount + ") - " +
                                   formatTime(thread.getTime()) + " result: SUCCESS");3
                thread.stopWait();4

                m_threads.put(thread, Integer.valueOf(currentCount + 1));

            } else {
                thread.doStop();5
            }
        }
    }
}

This is a simplified version of the actionPerformed method. The full method can be found in Main.java in the sample code.

1. If the thread is still running then we will just leave it alone.
2. We only want to restart the thread if it hasn’t run the prerequisite number of times.
3. Now we want to report the thread’s status.
4. Once we are done with it we will restart this thread.
5. If the thread has run the number of times we needed then we want to explicitly stop this thread. If we don’t do this then the thread will wait forever and cause a memory leak.

See It in Action

Now that we have reviewed the code let’s see the program in action. Once you have deployed your WAR you can start the load testing client JAR. The client JAR uses very simple reporting and will send all data out to the system console. You could import this data into a spread sheet program if you would like. If you run with the default configuration (five threads run five times each) you will see the following output:

Client Thread 2 (Iteration: 0) - 188 milliseconds result: SUCCESS
Client Thread 5 (Iteration: 0) - 188 milliseconds result: SUCCESS
Client Thread 3 (Iteration: 0) - 188 milliseconds result: SUCCESS
Client Thread 4 (Iteration: 0) - 188 milliseconds result: SUCCESS
Client Thread 1 (Iteration: 0) - 186 milliseconds result: SUCCESS
Client Thread 2 (Iteration: 1) - 23 milliseconds result: SUCCESS
Client Thread 5 (Iteration: 1) - 20 milliseconds result: SUCCESS
Client Thread 3 (Iteration: 1) - 25 milliseconds result: SUCCESS
Client Thread 4 (Iteration: 1) - 13 milliseconds result: SUCCESS
Client Thread 1 (Iteration: 1) - 28 milliseconds result: SUCCESS
Client Thread 2 (Iteration: 2) - 14 milliseconds result: SUCCESS
Client Thread 5 (Iteration: 2) - 10 milliseconds result: SUCCESS
Client Thread 3 (Iteration: 2) - 11 milliseconds result: SUCCESS
Client Thread 4 (Iteration: 2) - 25 milliseconds result: SUCCESS
Client Thread 1 (Iteration: 2) - 16 milliseconds result: SUCCESS
Client Thread 2 (Iteration: 3) - 4 milliseconds result: SUCCESS
Client Thread 5 (Iteration: 3) - 9 milliseconds result: SUCCESS
Client Thread 3 (Iteration: 3) - 10 milliseconds result: SUCCESS
Client Thread 4 (Iteration: 3) - 8 milliseconds result: SUCCESS
Client Thread 1 (Iteration: 3) - 14 milliseconds result: SUCCESS
Client Thread 2 (Iteration: 4) - 4 milliseconds result: SUCCESS
Client Thread 5 (Iteration: 4) - 10 milliseconds result: SUCCESS
Client Thread 3 (Iteration: 4) - 9 milliseconds result: SUCCESS
Client Thread 4 (Iteration: 4) - 15 milliseconds result: SUCCESS
Client Thread 1 (Iteration: 4) - 19 milliseconds result: SUCCESS

For each thread that is run there is an indication of which iteration it was running, how long that iteration took to run, and if that iteration was a success. In this case we can see that all threads completed successfully and that the application took well under a second to return. It is interesting to note that the threads will often run in a different order for each iteration of the test.

Conclusion

These have been just the basics of a load testing application. This application has been made simple for demonstration purposes and could easily be expanded. There is also much more information, comments, and sample code in the source archive for this sample.

I hope this has been a useful and educational example. You can take this example and adapt it to your web application. You can read the parameters for the test from a properties file, expand the test to include multiple server requests, redesign the UI, or design your own features.

What’s Missing

There are two large omissions in this sample. The sample code does not handle cookies and does not handle HTTP basic authentication. I left this out to make the application easier to understand. If you need to use cookies or basic authentication you can find more information about them in the HTTP specification and the Java Servlet Specification. If there is enough interest I might write another article about cookies and authentication.