| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.rng.examples.stress; |
| |
| import org.apache.commons.rng.UniformRandomProvider; |
| import org.apache.commons.rng.core.source64.RandomLongSource; |
| import org.apache.commons.rng.simple.RandomSource; |
| |
| import picocli.CommandLine.Command; |
| import picocli.CommandLine.Mixin; |
| import picocli.CommandLine.Option; |
| import picocli.CommandLine.Parameters; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteOrder; |
| import java.nio.file.Files; |
| import java.nio.file.StandardOpenOption; |
| import java.text.SimpleDateFormat; |
| import java.time.Instant; |
| import java.time.LocalDateTime; |
| import java.time.ZoneId; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.Formatter; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * Specification for the "stress" command. |
| * |
| * <p>This command loads a list of random generators and tests each generator by |
| * piping the values returned by its {@link UniformRandomProvider#nextInt()} |
| * method to a program that reads {@code int} values from its standard input and |
| * writes an analysis report to standard output.</p> |
| */ |
| @Command(name = "stress", |
| description = {"Run repeat trials of random data generators using a provided test application.", |
| "Data is transferred to the application sub-process via standard input."}) |
| class StressTestCommand implements Callable<Void> { |
| /** 1000. Any value below this can be exactly represented to 3 significant figures. */ |
| private static final int ONE_THOUSAND = 1000; |
| |
| /** The standard options. */ |
| @Mixin |
| private StandardOptions reusableOptions; |
| |
| /** The executable. */ |
| @Parameters(index = "0", |
| description = "The stress test executable.") |
| private File executable; |
| |
| /** The executable arguments. */ |
| @Parameters(index = "1..*", |
| description = "The arguments to pass to the executable.", |
| paramLabel = "<argument>") |
| private List<String> executableArguments = new ArrayList<>(); |
| |
| /** The file output prefix. */ |
| @Option(names = {"--prefix"}, |
| description = "Results file prefix (default: ${DEFAULT-VALUE}).") |
| private File fileOutputPrefix = new File("test_"); |
| |
| /** The stop file. */ |
| @Option(names = {"--stop-file"}, |
| description = {"Stop file (default: <Results file prefix>.stop).", |
| "When created it will prevent new tasks from starting " + |
| "but running tasks will complete."}) |
| private File stopFile; |
| |
| /** The output mode for existing files. */ |
| @Option(names = {"-o", "--output-mode"}, |
| description = {"Output mode for existing files (default: ${DEFAULT-VALUE}).", |
| "Valid values: ${COMPLETION-CANDIDATES}."}) |
| private StressTestCommand.OutputMode outputMode = OutputMode.ERROR; |
| |
| /** The list of random generators. */ |
| @Option(names = {"-l", "--list"}, |
| description = {"List of random generators.", |
| "The default list is all known generators."}, |
| paramLabel = "<genList>") |
| private File generatorsListFile; |
| |
| /** The number of trials to put in the template list of random generators. */ |
| @Option(names = {"-t", "--trials"}, |
| description = {"The number of trials for each random generator.", |
| "Used only for the default list (default: ${DEFAULT-VALUE})."}) |
| private int trials = 1; |
| |
| /** The trial offset. */ |
| @Option(names = {"--trial-offset"}, |
| description = {"Offset to add to the trial number for output files (default: ${DEFAULT-VALUE}).", |
| "Use for parallel tests with the same output prefix."}) |
| private int trialOffset; |
| |
| /** The number of available processors. */ |
| @Option(names = {"-p", "--processors"}, |
| description = {"Number of available processors (default: ${DEFAULT-VALUE}).", |
| "Number of concurrent tasks = ceil(processors / threadsPerTask)", |
| "threadsPerTask = applicationThreads + (ignoreJavaThread ? 0 : 1)"}) |
| private int processors = Math.max(1, Runtime.getRuntime().availableProcessors()); |
| |
| /** The number of threads to use for each test task. */ |
| @Option(names = {"--ignore-java-thread"}, |
| description = {"Ignore the java RNG thread when computing concurrent tasks."}) |
| private boolean ignoreJavaThread; |
| |
| /** The number of threads to use for each testing application. */ |
| @Option(names = {"--threads"}, |
| description = {"Number of threads to use for each application (default: ${DEFAULT-VALUE}).", |
| "Total threads per task includes an optional java thread."}) |
| private int applicationThreads = 1; |
| |
| /** The size of the byte buffer for the binary data. */ |
| @Option(names = {"--buffer-size"}, |
| description = {"Byte-buffer size for the transferred data (default: ${DEFAULT-VALUE})."}) |
| private int bufferSize = 8192; |
| |
| /** The output byte order of the binary data. */ |
| @Option(names = {"-b", "--byte-order"}, |
| description = {"Byte-order of the transferred data (default: ${DEFAULT-VALUE}).", |
| "Valid values: BIG_ENDIAN, LITTLE_ENDIAN."}) |
| private ByteOrder byteOrder = ByteOrder.nativeOrder(); |
| |
| /** Flag to indicate the output should be bit-reversed. */ |
| @Option(names = {"-r", "--reverse-bits"}, |
| description = {"Reverse the bits in the data (default: ${DEFAULT-VALUE}).", |
| "Note: Generators may fail tests for a reverse sequence " + |
| "when passing using the standard sequence."}) |
| private boolean reverseBits; |
| |
| /** Flag to use the upper 32-bits from the 64-bit long output. */ |
| @Option(names = {"--high-bits"}, |
| description = {"Use the upper 32-bits from the 64-bit long output.", |
| "Takes precedent over --low-bits."}) |
| private boolean longHighBits; |
| |
| /** Flag to use the lower 32-bits from the 64-bit long output. */ |
| @Option(names = {"--low-bits"}, |
| description = {"Use the lower 32-bits from the 64-bit long output."}) |
| private boolean longLowBits; |
| |
| /** Flag to use 64-bit long output. */ |
| @Option(names = {"--raw64"}, |
| description = {"Use 64-bit output (default is 32-bit).", |
| "This requires a 64-bit testing application and native 64-bit generators.", |
| "In 32-bit mode the output uses the upper then lower bits of 64-bit " + |
| "generators sequentially, each appropriately byte reversed for the platform."}) |
| private boolean raw64; |
| |
| /** The random seed as a byte[]. */ |
| @Option(names = {"-x", "--hex-seed"}, |
| description = {"The hex-encoded random seed.", |
| "Seed conversion for multi-byte primitives use little-endian format.", |
| "Use to repeat tests. Not recommended for batch testing."}) |
| private String byteSeed; |
| |
| /** |
| * Flag to indicate the output should be combined with a hashcode from a new object. |
| * This is a method previously used in the |
| * {@link org.apache.commons.rng.simple.internal.SeedFactory SeedFactory}. |
| * |
| * @see System#identityHashCode(Object) |
| */ |
| @Option(names = {"--hashcode"}, |
| description = {"Combine the bits with a hashcode (default: ${DEFAULT-VALUE}).", |
| "System.identityHashCode(new Object()) ^ rng.nextInt()."}) |
| private boolean xorHashCode; |
| |
| /** |
| * Flag to indicate the output should be combined with output from ThreadLocalRandom. |
| */ |
| @Option(names = {"--local-random"}, |
| description = {"Combine the bits with ThreadLocalRandom (default: ${DEFAULT-VALUE}).", |
| "ThreadLocalRandom.current().nextInt() ^ rng.nextInt()."}) |
| private boolean xorThreadLocalRandom; |
| |
| /** |
| * Optional second generator to be combined with the primary generator. |
| */ |
| @Option(names = {"--xor-rng"}, |
| description = {"Combine the bits with a second generator.", |
| "xorRng.nextInt() ^ rng.nextInt().", |
| "Valid values: Any known RandomSource enum value."}) |
| private RandomSource xorRandomSource; |
| |
| /** The flag to indicate a dry run. */ |
| @Option(names = {"--dry-run"}, |
| description = "Perform a dry run where the generators and output files are created " + |
| "but the stress test is not executed.") |
| private boolean dryRun; |
| |
| /** The locl to hold when checking the stop file. */ |
| private ReentrantLock stopFileLock = new ReentrantLock(false); |
| /** The stop file exists flag. This should be read/updated when holding the lock. */ |
| private boolean stopFileExists; |
| /** |
| * The timestamp when the stop file was last checked. |
| * This should be read/updated when holding the lock. |
| */ |
| private long stopFileTimestamp; |
| |
| /** |
| * The output mode for existing files. |
| */ |
| enum OutputMode { |
| /** Error if the files exists. */ |
| ERROR, |
| /** Skip existing files. */ |
| SKIP, |
| /** Append to existing files. */ |
| APPEND, |
| /** Overwrite existing files. */ |
| OVERWRITE |
| } |
| |
| /** |
| * Validates the run command arguments, creates the list of generators and runs the |
| * stress test tasks. |
| */ |
| @Override |
| public Void call() { |
| LogUtils.setLogLevel(reusableOptions.logLevel); |
| ProcessUtils.checkExecutable(executable); |
| ProcessUtils.checkOutputDirectory(fileOutputPrefix); |
| checkStopFileDoesNotExist(); |
| final Iterable<StressTestData> stressTestData = createStressTestData(); |
| printStressTestData(stressTestData); |
| runStressTest(stressTestData); |
| return null; |
| } |
| |
| /** |
| * Initialise the stop file to a default unless specified by the user, then check it |
| * does not currently exist. |
| * |
| * @throws ApplicationException If the stop file exists |
| */ |
| private void checkStopFileDoesNotExist() { |
| if (stopFile == null) { |
| stopFile = new File(fileOutputPrefix + ".stop"); |
| } |
| if (stopFile.exists()) { |
| throw new ApplicationException("Stop file exists: " + stopFile); |
| } |
| } |
| |
| /** |
| * Check if the stop file exists. |
| * |
| * <p>This method is thread-safe. It will log a message if the file exists one time only. |
| * |
| * @return true if the stop file exists |
| */ |
| private boolean isStopFileExists() { |
| stopFileLock.lock(); |
| try { |
| if (!stopFileExists) { |
| // This should hit the filesystem each time it is called. |
| // To prevent this happening a lot when all the first set of tasks run use |
| // a timestamp to limit the check to 1 time each interval. |
| final long timestamp = System.currentTimeMillis(); |
| if (timestamp > stopFileTimestamp) { |
| checkStopFile(timestamp); |
| } |
| } |
| return stopFileExists; |
| } finally { |
| stopFileLock.unlock(); |
| } |
| } |
| |
| /** |
| * Check if the stop file exists. Update the timestamp for the next check. If the stop file |
| * does exists then log a message. |
| * |
| * @param timestamp Timestamp of the last check. |
| */ |
| private void checkStopFile(final long timestamp) { |
| stopFileTimestamp = timestamp + TimeUnit.SECONDS.toMillis(2); |
| stopFileExists = stopFile.exists(); |
| if (stopFileExists) { |
| LogUtils.info("Stop file detected: %s", stopFile); |
| LogUtils.info("No further tasks will start"); |
| } |
| } |
| |
| /** |
| * Creates the test data. |
| * |
| * <p>If the input file is null then a default list is created. |
| * |
| * @return the stress test data |
| * @throws ApplicationException if an error occurred during the file read. |
| */ |
| private Iterable<StressTestData> createStressTestData() { |
| if (generatorsListFile == null) { |
| return new StressTestDataList("", trials); |
| } |
| // Read data into a list |
| try (BufferedReader reader = Files.newBufferedReader(generatorsListFile.toPath())) { |
| return ListCommand.readStressTestData(reader); |
| } catch (final IOException ex) { |
| throw new ApplicationException("Failed to read generators list: " + generatorsListFile, ex); |
| } |
| } |
| |
| /** |
| * Prints the stress test data if the verbosity allows. This is used to debug the list |
| * of generators to be tested. |
| * |
| * @param stressTestData List of generators to be tested. |
| */ |
| private static void printStressTestData(Iterable<StressTestData> stressTestData) { |
| if (!LogUtils.isLoggable(LogUtils.LogLevel.DEBUG)) { |
| return; |
| } |
| try { |
| final StringBuilder sb = new StringBuilder("Testing generators").append(System.lineSeparator()); |
| ListCommand.writeStressTestData(sb, stressTestData); |
| LogUtils.debug(sb.toString()); |
| } catch (final IOException ex) { |
| throw new ApplicationException("Failed to show list of generators", ex); |
| } |
| } |
| |
| /** |
| * Creates the tasks and starts the processes. |
| * |
| * @param stressTestData List of generators to be tested. |
| */ |
| private void runStressTest(Iterable<StressTestData> stressTestData) { |
| final List<String> command = ProcessUtils.buildSubProcessCommand(executable, executableArguments); |
| |
| LogUtils.info("Set-up stress test ..."); |
| |
| // Check existing output files before starting the tasks. |
| final String basePath = fileOutputPrefix.getAbsolutePath(); |
| checkExistingOutputFiles(basePath, stressTestData); |
| |
| final int parallelTasks = getParallelTasks(); |
| |
| final ProgressTracker progressTracker = new ProgressTracker(parallelTasks); |
| final List<Runnable> tasks = createTasks(command, basePath, stressTestData, progressTracker); |
| |
| // Run tasks with parallel execution. |
| final ExecutorService service = Executors.newFixedThreadPool(parallelTasks); |
| |
| LogUtils.info("Running stress test ..."); |
| LogUtils.info("Shutdown by creating stop file: %s", stopFile); |
| progressTracker.setTotal(tasks.size()); |
| final List<Future<?>> taskList = submitTasks(service, tasks); |
| |
| // Wait for completion (ignoring return value). |
| try { |
| for (final Future<?> f : taskList) { |
| try { |
| f.get(); |
| } catch (final ExecutionException ex) { |
| // Log the error. Do not re-throw as other tasks may be processing that |
| // can still complete successfully. |
| LogUtils.error(ex.getCause(), ex.getMessage()); |
| } |
| } |
| } catch (final InterruptedException ex) { |
| // Restore interrupted state... |
| Thread.currentThread().interrupt(); |
| throw new ApplicationException("Unexpected interruption: " + ex.getMessage(), ex); |
| } finally { |
| // Terminate all threads. |
| service.shutdown(); |
| } |
| |
| LogUtils.info("Finished stress test"); |
| } |
| |
| /** |
| * Check for existing output files. |
| * |
| * @param basePath The base path to the output results files. |
| * @param stressTestData List of generators to be tested. |
| * @throws ApplicationException If an output file exists and the output mode is error |
| */ |
| private void checkExistingOutputFiles(String basePath, |
| Iterable<StressTestData> stressTestData) { |
| if (outputMode == StressTestCommand.OutputMode.ERROR) { |
| for (final StressTestData testData : stressTestData) { |
| for (int trial = 1; trial <= testData.getTrials(); trial++) { |
| // Create the output file |
| final File output = createOutputFile(basePath, testData, trial); |
| if (output.exists()) { |
| throw new ApplicationException(createExistingFileMessage(output)); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates the named output file. |
| * |
| * <p>Note: The trial will be combined with the trial offset to create the file name. |
| * |
| * @param basePath The base path to the output results files. |
| * @param testData The test data. |
| * @param trial The trial. |
| * @return the file |
| */ |
| private File createOutputFile(String basePath, |
| StressTestData testData, |
| int trial) { |
| return new File(String.format("%s%s_%d", basePath, testData.getId(), trial + trialOffset)); |
| } |
| |
| /** |
| * Creates the existing file message. |
| * |
| * @param output The output file. |
| * @return the message |
| */ |
| private static String createExistingFileMessage(File output) { |
| return "Existing output file: " + output; |
| } |
| |
| /** |
| * Gets the number of parallel tasks. This uses the number of available processors and |
| * the number of threads to use per task. |
| * |
| * <pre> |
| * threadsPerTask = applicationThreads + (ignoreJavaThread ? 0 : 1) |
| * parallelTasks = ceil(processors / threadsPerTask) |
| * </pre> |
| * |
| * @return the parallel tasks |
| */ |
| private int getParallelTasks() { |
| // Avoid zeros in the fraction numberator and denominator |
| final int availableProcessors = Math.max(1, processors); |
| final int threadsPerTask = Math.max(1, applicationThreads + (ignoreJavaThread ? 0 : 1)); |
| final int parallelTasks = (int) Math.ceil((double) availableProcessors / threadsPerTask); |
| LogUtils.debug("Parallel tasks = %d (%d / %d)", |
| parallelTasks, availableProcessors, threadsPerTask); |
| return parallelTasks; |
| } |
| |
| /** |
| * Create the tasks for the test data. The output file for the sub-process will be |
| * constructed using the base path, the test identifier and the trial number. |
| * |
| * @param command The command for the test application. |
| * @param basePath The base path to the output results files. |
| * @param stressTestData List of generators to be tested. |
| * @param progressTracker Progress tracker. |
| * @return the list of tasks |
| */ |
| private List<Runnable> createTasks(List<String> command, |
| String basePath, |
| Iterable<StressTestData> stressTestData, |
| ProgressTracker progressTracker) { |
| final List<Runnable> tasks = new ArrayList<>(); |
| for (final StressTestData testData : stressTestData) { |
| for (int trial = 1; trial <= testData.getTrials(); trial++) { |
| // Create the output file |
| final File output = createOutputFile(basePath, testData, trial); |
| if (output.exists()) { |
| // In case the file was created since the last check |
| if (outputMode == StressTestCommand.OutputMode.ERROR) { |
| throw new ApplicationException(createExistingFileMessage(output)); |
| } |
| // Log the decision |
| LogUtils.info("%s existing output file: %s", outputMode, output); |
| if (outputMode == StressTestCommand.OutputMode.SKIP) { |
| continue; |
| } |
| } |
| // Create the generator. Explicitly create a seed so it can be recorded. |
| final byte[] seed = createSeed(testData.getRandomSource()); |
| UniformRandomProvider rng = testData.createRNG(seed); |
| |
| // Upper or lower bits from 64-bit generators must be created first. |
| // This will throw if not a 64-bit generator. |
| if (longHighBits) { |
| rng = RNGUtils.createLongUpperBitsIntProvider(rng); |
| } else if (longLowBits) { |
| rng = RNGUtils.createLongLowerBitsIntProvider(rng); |
| } |
| |
| // Combination generators. Mainly used for testing. |
| // These operations maintain the native output type (int/long). |
| if (xorHashCode) { |
| rng = RNGUtils.createHashCodeProvider(rng); |
| } |
| if (xorThreadLocalRandom) { |
| rng = RNGUtils.createThreadLocalRandomProvider(rng); |
| } |
| if (xorRandomSource != null) { |
| rng = RNGUtils.createXorProvider( |
| RandomSource.create(xorRandomSource), |
| rng); |
| } |
| if (reverseBits) { |
| rng = RNGUtils.createReverseBitsProvider(rng); |
| } |
| |
| // ------- |
| // Note: Manipulation of the byte order for the platform is done during output. |
| // ------- |
| |
| // Run the test |
| final Runnable r = new StressTestTask(testData.getRandomSource(), rng, seed, |
| output, command, this, progressTracker); |
| tasks.add(r); |
| } |
| } |
| return tasks; |
| } |
| |
| /** |
| * Creates the seed. This will call {@link RandomSource#createSeed()} unless a hex seed has |
| * been explicitly specified on the command line. |
| * |
| * @param randomSource Random source. |
| * @return the seed |
| */ |
| private byte[] createSeed(RandomSource randomSource) { |
| if (byteSeed != null) { |
| try { |
| return Hex.decodeHex(byteSeed); |
| } catch (IllegalArgumentException ex) { |
| throw new ApplicationException("Invalid hex seed: " + ex.getMessage(), ex); |
| } |
| } |
| return randomSource.createSeed(); |
| } |
| |
| /** |
| * Submit the tasks to the executor service. |
| * |
| * @param service The executor service. |
| * @param tasks The list of tasks. |
| * @return the list of submitted tasks |
| */ |
| private static List<Future<?>> submitTasks(ExecutorService service, |
| List<Runnable> tasks) { |
| final List<Future<?>> taskList = new ArrayList<>(tasks.size()); |
| tasks.forEach(r -> taskList.add(service.submit(r))); |
| return taskList; |
| } |
| |
| /** |
| * Class for reporting total progress of tasks to the console. |
| * |
| * <p>This stores the start and end time of tasks to allow it to estimate the time remaining |
| * for all the tests. |
| */ |
| static class ProgressTracker { |
| /** The interval at which to report progress (in milliseconds). */ |
| private static final long PROGRESS_INTERVAL = 500; |
| |
| /** The total. */ |
| private int total; |
| /** The level of parallelisation. */ |
| private final int parallelTasks; |
| /** The task id. */ |
| private int taskId; |
| /** The start time of tasks (in milliseconds from the epoch). */ |
| private long[] startTimes; |
| /** The durations of all completed tasks (in milliseconds). This is sorted. */ |
| private long[] sortedDurations; |
| /** The number of completed tasks. */ |
| private int completed; |
| /** The timestamp of the next progress report. */ |
| private long nextReportTimestamp; |
| |
| /** |
| * Create a new instance. The total number of tasks must be initialised before use. |
| * |
| * @param parallelTasks The number of parallel tasks. |
| */ |
| ProgressTracker(int parallelTasks) { |
| this.parallelTasks = parallelTasks; |
| } |
| |
| /** |
| * Sets the total number of tasks to track. |
| * |
| * @param total The total tasks. |
| */ |
| void setTotal(int total) { |
| this.total = total; |
| startTimes = new long[total]; |
| sortedDurations = new long[total]; |
| } |
| |
| /** |
| * Submit a task for progress tracking. The task start time is recorded and the |
| * task is allocated an identifier. |
| * |
| * @return the task Id |
| */ |
| int submitTask() { |
| int id; |
| synchronized (this) { |
| final long current = System.currentTimeMillis(); |
| id = taskId++; |
| startTimes[id] = current; |
| reportProgress(current); |
| } |
| return id; |
| } |
| |
| /** |
| * Signal that a task has completed. The task duration will be returned. |
| * |
| * @param id Task Id. |
| * @return the task time in milliseconds |
| */ |
| long endTask(int id) { |
| long duration; |
| synchronized (this) { |
| final long current = System.currentTimeMillis(); |
| duration = current - startTimes[id]; |
| sortedDurations[completed++] = duration; |
| reportProgress(current); |
| } |
| return duration; |
| } |
| |
| /** |
| * Report the progress. This uses the current state and should be done within a |
| * synchronized block. |
| * |
| * @param current Current time (in milliseconds). |
| */ |
| private void reportProgress(long current) { |
| // Determine the current state of tasks |
| final int pending = total - taskId; |
| final int running = taskId - completed; |
| |
| // Report progress in the following conditions: |
| // - All tasks have completed (i.e. the end); or |
| // - The current timestamp is above the next reporting time and either: |
| // -- The number of running tasks is equal to the level of parallel tasks |
| // (i.e. the system is running at capacity, so not the end of a task but the start |
| // of a new one) |
| // -- There are no pending tasks (i.e. the final submission or the end of a final task) |
| if (completed >= total || |
| (current >= nextReportTimestamp && (running == parallelTasks || pending == 0))) { |
| // Report |
| nextReportTimestamp = current + PROGRESS_INTERVAL; |
| final StringBuilder sb = createStringBuilderWithTimestamp(current, pending, running, completed); |
| try (Formatter formatter = new Formatter(sb)) { |
| formatter.format(" (%.2f%%)", 100.0 * completed / total); |
| appendRemaining(sb, current, pending, running); |
| LogUtils.info(sb.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Creates the string builder for the progress message with a timestamp prefix. |
| * |
| * <pre> |
| * [HH:mm:ss] Pending [pending]. Running [running]. Completed [completed] |
| * </pre> |
| * |
| * @param current Current time (in milliseconds) |
| * @param pending Pending tasks. |
| * @param running Running tasks. |
| * @param completed Completed tasks. |
| * @return the string builder |
| */ |
| private static StringBuilder createStringBuilderWithTimestamp(long current, |
| int pending, int running, int completed) { |
| final StringBuilder sb = new StringBuilder(80); |
| // Use local time to adjust for timezone |
| final LocalDateTime time = LocalDateTime.ofInstant( |
| Instant.ofEpochMilli(current), ZoneId.systemDefault()); |
| sb.append('['); |
| append00(sb, time.getHour()).append(':'); |
| append00(sb, time.getMinute()).append(':'); |
| append00(sb, time.getSecond()); |
| return sb.append("] Pending ").append(pending) |
| .append(". Running ").append(running) |
| .append(". Completed ").append(completed); |
| } |
| |
| /** |
| * Compute an estimate of the time remaining and append to the progress. Updates |
| * the estimated time of arrival (ETA). |
| * |
| * @param sb String Builder. |
| * @param current Current time (in milliseconds) |
| * @param pending Pending tasks. |
| * @param running Running tasks. |
| * @return the string builder |
| */ |
| private StringBuilder appendRemaining(StringBuilder sb, long current, int pending, int running) { |
| final long millis = getRemainingTime(current, pending, running); |
| if (millis == 0) { |
| // Unknown. |
| return sb; |
| } |
| |
| // HH:mm:ss format |
| sb.append(". Remaining = "); |
| hms(sb, millis); |
| return sb; |
| } |
| |
| /** |
| * Gets the remaining time (in milliseconds). |
| * |
| * @param current Current time (in milliseconds) |
| * @param pending Pending tasks. |
| * @param running Running tasks. |
| * @return the remaining time |
| */ |
| private long getRemainingTime(long current, int pending, int running) { |
| final long taskTime = getEstimatedTaskTime(); |
| if (taskTime == 0) { |
| // No estimate possible |
| return 0; |
| } |
| |
| // The start times are sorted. This method assumes the most recent start times |
| // are still running tasks. |
| // If this is wrong (more recently submitted tasks finished early) the result |
| // is the estimate is too high. This could be corrected by storing the tasks |
| // that have finished and finding the times of only running tasks. |
| |
| // The remaining time is: |
| // The time for all running tasks to finish |
| // + The time for pending tasks to run |
| |
| // The id of the most recently submitted task. |
| // Guard with a minimum index of zero to get a valid index. |
| final int id = Math.max(0, taskId - 1); |
| |
| // If there is a running task assume the youngest task is still running |
| // and estimate the time left. |
| long millis = (running == 0) ? 0 : getTimeRemaining(taskTime, current, startTimes[id]); |
| |
| // If additional tasks must also be submitted then the time must include |
| // the estimated time for running tasks to finish before new submissions |
| // in the batch can be made. |
| // now |
| // s1 --------------->| |
| // s2 -----------|--------> |
| // s3 -------|------------> |
| // s4 --------------> |
| // |
| |
| // Assume parallel batch execution. |
| // E.g. 3 additional tasks with parallelisation 4 is 0 batches |
| int batches = pending / parallelTasks; |
| millis += batches * taskTime; |
| |
| // Compute the expected end time of the final batch based on it starting when |
| // a currently running task ends. |
| // E.g. 3 remaining tasks requires the end time of the 3rd oldest running task. |
| int remainder = pending % parallelTasks; |
| if (remainder != 0) { |
| // Guard with a minimum index of zero to get a valid index. |
| final int nthOldest = Math.max(0, id - parallelTasks + remainder); |
| millis += getTimeRemaining(taskTime, current, startTimes[nthOldest]); |
| } |
| |
| return millis; |
| } |
| |
| /** |
| * Gets the estimated task time. |
| * |
| * @return the estimated task time |
| */ |
| private long getEstimatedTaskTime() { |
| Arrays.sort(sortedDurations, 0, completed); |
| |
| // Return median of small lists. If no tasks have finished this returns zero. |
| // as the durations is zero initialised. |
| if (completed < 4) { |
| return sortedDurations[completed / 2]; |
| } |
| |
| // Dieharder and BigCrush run in approximately constant time. |
| // Speed varies with the speed of the RNG by about 2-fold, and |
| // for Dieharder it may repeat suspicious tests. |
| // PractRand may fail very fast for bad generators which skews |
| // using the mean or even the median. So look at the longest |
| // running tests. |
| |
| // Find long running tests (>50% of the max run-time) |
| int upper = completed - 1; |
| final long halfMax = sortedDurations[upper] / 2; |
| // Binary search for the approximate cut-off |
| int lower = 0; |
| while (lower + 1 < upper) { |
| int mid = (lower + upper) >>> 1; |
| if (sortedDurations[mid] < halfMax) { |
| lower = mid; |
| } else { |
| upper = mid; |
| } |
| } |
| // Use the median of all tasks within approximately 50% of the max. |
| return sortedDurations[(upper + completed - 1) / 2]; |
| } |
| |
| /** |
| * Gets the time remaining for the task. |
| * |
| * @param taskTime Estimated task time. |
| * @param current Current time. |
| * @param startTime Start time. |
| * @return the time remaining |
| */ |
| private static long getTimeRemaining(long taskTime, long current, long startTime) { |
| final long endTime = startTime + taskTime; |
| // Ensure the time is positive in the case where the estimate is too low. |
| return Math.max(0, endTime - current); |
| } |
| |
| /** |
| * Append the milliseconds using {@code HH::mm:ss} format. |
| * |
| * @param sb String Builder. |
| * @param millis Milliseconds. |
| * @return the string builder |
| */ |
| static StringBuilder hms(StringBuilder sb, final long millis) { |
| final long hours = TimeUnit.MILLISECONDS.toHours(millis); |
| long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); |
| long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); |
| // Truncate to interval [0,59] |
| seconds -= TimeUnit.MINUTES.toSeconds(minutes); |
| minutes -= TimeUnit.HOURS.toMinutes(hours); |
| |
| append00(sb, hours).append(':'); |
| append00(sb, minutes).append(':'); |
| return append00(sb, seconds); |
| } |
| |
| /** |
| * Append the ticks to the string builder in the format {@code %02d}. |
| * |
| * @param sb String Builder. |
| * @param ticks Ticks. |
| * @return the string builder |
| */ |
| static StringBuilder append00(StringBuilder sb, long ticks) { |
| if (ticks == 0) { |
| sb.append("00"); |
| } else { |
| if (ticks < 10) { |
| sb.append('0'); |
| } |
| sb.append(ticks); |
| } |
| return sb; |
| } |
| } |
| |
| /** |
| * Pipes random numbers to the standard input of an analyzer executable. |
| */ |
| private static class StressTestTask implements Runnable { |
| /** Comment prefix. */ |
| private static final String C = "# "; |
| /** New line. */ |
| private static final String N = System.lineSeparator(); |
| /** The date format. */ |
| private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; |
| /** The SI units for bytes in increments of 10^3. */ |
| private static final String[] SI_UNITS = {"B", "kB", "MB", "GB", "TB", "PB", "EB"}; |
| /** The SI unit base for bytes (10^3). */ |
| private static final long SI_UNIT_BASE = 1000; |
| |
| /** The random source. */ |
| private final RandomSource randomSource; |
| /** RNG to be tested. */ |
| private final UniformRandomProvider rng; |
| /** The seed used to create the RNG. */ |
| private final byte[] seed; |
| /** Output report file of the sub-process. */ |
| private final File output; |
| /** The sub-process command to run. */ |
| private final List<String> command; |
| /** The stress test command. */ |
| private final StressTestCommand cmd; |
| /** The progress tracker. */ |
| private final ProgressTracker progressTracker; |
| |
| /** The count of bytes used by the sub-process. */ |
| private long bytesUsed; |
| |
| /** |
| * Creates the task. |
| * |
| * @param randomSource The random source. |
| * @param rng RNG to be tested. |
| * @param seed The seed used to create the RNG. |
| * @param output Output report file. |
| * @param command The sub-process command to run. |
| * @param cmd The run command. |
| * @param progressTracker The progress tracker. |
| */ |
| StressTestTask(RandomSource randomSource, |
| UniformRandomProvider rng, |
| byte[] seed, |
| File output, |
| List<String> command, |
| StressTestCommand cmd, |
| ProgressTracker progressTracker) { |
| this.randomSource = randomSource; |
| this.rng = rng; |
| this.seed = seed; |
| this.output = output; |
| this.command = command; |
| this.cmd = cmd; |
| this.progressTracker = progressTracker; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void run() { |
| if (cmd.isStopFileExists()) { |
| // Do nothing |
| return; |
| } |
| |
| try { |
| printHeader(); |
| |
| Object exitValue; |
| long millis; |
| final int taskId = progressTracker.submitTask(); |
| if (cmd.dryRun) { |
| // Do not do anything. Ignore the runtime. |
| exitValue = "N/A"; |
| progressTracker.endTask(taskId); |
| millis = 0; |
| } else { |
| // Run the sub-process |
| exitValue = runSubProcess(); |
| millis = progressTracker.endTask(taskId); |
| } |
| |
| printFooter(millis, exitValue); |
| |
| } catch (final IOException ex) { |
| throw new ApplicationException("Failed to run task: " + ex.getMessage(), ex); |
| } |
| } |
| |
| /** |
| * Run the analyzer sub-process command. |
| * |
| * @return The exit value. |
| * @throws IOException Signals that an I/O exception has occurred. |
| */ |
| private Integer runSubProcess() throws IOException { |
| // Start test suite. |
| final ProcessBuilder builder = new ProcessBuilder(command); |
| builder.redirectOutput(ProcessBuilder.Redirect.appendTo(output)); |
| builder.redirectErrorStream(true); |
| final Process testingProcess = builder.start(); |
| |
| // Use a custom data output to write the RNG. |
| try (RngDataOutput sink = RNGUtils.createDataOutput(rng, cmd.raw64, |
| testingProcess.getOutputStream(), cmd.bufferSize, cmd.byteOrder)) { |
| for (;;) { |
| sink.write(rng); |
| bytesUsed++; |
| } |
| } catch (final IOException ignored) { |
| // Hopefully getting here when the analyzing software terminates. |
| } |
| |
| bytesUsed *= cmd.bufferSize; |
| |
| // Get the exit value. |
| // Wait for up to 60 seconds. |
| // If an application does not exit after this time then something is wrong. |
| // Dieharder and TestU01 BigCrush exit within 1 second. |
| // PractRand has been observed to take longer than 1 second. It calls std::exit(0) |
| // when failing a test so the length of time may be related to freeing memory. |
| return ProcessUtils.getExitValue(testingProcess, TimeUnit.SECONDS.toMillis(60)); |
| } |
| |
| /** |
| * Prints the header. |
| * |
| * @throws IOException if there was a problem opening or writing to the |
| * {@code output} file. |
| */ |
| private void printHeader() throws IOException { |
| final StringBuilder sb = new StringBuilder(200); |
| sb.append(C).append(N) |
| .append(C).append("RandomSource: ").append(randomSource.name()).append(N) |
| .append(C).append("RNG: ").append(rng.toString()).append(N) |
| .append(C).append("Seed: ").append(Hex.encodeHex(seed)).append(N) |
| .append(C).append(N) |
| |
| // Match the output of 'java -version', e.g. |
| // java version "1.8.0_131" |
| // Java(TM) SE Runtime Environment (build 1.8.0_131-b11) |
| // Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode) |
| .append(C).append("Java: ").append(System.getProperty("java.version")).append(N); |
| appendNameAndVersion(sb, "Runtime", "java.runtime.name", "java.runtime.version"); |
| appendNameAndVersion(sb, "JVM", "java.vm.name", "java.vm.version", "java.vm.info"); |
| |
| sb.append(C).append("OS: ").append(System.getProperty("os.name")) |
| .append(' ').append(System.getProperty("os.version")) |
| .append(' ').append(System.getProperty("os.arch")).append(N) |
| .append(C).append("Native byte-order: ").append(ByteOrder.nativeOrder()).append(N) |
| .append(C).append("Output byte-order: ").append(cmd.byteOrder).append(N); |
| if (rng instanceof RandomLongSource) { |
| sb.append(C).append("64-bit output: ").append(cmd.raw64).append(N); |
| } |
| sb.append(C).append(N) |
| .append(C).append("Analyzer: "); |
| for (final String s : command) { |
| sb.append(s).append(' '); |
| } |
| sb.append(N) |
| .append(C).append(N); |
| |
| appendDate(sb, "Start").append(C).append(N); |
| |
| write(sb, output, cmd.outputMode == StressTestCommand.OutputMode.APPEND); |
| } |
| |
| /** |
| * Prints the footer. |
| * |
| * @param millis Duration of the run (in milliseconds). |
| * @param exitValue The process exit value. |
| * @throws IOException if there was a problem opening or writing to the |
| * {@code output} file. |
| */ |
| private void printFooter(long millis, |
| Object exitValue) throws IOException { |
| final StringBuilder sb = new StringBuilder(200); |
| sb.append(C).append(N); |
| |
| appendDate(sb, "End").append(C).append(N); |
| |
| sb.append(C).append("Exit value: ").append(exitValue).append(N) |
| .append(C).append("Bytes used: ").append(bytesUsed) |
| .append(" >= 2^").append(log2(bytesUsed)) |
| .append(" (").append(bytesToString(bytesUsed)).append(')').append(N) |
| .append(C).append(N); |
| |
| final double duration = millis * 1e-3 / 60; |
| sb.append(C).append("Test duration: ").append(duration).append(" minutes").append(N) |
| .append(C).append(N); |
| |
| write(sb, output, true); |
| } |
| |
| /** |
| * Write the string builder to the output file. |
| * |
| * @param sb The string builder. |
| * @param output The output file. |
| * @param append Set to {@code true} to append to the file. |
| * @throws IOException Signals that an I/O exception has occurred. |
| */ |
| private static void write(StringBuilder sb, |
| File output, |
| boolean append) throws IOException { |
| try (BufferedWriter w = append ? |
| Files.newBufferedWriter(output.toPath(), StandardOpenOption.APPEND) : |
| Files.newBufferedWriter(output.toPath())) { |
| w.write(sb.toString()); |
| } |
| } |
| |
| /** |
| * Append prefix and then name and version from System properties, finished with |
| * a new line. The format is: |
| * |
| * <pre>{@code # <prefix>: <name> (build <version>[, <info>, ...])}</pre> |
| * |
| * @param sb The string builder. |
| * @param prefix The prefix. |
| * @param nameKey The name key. |
| * @param versionKey The version key. |
| * @param infoKeys The additional information keys. |
| * @return the StringBuilder. |
| */ |
| private static StringBuilder appendNameAndVersion(StringBuilder sb, |
| String prefix, |
| String nameKey, |
| String versionKey, |
| String... infoKeys) { |
| appendPrefix(sb, prefix) |
| .append(System.getProperty(nameKey, "?")) |
| .append(" (build ") |
| .append(System.getProperty(versionKey, "?")); |
| for (final String key : infoKeys) { |
| final String value = System.getProperty(key, ""); |
| if (!value.isEmpty()) { |
| sb.append(", ").append(value); |
| } |
| } |
| return sb.append(')').append(N); |
| } |
| |
| /** |
| * Append a comment with the current date to the {@link StringBuilder}, finished with |
| * a new line. The format is: |
| * |
| * <pre>{@code # <prefix>: yyyy-MM-dd HH:mm:ss}</pre> |
| * |
| * @param sb The StringBuilder. |
| * @param prefix The prefix used before the formatted date, e.g. "Start". |
| * @return the StringBuilder. |
| */ |
| private static StringBuilder appendDate(StringBuilder sb, |
| String prefix) { |
| // Use local date format. It is not thread safe. |
| final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); |
| return appendPrefix(sb, prefix).append(dateFormat.format(new Date())).append(N); |
| } |
| |
| /** |
| * Append a comment with the current date to the {@link StringBuilder}. |
| * |
| * <pre> |
| * {@code # <prefix>: yyyy-MM-dd HH:mm:ss} |
| * </pre> |
| * |
| * @param sb The StringBuilder. |
| * @param prefix The prefix used before the formatted date, e.g. "Start". |
| * @return the StringBuilder. |
| */ |
| private static StringBuilder appendPrefix(StringBuilder sb, |
| String prefix) { |
| return sb.append(C).append(prefix).append(": "); |
| } |
| |
| /** |
| * Convert bytes to a human readable string. Example output: |
| * |
| * <pre> |
| * SI |
| * 0: 0 B |
| * 27: 27 B |
| * 999: 999 B |
| * 1000: 1.0 kB |
| * 1023: 1.0 kB |
| * 1024: 1.0 kB |
| * 1728: 1.7 kB |
| * 110592: 110.6 kB |
| * 7077888: 7.1 MB |
| * 452984832: 453.0 MB |
| * 28991029248: 29.0 GB |
| * 1855425871872: 1.9 TB |
| * 9223372036854775807: 9.2 EB (Long.MAX_VALUE) |
| * </pre> |
| * |
| * @param bytes the bytes |
| * @return the string |
| * @see <a |
| * href="https://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java">How |
| * to convert byte size into human readable format in java?</a> |
| */ |
| static String bytesToString(long bytes) { |
| // When using the smallest unit no decimal point is needed, because it's the exact number. |
| if (bytes < ONE_THOUSAND) { |
| return bytes + " " + SI_UNITS[0]; |
| } |
| |
| final int exponent = (int) (Math.log(bytes) / Math.log(SI_UNIT_BASE)); |
| final String unit = SI_UNITS[exponent]; |
| return String.format(Locale.US, "%.1f %s", bytes / Math.pow(SI_UNIT_BASE, exponent), unit); |
| } |
| |
| /** |
| * Return the log2 of a {@code long} value rounded down to a power of 2. |
| * |
| * @param x the value |
| * @return {@code floor(log2(x))} |
| */ |
| static int log2(long x) { |
| return 63 - Long.numberOfLeadingZeros(x); |
| } |
| } |
| } |