
org.jsoar.performancetesting.PerformanceTesting Maven / Gradle / Ivy
/**
*
*/
package org.jsoar.performancetesting;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.jsoar.kernel.SoarException;
import org.jsoar.performancetesting.csoar.CSoarTestFactory;
import org.jsoar.performancetesting.jsoar.JSoarTestFactory;
import org.jsoar.performancetesting.yaml.Configuration;
import org.jsoar.performancetesting.yaml.ConfigurationTest;
import org.jsoar.performancetesting.yaml.TestSettings;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import com.opencsv.exceptions.CsvDataTypeMismatchException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import io.github.classgraph.ClassGraph;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Spec;
/**
* @author ALT
*
*/
@Command(name = "performance-testing", description = "Performance Testing - Performance testing framework for JSoar and CSoar", subcommands = { HelpCommand.class })
public class PerformanceTesting implements Runnable
{
private static TestSettings defaultTestSettings = new TestSettings(false,
false, 0, 0, new ArrayList(), false, 1, Paths.get("."), null,
null, null, "");
@Spec
CommandSpec spec; // injected by picocli
// command line options
@Option(names = { "-C", "--configuration" }, description = "Load a configuration file to use for testing. Ignore the rest of the options.")
Path configuration;
@Option(names = { "-T", "--test" }, description = "Manually specify a .soar file to run tests on. Useful for one-off tests where you don't want to create a configuration file. Also see -n")
Path testPath = null;
@Option(names = { "-o", "--output" }, description = "The directory for all the CSV test results.")
Path output;
@Option(names = { "-w", "--warmup" }, defaultValue = "0", description = "Specify the number of warm up runs for JSoar.")
int warmup;
@Option(names = { "-c", "--category" }, description = "Specify the number of warm up runs for JSoar.")
String category;
@Option(names = { "-j",
"--jsoar" }, arity = "0..1", fallbackValue = "Internal", description = "Run the tests in JSoar optionally specifying the jsoar jar. If not specified will default to the internal jsoar version.")
Path jsoar = null;
@Option(names = { "-s",
"--soar" }, description = "Run the tests in CSoar specifying the directory of CSoar's bin. When running with CSoar, CSoar's bin directory must be on the system path or in java.library.path or specified in a configuration file.")
Path csoar = null;
@Option(names = { "-u", "--uniqueJVMs" }, defaultValue = "false", description = "Whether to run the tests in seperate jvms or not.")
boolean uniqueJVMs;
@Option(names = { "-d", "--decisions" }, description = "Run the tests specified number of decisions.")
Integer decisions = null;
@Option(names = { "-r", "--run" }, description = "How many test runs to perform.")
private Integer runNumber = null;
@Option(names = { "-n", "--name" }, description = "Used in conjunction with -T; specifies the test's name.")
String name = "";
@Option(names = { "-N", "--nosummary" }, defaultValue = "false", description = "Don't output results to a summary file.")
boolean nosummary;
@Option(names = { "-S", "--single" }, hidden = true, description = "Used internally when spawning child JVMs")
boolean singleTest = false;
// Locals
private final PrintWriter out;
// Tests
private CSoarTestFactory csoarTestFactory;
private Test csoarTest;
private JSoarTestFactory jsoarTestFactory;
private Test jsoarTest;
// Class Specific Configuration
private List configurationTests;
// Used for killing the current child process in the shutdown hook
private Process currentChildProcess = null;
/**
* @param args
*/
public static void main(String[] args)
{
final PrintWriter writer = new PrintWriter(System.out);
final int result = new CommandLine(new PerformanceTesting(writer)).execute(args);
writer.flush();
System.exit(result);
}
/**
*
* @param out
*/
public PerformanceTesting(PrintWriter out)
{
this.out = out;
this.jsoarTestFactory = new JSoarTestFactory();
this.csoarTestFactory = new CSoarTestFactory();
}
public void run()
{
try
{
// This adds a shutdown hook to the runtime
// to kill any child processes spawned that
// are still running. This handles CTRL-C
// as well as normal kills, SIGKILL and
// SIGHALT I believe
// - ALT
Runtime.getRuntime().addShutdownHook(new Thread()
{
@Override
public void run()
{
if(currentChildProcess != null)
{
currentChildProcess.destroy();
}
}
});
// Parse the CLI options and Configuration options
// if there are any
parseOptions();
// If this is not a single test and configurationTests is null
// exit because the configuration file didn't work out
// or something else bad happened.
if(!singleTest && configurationTests == null)
{
out.println("Did not load any tests or configuration.");
throw new ParameterException(this.spec.commandLine(), "Did not load any tests or configuration.");
}
// Only output starting information like this if we're not in a child
// process
if(!singleTest)
{
out.printf(
"Performance Testing - Starting Tests - %d Tests Loaded\n\n",
configurationTests.size());
out.flush();
}
// If we have multiple tests to run, then run
// them in children JVMs
if(configurationTests != null)
{
runTestsInChildrenJVMs(configurationTests);
}
else
{
// In this case, we are running just one test which means
// the test is either going to be jsoarTest or csoarTest.
if(jsoarTest != null)
{
runTest(new TestRunner(jsoarTest, out));
}
else
{
runTest(new TestRunner(csoarTest, out));
}
}
}
catch(CsvDataTypeMismatchException | CsvRequiredFieldEmptyException | IllegalStateException | URISyntaxException | IOException | SoarException | ClassNotFoundException | IllegalAccessException
| NoSuchFieldException e)
{
throw new RuntimeException(e);
}
}
/**
* Parses the CLI and Configuration Options
*
* @throws IOException
* @throws JsonMappingException
* @throws JsonParseException
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws ClassNotFoundException
* @throws Exception
*/
private void parseOptions() throws JsonParseException, JsonMappingException, IOException, ClassNotFoundException, IllegalAccessException, NoSuchFieldException
{
// If there is a configuration option,
// ignore any CLI options beyond that.
if(this.configuration != null)
{
parseConfiguration(configuration);
}
// Since we don't have a configuration
// option, parse the CLI arguments.
parseCLIOptions();
}
/**
* Parse a configuration file from a given options processor. This will do
* any validity checks on the configuration file as well.
*
* @param configuration Path to the configuration file
* @throws IOException
* @throws JsonMappingException
* @throws JsonParseException
*/
private void parseConfiguration(Path configuration) throws JsonParseException, JsonMappingException, IOException
{
if(!configuration.toString().endsWith(".yaml"))
{
throw new ParameterException(spec.commandLine(), "Configuration files need to be yaml files; got: " + configuration);
}
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
Configuration config = mapper.readValue(configuration.toFile(), Configuration.class);
defaultTestSettings = config.getDefaultSettings();
if(!defaultTestSettings.isJsoarEnabled()
&& !defaultTestSettings.isCsoarEnabled())
{
out.println("WARNING: Neither jsoar nor csoar selected to run in defaults.");
}
configurationTests = config.getTests();
}
/**
* Parse the CLI arguments
*
* @throws ClassNotFoundException
* @throws MalformedURLException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private void parseCLIOptions() throws MalformedURLException, ClassNotFoundException, IllegalAccessException, NoSuchFieldException
{
if(output != null)
{
defaultTestSettings.setCsvDirectory(output);
}
defaultTestSettings.setWarmUpCount(this.warmup);
if(this.jsoar != null)
{
defaultTestSettings.setJsoarEnabled(true);
if(this.jsoar.toString().equals("Internal"))
{
jsoarTestFactory.setJsoarCoreJar(Paths.get("Internal"));
}
else
{
List tempArray = new ArrayList<>();
tempArray.add(this.jsoar);
defaultTestSettings.setJsoarCoreJars(tempArray);
jsoarTestFactory.setJsoarCoreJar(this.jsoar);
}
}
if(this.csoar != null)
{
defaultTestSettings.setCsoarEnabled(true);
List tempArray = new ArrayList<>();
tempArray.add(this.csoar);
defaultTestSettings.setCsoarDirectories(tempArray);
Path path = this.csoar;
csoarTestFactory.setCSoarDirectory(path);
}
if(this.decisions != null)
{
List decisions = new ArrayList<>();
decisions.add(this.decisions);
defaultTestSettings.setDecisionCycles(decisions);
}
if(this.runNumber != null)
{
defaultTestSettings.setRunCount(1);
}
// This will load an individual test into the uncategorized tests
// category, only really useful
// for single tests that you don't want to create a configuration file
// for
if(this.testPath != null)
{
if(!testPath.toString().endsWith(".soar"))
{
throw new ParameterException(spec.commandLine(), "Tests need to end with .soar");
}
String testName = testPath.getFileName().toString();
if(this.name.length() != 0)
{
testName = this.name;
}
if(defaultTestSettings.isJsoarEnabled())
{
jsoarTest = jsoarTestFactory.createTest(testName, testPath,
defaultTestSettings);
}
if(defaultTestSettings.isCsoarEnabled())
{
csoarTest = csoarTestFactory.createTest(testName, testPath,
defaultTestSettings);
}
}
}
/**
*
* @param tests All the tests
* @throws URISyntaxException
* @throws IOException
* @throws IllegalStateException
* @throws CsvRequiredFieldEmptyException
* @throws CsvDataTypeMismatchException
* @throws Exception
*/
private void runTestsInChildrenJVMs(
List tests) throws URISyntaxException, CsvDataTypeMismatchException, CsvRequiredFieldEmptyException, IllegalStateException, IOException
{
// Since we have more than one test to run, spawn a separate JVM for
// each run.
int i = 0;
for(ConfigurationTest test : tests)
{
Path dir = test.getTestSettings().getCsvDirectory();
if(!Files.exists(dir))
{
try
{
Files.createDirectories(dir);
}
catch(IOException e)
{
throw new RuntimeException(e);
}
}
out.println("--------------------------------------------------");
out.println("Starting " + test.getName() + " Test (" + (++i)
+ "/" + tests.size() + ")");
out.println("--------------------------------------------------");
if(test.getTestSettings().isJsoarEnabled())
{
spawnChildJVMForTest(test, true);
}
if(test.getTestSettings().isCsoarEnabled())
{
spawnChildJVMForTest(test, false);
}
// Generate a summary file
appendToSummaryFile(test);
}
out.println("Performance Testing - Done");
}
/**
* Spawns a child JVM for the test and waits for it to exit.
*
* @param test The test to run
* @param jsoar Whether this is a JSoar or CSoar test
* @throws URISyntaxException
*/
private void spawnChildJVMForTest(ConfigurationTest test, boolean jsoar) throws URISyntaxException
{
// Arguments to the process builder including the command to run
List arguments = new ArrayList<>();
URL baseURL = PerformanceTesting.class.getProtectionDomain()
.getCodeSource().getLocation();
// Path jarPath = null;
// Get the directory for the jar file or class path
String classpathString = baseURL.toString();
if(!classpathString.endsWith(".jar"))
{
List classpath = new ClassGraph().getClasspathURIs();
classpathString = classpath.stream()
.map(Paths::get)
.map(Path::toString)
.collect(Collectors.joining(String.valueOf(File.pathSeparatorChar)));
}
// Construct the array with the command and arguments
// Use javaw for no console window spawning
arguments.add("java");
arguments.addAll(Arrays.asList(test.getTestSettings().getJvmSettings()
.split("\\s")));
arguments.add("-classpath"); // Always use class path. This will even
// work for jars.
arguments.add(classpathString);
arguments.add(PerformanceTesting.class.getCanonicalName()); // Get the
// class
// name to
// load
arguments.add("--test");
arguments.add(test.getFile().toString());
arguments.add("--output");
arguments.add(test.getTestSettings().getCsvDirectory().toString());
arguments.add("--warmup");
arguments.add(String.valueOf(test.getTestSettings().getWarmUpCount()));
arguments.add("--name");
arguments.add(test.getName());
arguments.add("--nosummary");
arguments.add("--single");
if(jsoar)
{
arguments.add("--jsoar");
// For each version of JSoar, run a child JVM
if(test.getTestSettings().getJsoarCoreJars() == null)
{
// Default to internal representation
runJVM(test, arguments);
}
else
{
List paths = test.getTestSettings().getJsoarCoreJars();
for(Path path : paths)
{
List argumentsPerTest = new ArrayList<>(arguments);
argumentsPerTest.add(path.toString());
runJVM(test, argumentsPerTest);
}
}
}
else
{
arguments.add("--soar");
// For each version of CSoar, run a child JVM
if(test.getTestSettings().getCsoarDirectories() == null)
{
throw new RuntimeException(
"CSoar Enabled but no versions specified");
}
for(Path path : test.getTestSettings().getCsoarDirectories())
{
List argumentsPerTest = new ArrayList<>(arguments);
argumentsPerTest.add(path.toString());
runJVM(test, argumentsPerTest);
}
}
}
/**
* This runs a test in a child JVM with the given arguments
*
* @param test
* @param arguments
*/
private void runJVM(ConfigurationTest test,
List arguments)
{
// Run the test in a new child JVM for each run
for(int i = 1; i <= test.getTestSettings().getRunCount(); ++i)
{
List argumentsPerRun = new ArrayList<>(arguments);
int soarLocation = argumentsPerRun.size() - 1;
List soarArguments = new ArrayList<>();
soarArguments.add(argumentsPerRun.get(soarLocation));
argumentsPerRun.remove(soarLocation);
if(soarArguments.get(0).contains("--") != true
|| soarArguments.get(0).contains("soar") != true)
{
--soarLocation;
soarArguments.add(0, argumentsPerRun.get(soarLocation));
argumentsPerRun.remove(soarLocation);
}
argumentsPerRun.add("--run");
argumentsPerRun.add(String.valueOf(i));
out.println("Starting Test - " + test.getName() + " - " + i
+ "/" + test.getTestSettings().getRunCount());
// For each decision cycle count in the list, run a new child JVM
for(Integer j : test.getTestSettings().getDecisionCycles())
{
List argumentsPerCycle = new ArrayList<>(
argumentsPerRun);
argumentsPerCycle.add("--decisions");
argumentsPerCycle.add(j.toString());
argumentsPerCycle.addAll(soarArguments);
String decisionsString;
if(j == 0)
{
decisionsString = "Forever";
}
else
{
decisionsString = "for " + j + " decisions";
}
out.println("Running " + decisionsString);
out.flush();
// This is Java 1.7 only
// Create a new process builder for the child JVM
ProcessBuilder processBuilder = new ProcessBuilder(
argumentsPerCycle);
Process process = null;
try
{
// Start the process
process = processBuilder.start();
// Setup the parameters for the shutdown hook,
// in case we're killed off early.
currentChildProcess = process;
// Create some StreamGobblers to handle output to the screen
// We don't redirectIO here because on Windows in CMD, we
// won't redirect to the console output. Instead we will
// redirect to the original java System.out which isn't the
// screen. This means that the output both, won't show up
// and we could potentially crash the JVM if we output
// enough.
// - ALT
StreamGobbler outputGobbler = new StreamGobbler(
process.getInputStream(), out);
StreamGobbler errorGobbler = new StreamGobbler(
process.getErrorStream(), out);
// Start the gobblers
outputGobbler.start();
errorGobbler.start();
// Wait for the process to exit and then get the exit code
if(process.waitFor() != 0)
{
throw new RuntimeException("Child jvm exited with error");
}
// Make sure we don't try to kill a non-existent process
// if we are shutdown after this.
currentChildProcess = null;
}
catch(IOException e)
{
throw new RuntimeException(e);
}
catch(InterruptedException e)
{
Thread.currentThread().interrupt();
}
finally
{
// Make sure to always destroy the process if exceptions
// occur
if(process != null)
{
process.destroy();
}
}
try
{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
Thread.currentThread().interrupt();
}
finally
{
// Flush the output to make sure we have everything
// probably not needed but there are cases when it is
out.flush();
}
}
}
}
/**
* Output the results to a summary file for a given test. This reads in the
* individual run results and then computes the summary for the test. The
* reading is necessary as these results were (probably) generated in another
* jvm.
*
* @throws IOException
* @throws IllegalStateException
* @throws CsvRequiredFieldEmptyException
* @throws CsvDataTypeMismatchException
*/
private void appendToSummaryFile(ConfigurationTest test) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException, IllegalStateException, IOException
{
TestSettings settings = test.getTestSettings();
if(settings.isJsoarEnabled())
{
List jsoarVersions = settings.getJsoarCoreJars();
if(jsoarVersions == null)
{
jsoarVersions = new ArrayList<>();
jsoarVersions.add(Paths.get("Internal"));
}
for(Path jsoarPath : jsoarVersions)
{
for(int dcs : settings.getDecisionCycles())
{
appendToSummaryFileInternal(test, "JSoar", jsoarPath, dcs, "-raw");
}
}
}
if(settings.isCsoarEnabled())
{
for(Path csoarPath : test.getTestSettings().getCsoarDirectories())
{
for(Integer dcs : settings.getDecisionCycles())
{
appendToSummaryFileInternal(test, "CSoar", csoarPath, dcs, "-raw");
}
}
}
}
private Path getTestPath(TestSettings settings, String testName, String soarVariant, Path soarPath, int dcs, String fileSuffix)
{
String testNameWithoutSpaces = testName.replaceAll("\\s+", "-");
Path testDirectory = settings.getCsvDirectory().resolve(testNameWithoutSpaces);
String label = soarVariant + "-" + soarPath.toString().replaceAll("[^a-zA-Z0-9]+", "");
Path categoryDirectory = testDirectory.resolve(label);
String finalTestName = testNameWithoutSpaces;
if(dcs != 0)
{
finalTestName += "-" + dcs;
}
else
{
finalTestName += "-Forever";
}
Path testPath = categoryDirectory.resolve(finalTestName + fileSuffix + ".csv");
return testPath;
}
private void appendToSummaryFileInternal(ConfigurationTest test, String soarVariant, Path soarPath, int dcs, String fileSuffix)
throws IllegalStateException, IOException, CsvDataTypeMismatchException, CsvRequiredFieldEmptyException
{
File testFile = getTestPath(test.getSettings(), test.getName(), soarVariant, soarPath, dcs, "-raw").toFile();
List rawResults = new CsvToBeanBuilder(new FileReader(testFile))
.withType(RawResults.class).withSkipLines(1).build().parse();
RawResults combinedRawResults = rawResults.stream()
.reduce(new RawResults(), (subtotal, element) -> subtotal.accumulate(element));
Results summary = new Results(test.getName() + "-" + dcs, test.getFile(), soarVariant, soarPath);
summary.updateStats(combinedRawResults);
Path finalPath = test.getSettings().getCsvDirectory().resolve(test.getSettings().getSummaryFile());
boolean newFile = !Files.exists(finalPath);
try(Writer writer = new FileWriter(finalPath.toFile(), true))
{
if(newFile)
{
writer.write(String.join(",", Results.header) + "\n");
}
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(summary);
}
}
/**
* This runs a test. This assume we're already in the child JVM or at least
* are only ever running one test.
*
* @param testRunner A test
* @throws SoarException
* @throws IOException
* @throws CsvRequiredFieldEmptyException
* @throws CsvDataTypeMismatchException
* @throws Exception
*/
private void runTest(TestRunner testRunner) throws SoarException, IOException, CsvDataTypeMismatchException, CsvRequiredFieldEmptyException
{
Test test = testRunner.getTest();
TestSettings settings = test.getTestSettings();
if(!singleTest)
{
out.println("Starting Test: " + test.getTestName());
out.flush();
}
testRunner.runTestsForAverage(settings);
if(settings.getCsvDirectory().getNameCount() != 0)
{
Path finalPath = getTestPath(test.getTestSettings(), test.getTestName(), test.getSoarVariant(), test.getSoarPath(), test.getTestSettings().getDecisionCycles().get(0), "-raw");
Files.createDirectories(finalPath.getParent()); // this will create the dirs needed for the rest of the files below
boolean newFile = !Files.exists(finalPath);
try(Writer writer = new FileWriter(finalPath.toFile(), true))
{
if(newFile)
{
writer.write(String.join(",", RawResults.header) + "\n");
}
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(testRunner.getRawResults());
}
finalPath = getTestPath(test.getTestSettings(), test.getTestName(), test.getSoarVariant(), test.getSoarPath(), test.getTestSettings().getDecisionCycles().get(0), "");
try(Writer writer = new FileWriter(finalPath.toFile()))
{
writer.write(String.join(",", Results.header) + "\n");
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(testRunner.getResults());
}
if(!nosummary)
{
finalPath = settings.getCsvDirectory().resolve(settings.getSummaryFile());
newFile = !Files.exists(finalPath);
try(Writer writer = new FileWriter(finalPath.toFile(), true))
{
if(newFile)
{
writer.write(String.join(",", Results.header) + "\n");
}
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(testRunner.getResults());
}
}
}
out.print("\n");
out.flush();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy