/*************************************************************************
* *
* SignServer: The OpenSource Automated Signing Server *
* *
* This software is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or any later version. *
* *
* See terms of license at gnu.org. *
* *
*************************************************************************/
package org.signserver.test.performance.cli;
import java.io.*;
import java.rmi.RemoteException;
import java.util.*;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.signserver.common.InvalidWorkerIdException;
import org.signserver.test.performance.FailureCallback;
import org.signserver.test.performance.WorkerThread;
import org.signserver.test.performance.impl.DocumentSignerThread;
import org.signserver.test.performance.impl.TimeStampThread;
/**
* Performance test tool.
*
* @author Marcus Lundblad
* @version $Id: Main.java 3506 2013-05-27 13:00:12Z malu9369 $
*
*/
public class Main {
/** Logger for this class */
private static Logger LOG = Logger.getLogger(Main.class);
private static final String TEST_SUITE = "testsuite";
private static final String TIME_LIMIT = "timelimit";
private static final String THREADS = "threads";
private static final String TSA_URL = "tsaurl";
private static final String PROCESS_URL = "processurl";
private static final String WORKER_NAME_OR_ID = "worker";
private static final String MAX_WAIT_TIME = "maxwaittime";
private static final String WARMUP_TIME = "warmuptime";
private static final String STAT_OUTPUT_DIR = "statoutputdir";
private static final String INFILE = "infile";
private static final String DATA = "data";
private static final Options OPTIONS;
private static final String NL = System.getProperty("line.separator");
private static final String COMMAND = "stresstest";
private static final int DEFUALT_MAX_WAIT_TIME = 100;
private static int exitCode;
private static long startTime;
private static long warmupTime;
private static String infile;
private static byte[] data;
private enum TestSuites {
TimeStamp1,
DocumentSigner1,
}
static {
OPTIONS = new Options();
OPTIONS.addOption(TEST_SUITE, true, "Test suite to run. Any of " + Arrays.asList(TestSuites.values()) + ".");
OPTIONS.addOption(TIME_LIMIT, true, "Optional. Only run for the specified time (in milliseconds).");
OPTIONS.addOption(THREADS, true, "Number of threads requesting time stamps.");
OPTIONS.addOption(TSA_URL, true, "URL to timestamp worker to use.");
OPTIONS.addOption(PROCESS_URL, true, "URL to process servlet (for the DocumentSigner1 test suite).");
OPTIONS.addOption(WORKER_NAME_OR_ID, true, "Worker name or ID to use (with the DocumentSigner1 test suite).");
OPTIONS.addOption(MAX_WAIT_TIME, true, "Maximum number of milliseconds for a thread to wait until issuing the next time stamp. Default=100");
OPTIONS.addOption(WARMUP_TIME, true,
"Don't count number of signings and response times until after this time (in milliseconds). Default=0 (no warmup time).");
OPTIONS.addOption(STAT_OUTPUT_DIR, true,
"Optional. Directory to output statistics to. If set, each threads creates a file in this directory to output its response times to. The directory must exist.");
OPTIONS.addOption(INFILE, true, "Input file used for DocumentSigner1 testsuite.");
OPTIONS.addOption(DATA, true, "Input data to be used with the DocumentSigner1 testsuite using an XMLSigner.");
}
/**
* Print usage message.
*/
private static void printUsage() {
StringBuilder footer = new StringBuilder();
footer.append(NL)
.append("Sample usages:").append(NL)
.append("a) ").append(COMMAND)
.append(" -testsuite TimeStamp1 -threads 4 -tsaurl http://localhost:8080/signserver/tsa?workerId=1").append(NL)
.append("b) ").append(COMMAND)
.append(" -testsuite TimeStamp1 -threads 4 -maxwaittime 100 -statoutputdir ./statistics/ -tsaurl http://localhost:8080/signserver/tsa?workerId=1").append(NL)
.append("c) ").append(COMMAND)
.append(" -testsuite DocumentSigner1 -threads 4 -processurl http://localhost:8080/signserver/process -worker PDFSigner -infile test.pdf").append(NL)
.append("d) ").append(COMMAND)
.append(" -testsuite DocumentSigner1 -threads 4 -processurl http://localhost:8080/signserver/process -worker XMLSigner -data \"\"").append(NL);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
final HelpFormatter formatter = new HelpFormatter();
PrintWriter pw = new PrintWriter(bout);
formatter.printHelp(pw, HelpFormatter.DEFAULT_WIDTH, COMMAND + " ", "Performance testing tool", OPTIONS, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer.toString());
pw.close();
LOG.info(bout.toString());
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws RemoteException, InvalidWorkerIdException {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("(Debug logging is enabled)");
}
final CommandLine commandLine = new GnuParser().parse(OPTIONS, args);
// Test suite
final TestSuites ts;
if (commandLine.hasOption(TEST_SUITE)) {
ts = TestSuites.valueOf(commandLine.getOptionValue(TEST_SUITE));
} else {
throw new ParseException("Missing option: -" + TEST_SUITE);
}
// Time limit
final long limitedTime;
if (commandLine.hasOption(TIME_LIMIT)) {
limitedTime = Long.parseLong(commandLine.getOptionValue(TIME_LIMIT));
} else {
limitedTime = -1;
}
final int numThreads;
if (commandLine.hasOption(THREADS)) {
numThreads = Integer.parseInt(commandLine.getOptionValue(THREADS));
} else {
throw new ParseException("Missing option: -" + THREADS);
}
final int maxWaitTime;
if (commandLine.hasOption(MAX_WAIT_TIME)) {
maxWaitTime = Integer.parseInt(commandLine.getOptionValue(MAX_WAIT_TIME));
} else {
maxWaitTime = DEFUALT_MAX_WAIT_TIME;
}
final String url;
if (commandLine.hasOption(TSA_URL)) {
if (!ts.equals(TestSuites.TimeStamp1)) {
throw new ParseException("Option " + TSA_URL + " can only be used with the " +
TestSuites.TimeStamp1.toString() + " test suite.");
}
url = commandLine.getOptionValue(TSA_URL);
} else if (commandLine.hasOption(PROCESS_URL)) {
if (!ts.equals(TestSuites.DocumentSigner1)) {
throw new ParseException("Option " + TSA_URL + " can only be used with the " +
TestSuites.TimeStamp1.toString() + " test suite.");
}
url = commandLine.getOptionValue(PROCESS_URL);
} else {
if (ts.equals(TestSuites.TimeStamp1)) {
throw new ParseException("Missing option: -" + TSA_URL);
} else {
throw new ParseException("Missing option: -" + PROCESS_URL);
}
}
String workerNameOrId = null;
if (commandLine.hasOption(WORKER_NAME_OR_ID)) {
workerNameOrId = commandLine.getOptionValue(WORKER_NAME_OR_ID);
} else if (ts.equals(TestSuites.DocumentSigner1)) {
throw new ParseException("Must specify worker name or ID.");
}
if (commandLine.hasOption(INFILE)) {
final String file = commandLine.getOptionValue(INFILE);
final File infile = new File(file);
try {
data = FileUtils.readFileToByteArray(infile);
} catch (IOException e) {
LOG.error("Failed to read input file: " + e.getMessage());
}
} else if (commandLine.hasOption(DATA)) {
data = commandLine.getOptionValue(DATA).getBytes();
} else if (ts.equals(TestSuites.DocumentSigner1)) {
throw new ParseException("Must specify an input file.");
}
if (commandLine.hasOption(WARMUP_TIME)) {
warmupTime = Long.parseLong(commandLine.getOptionValue(WARMUP_TIME));
} else {
warmupTime = 0;
}
// Time limit
final File statFolder;
if (commandLine.hasOption(STAT_OUTPUT_DIR)) {
statFolder = new File(commandLine.getOptionValue(STAT_OUTPUT_DIR));
if (!statFolder.exists() || !statFolder.isDirectory()) {
throw new ParseException("Option -" + STAT_OUTPUT_DIR + " must be an existing directory");
}
} else {
statFolder = null;
}
// Print info
LOG.info(String.format(
"-- Configuration -----------------------------------------------------------%n"
+ " Start time: %s%n"
+ " Test suite: %s%n"
+ " Threads: %10d%n"
+ " Warm up time: %10d ms%n"
+ " Max wait time: %10d ms%n"
+ " Time limit: %10d ms%n"
+ " URL: %s%n"
+ " Output statistics: %s%n"
+ "-------------------------------------------------------------------------------%n", new Date(), ts.name(), numThreads, warmupTime, maxWaitTime, limitedTime, url, statFolder == null ? "no" : statFolder.getAbsolutePath()));
final LinkedList threads = new LinkedList();
final FailureCallback callback = new FailureCallback() {
@Override
public void failed(WorkerThread thread, String message) {
for (WorkerThread w : threads) {
w.stopIt();
}
// Print message
LOG.error(" " + message);
exitCode = -1;
}
};
Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOG.error("Uncaught exception from t", e);
callback.failed((WorkerThread) t, "Uncaught exception: " + e.getMessage());
}
};
Thread shutdownHook = new Thread() {
@Override
public void run() {
if (LOG.isDebugEnabled()) {
LOG.debug("Shutdown hook called");
}
shutdown(threads);
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
try {
switch (ts) {
case TimeStamp1:
timeStamp1(threads, numThreads, callback, url, maxWaitTime, warmupTime, limitedTime, statFolder);
break;
case DocumentSigner1:
documentSigner1(threads, numThreads, callback, url, workerNameOrId, maxWaitTime, warmupTime, limitedTime, statFolder);
break;
default:
throw new Exception("Unsupported test suite");
}
// Wait 1 second to start
Thread.sleep(1000);
// Start all threads
startTime = System.currentTimeMillis();
for (WorkerThread w : threads) {
w.setUncaughtExceptionHandler(handler);
w.start();
}
// Wait for the threads to finish
try {
for (WorkerThread w : threads) {
if (LOG.isDebugEnabled()) {
LOG.debug("Waiting for thread " + w.getName());
}
w.join();
if (LOG.isDebugEnabled()) {
LOG.debug("Thread " + w.getName() + " stopped");
}
}
} catch (InterruptedException ex) {
if (LOG.isDebugEnabled()) {
LOG.debug("Interupted when waiting for thread: " + ex.getMessage());
}
}
} catch (Exception ex) {
LOG.error("Failed: " + ex.getMessage(), ex);
exitCode = -1;
}
System.exit(exitCode);
} catch (ParseException ex) {
LOG.error("Parse error: " + ex.getMessage());
printUsage();
System.exit(-2);
}
}
/**
* Shutdown worker threads.
*
* @param threads
*/
private static void shutdown(final List threads) {
for (WorkerThread w : threads) {
w.stopIt();
}
// Total statistics
long totalRunTime = System.currentTimeMillis() - startTime - warmupTime;
long totalOperationsPerformed = 0;
long totalResponseTime = 0;
double totalAverageResponseTime;
long totalMaxResponseTime = 0;
long totalMinResponseTime = Long.MAX_VALUE;
// Wait until all stopped
try {
for (WorkerThread w : threads) {
if (LOG.isDebugEnabled()) {
LOG.debug("Waiting for thread " + w.getName() + " to finish.");
}
w.join();
final long operationsPerformed = w.getOperationsPerformed();
final long maxResponseTime = w.getMaxResponseTime();
final long minResponseTime = w.getMinResponseTime();
totalOperationsPerformed += operationsPerformed;
totalResponseTime += w.getResponseTimeSum();
totalMaxResponseTime = Math.max(totalMaxResponseTime, maxResponseTime);
totalMinResponseTime = Math.min(totalMinResponseTime, minResponseTime);
}
} catch (InterruptedException ex) {
LOG.error("Interrupted: " + ex.getMessage());
}
if (totalOperationsPerformed > 0) {
totalAverageResponseTime = totalResponseTime / (double) totalOperationsPerformed;
} else {
totalAverageResponseTime = Double.NaN;
}
final double tps;
if (totalRunTime > 1000) {
tps = totalOperationsPerformed / (totalRunTime / 1000d);
} else {
tps = Double.NaN;
}
if (totalMinResponseTime == Long.MAX_VALUE) {
totalMinResponseTime = 0;
}
if (totalRunTime < 0) {
totalRunTime = 0;
}
LOG.info(String.format(
"%n-- Summary -------------------------------------------------------------------%n"
+ " End time: %s%n"
+ " Operations performed: %10d%n"
+ " Minimum response time: %10d ms%n"
+ " Average response time: %12.1f ms%n"
+ " Maximum response time: %10d ms%n"
+ " Run time: %10d ms%n"
+ " Transactions per second: %12.1f tps%n"
+ "------------------------------------------------------------------------------%n", new Date(), totalOperationsPerformed, totalMinResponseTime, totalAverageResponseTime, totalMaxResponseTime, totalRunTime, tps));
}
/**
* Initialize the worker thread list for the time stamp test suite.
*
* @param threads A list to hold the worker threads. This list is filled by the method.
* @param numThreads Number of threads to create.
* @param failureCallback Callback to handle failures.
* @param url Time stamp signer URL.
* @param maxWaitTime Maximum waiting time between generated requests.
* @param warmupTime Warmup time, if set to > 0, will add a warmup period where no stats are collected.
* @param limitedTime Maximum run time, if set to -1, threads will run until interrupted.
* @param statFolder Output folder for statistics.
* @throws Exception
*/
private static void timeStamp1(final List threads, final int numThreads, final FailureCallback failureCallback,
final String url, int maxWaitTime, long warmupTime, final long limitedTime, final File statFolder) throws Exception {
final Random random = new Random();
for (int i = 0; i < numThreads; i++) {
final String name = "TimeStamp1-" + i;
final File statFile;
if (statFolder == null) {
statFile = null;
} else {
statFile = new File(statFolder, name + ".csv");
}
threads.add(new TimeStampThread(name, failureCallback, url, maxWaitTime, random.nextInt(),
warmupTime, limitedTime, statFile));
}
}
/**
* Initialize the worker thread list for the document signer test suite.
*
* @param threads A list to hold the worker threads. This list is filled by the method.
* @param numThreads Number of threads to create.
* @param failureCallback Callback to handle failures.
* @param url Base process URL.
* @param workerNameOrId Worker name of worker ID.
* @param maxWaitTime Maximum waiting time between generated requests.
* @param warmupTime Warmup time, if set to > 0, will add a warmup period where no stats are collected.
* @param limitedTime Maximum run time, if set to -1, threads will run until interrupted.
* @param statFolder Output folder for statistics.
* @throws Exception
*/
private static void documentSigner1(final List threads, final int numThreads,
final FailureCallback failureCallback, final String url, final String workerNameOrId,
int maxWaitTime, long warmupTime,
final long limitedTime, final File statFolder) throws Exception {
final Random random = new Random();
for (int i = 0; i < numThreads; i++) {
final String name = "DocumentSigner1-" + i;
final File statFile;
if (statFolder == null) {
statFile = null;
} else {
statFile = new File(statFolder, name + ".csv");
}
threads.add(new DocumentSignerThread(name, failureCallback, url, data, workerNameOrId, maxWaitTime,
random.nextInt(), warmupTime, limitedTime, statFile));
}
}
}