View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.rng.examples.stress;
18  
19  import org.apache.commons.rng.UniformRandomProvider;
20  import org.apache.commons.rng.core.source64.RandomLongSource;
21  import org.apache.commons.rng.simple.RandomSource;
22  
23  import picocli.CommandLine.Command;
24  import picocli.CommandLine.Mixin;
25  import picocli.CommandLine.Option;
26  import picocli.CommandLine.Parameters;
27  
28  import java.io.BufferedReader;
29  import java.io.BufferedWriter;
30  import java.io.File;
31  import java.io.IOException;
32  import java.nio.ByteOrder;
33  import java.nio.file.Files;
34  import java.nio.file.StandardOpenOption;
35  import java.text.SimpleDateFormat;
36  import java.time.Instant;
37  import java.time.LocalDateTime;
38  import java.time.ZoneId;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Date;
42  import java.util.Formatter;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.concurrent.Callable;
46  import java.util.concurrent.ExecutionException;
47  import java.util.concurrent.ExecutorService;
48  import java.util.concurrent.Executors;
49  import java.util.concurrent.Future;
50  import java.util.concurrent.TimeUnit;
51  import java.util.concurrent.locks.ReentrantLock;
52  
53  /**
54   * Specification for the "stress" command.
55   *
56   * <p>This command loads a list of random generators and tests each generator by
57   * piping the values returned by its {@link UniformRandomProvider#nextInt()}
58   * method to a program that reads {@code int} values from its standard input and
59   * writes an analysis report to standard output.</p>
60   */
61  @Command(name = "stress",
62           description = {"Run repeat trials of random data generators using a provided test application.",
63                          "Data is transferred to the application sub-process via standard input."})
64  class StressTestCommand implements Callable<Void> {
65      /** 1000. Any value below this can be exactly represented to 3 significant figures. */
66      private static final int ONE_THOUSAND = 1000;
67  
68      /** The standard options. */
69      @Mixin
70      private StandardOptions reusableOptions;
71  
72      /** The executable. */
73      @Parameters(index = "0",
74                  description = "The stress test executable.")
75      private File executable;
76  
77      /** The executable arguments. */
78      @Parameters(index = "1..*",
79                  description = "The arguments to pass to the executable.",
80                  paramLabel = "<argument>")
81      private List<String> executableArguments = new ArrayList<>();
82  
83      /** The file output prefix. */
84      @Option(names = {"--prefix"},
85              description = "Results file prefix (default: ${DEFAULT-VALUE}).")
86      private File fileOutputPrefix = new File("test_");
87  
88      /** The stop file. */
89      @Option(names = {"--stop-file"},
90              description = {"Stop file (default: <Results file prefix>.stop).",
91                             "When created it will prevent new tasks from starting " +
92                             "but running tasks will complete."})
93      private File stopFile;
94  
95      /** The output mode for existing files. */
96      @Option(names = {"-o", "--output-mode"},
97              description = {"Output mode for existing files (default: ${DEFAULT-VALUE}).",
98                             "Valid values: ${COMPLETION-CANDIDATES}."})
99      private StressTestCommand.OutputMode outputMode = OutputMode.ERROR;
100 
101     /** The list of random generators. */
102     @Option(names = {"-l", "--list"},
103             description = {"List of random generators.",
104                            "The default list is all known generators."},
105             paramLabel = "<genList>")
106     private File generatorsListFile;
107 
108     /** The number of trials to put in the template list of random generators. */
109     @Option(names = {"-t", "--trials"},
110             description = {"The number of trials for each random generator.",
111                            "Used only for the default list (default: ${DEFAULT-VALUE})."})
112     private int trials = 1;
113 
114     /** The trial offset. */
115     @Option(names = {"--trial-offset"},
116             description = {"Offset to add to the trial number for output files (default: ${DEFAULT-VALUE}).",
117                            "Use for parallel tests with the same output prefix."})
118     private int trialOffset;
119 
120     /** The number of available processors. */
121     @Option(names = {"-p", "--processors"},
122             description = {"Number of available processors (default: ${DEFAULT-VALUE}).",
123                            "Number of concurrent tasks = ceil(processors / threadsPerTask)",
124                            "threadsPerTask = applicationThreads + (ignoreJavaThread ? 0 : 1)"})
125     private int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
126 
127     /** The number of threads to use for each test task. */
128     @Option(names = {"--ignore-java-thread"},
129             description = {"Ignore the java RNG thread when computing concurrent tasks."})
130     private boolean ignoreJavaThread;
131 
132     /** The number of threads to use for each testing application. */
133     @Option(names = {"--threads"},
134             description = {"Number of threads to use for each application (default: ${DEFAULT-VALUE}).",
135                            "Total threads per task includes an optional java thread."})
136     private int applicationThreads = 1;
137 
138     /** The size of the byte buffer for the binary data. */
139     @Option(names = {"--buffer-size"},
140             description = {"Byte-buffer size for the transferred data (default: ${DEFAULT-VALUE})."})
141     private int bufferSize = 8192;
142 
143     /** The output byte order of the binary data. */
144     @Option(names = {"-b", "--byte-order"},
145             description = {"Byte-order of the transferred data (default: ${DEFAULT-VALUE}).",
146                            "Valid values: BIG_ENDIAN, LITTLE_ENDIAN."})
147     private ByteOrder byteOrder = ByteOrder.nativeOrder();
148 
149     /** Flag to indicate the output should be bit-reversed. */
150     @Option(names = {"-r", "--reverse-bits"},
151             description = {"Reverse the bits in the data (default: ${DEFAULT-VALUE}).",
152                            "Note: Generators may fail tests for a reverse sequence " +
153                            "when passing using the standard sequence."})
154     private boolean reverseBits;
155 
156     /** Flag to use the upper 32-bits from the 64-bit long output. */
157     @Option(names = {"--high-bits"},
158             description = {"Use the upper 32-bits from the 64-bit long output.",
159                            "Takes precedent over --low-bits."})
160     private boolean longHighBits;
161 
162     /** Flag to use the lower 32-bits from the 64-bit long output. */
163     @Option(names = {"--low-bits"},
164             description = {"Use the lower 32-bits from the 64-bit long output."})
165     private boolean longLowBits;
166 
167     /** Flag to use 64-bit long output. */
168     @Option(names = {"--raw64"},
169             description = {"Use 64-bit output (default is 32-bit).",
170                            "This requires a 64-bit testing application and native 64-bit generators.",
171                            "In 32-bit mode the output uses the upper then lower bits of 64-bit " +
172                            "generators sequentially, each appropriately byte reversed for the platform."})
173     private boolean raw64;
174 
175     /** The random seed as a byte[]. */
176     @Option(names = {"-x", "--hex-seed"},
177             description = {"The hex-encoded random seed.",
178                            "Seed conversion for multi-byte primitives use little-endian format.",
179                            "Use to repeat tests. Not recommended for batch testing."})
180     private String byteSeed;
181 
182     /**
183      * Flag to indicate the output should be combined with a hashcode from a new object.
184      * This is a method previously used in the
185      * {@link org.apache.commons.rng.simple.internal.SeedFactory SeedFactory}.
186      *
187      * @see System#identityHashCode(Object)
188      */
189     @Option(names = {"--hashcode"},
190             description = {"Combine the bits with a hashcode (default: ${DEFAULT-VALUE}).",
191                            "System.identityHashCode(new Object()) ^ rng.nextInt()."})
192     private boolean xorHashCode;
193 
194     /**
195      * Flag to indicate the output should be combined with output from ThreadLocalRandom.
196      */
197     @Option(names = {"--local-random"},
198             description = {"Combine the bits with ThreadLocalRandom (default: ${DEFAULT-VALUE}).",
199                            "ThreadLocalRandom.current().nextInt() ^ rng.nextInt()."})
200     private boolean xorThreadLocalRandom;
201 
202     /**
203      * Optional second generator to be combined with the primary generator.
204      */
205     @Option(names = {"--xor-rng"},
206             description = {"Combine the bits with a second generator.",
207                            "xorRng.nextInt() ^ rng.nextInt().",
208                            "Valid values: Any known RandomSource enum value."})
209     private RandomSource xorRandomSource;
210 
211     /** The flag to indicate a dry run. */
212     @Option(names = {"--dry-run"},
213             description = "Perform a dry run where the generators and output files are created " +
214                           "but the stress test is not executed.")
215     private boolean dryRun;
216 
217     /** The locl to hold when checking the stop file. */
218     private ReentrantLock stopFileLock = new ReentrantLock(false);
219     /** The stop file exists flag. This should be read/updated when holding the lock. */
220     private boolean stopFileExists;
221     /**
222      * The timestamp when the stop file was last checked.
223      * This should be read/updated when holding the lock.
224      */
225     private long stopFileTimestamp;
226 
227     /**
228      * The output mode for existing files.
229      */
230     enum OutputMode {
231         /** Error if the files exists. */
232         ERROR,
233         /** Skip existing files. */
234         SKIP,
235         /** Append to existing files. */
236         APPEND,
237         /** Overwrite existing files. */
238         OVERWRITE
239     }
240 
241     /**
242      * Validates the run command arguments, creates the list of generators and runs the
243      * stress test tasks.
244      */
245     @Override
246     public Void call() {
247         LogUtils.setLogLevel(reusableOptions.logLevel);
248         ProcessUtils.checkExecutable(executable);
249         ProcessUtils.checkOutputDirectory(fileOutputPrefix);
250         checkStopFileDoesNotExist();
251         final Iterable<StressTestData> stressTestData = createStressTestData();
252         printStressTestData(stressTestData);
253         runStressTest(stressTestData);
254         return null;
255     }
256 
257     /**
258      * Initialise the stop file to a default unless specified by the user, then check it
259      * does not currently exist.
260      *
261      * @throws ApplicationException If the stop file exists
262      */
263     private void checkStopFileDoesNotExist() {
264         if (stopFile == null) {
265             stopFile = new File(fileOutputPrefix + ".stop");
266         }
267         if (stopFile.exists()) {
268             throw new ApplicationException("Stop file exists: " + stopFile);
269         }
270     }
271 
272     /**
273      * Check if the stop file exists.
274      *
275      * <p>This method is thread-safe. It will log a message if the file exists one time only.
276      *
277      * @return true if the stop file exists
278      */
279     private boolean isStopFileExists() {
280         stopFileLock.lock();
281         try {
282             if (!stopFileExists) {
283                 // This should hit the filesystem each time it is called.
284                 // To prevent this happening a lot when all the first set of tasks run use
285                 // a timestamp to limit the check to 1 time each interval.
286                 final long timestamp = System.currentTimeMillis();
287                 if (timestamp > stopFileTimestamp) {
288                     checkStopFile(timestamp);
289                 }
290             }
291             return stopFileExists;
292         } finally {
293             stopFileLock.unlock();
294         }
295     }
296 
297     /**
298      * Check if the stop file exists. Update the timestamp for the next check. If the stop file
299      * does exists then log a message.
300      *
301      * @param timestamp Timestamp of the last check.
302      */
303     private void checkStopFile(final long timestamp) {
304         stopFileTimestamp = timestamp + TimeUnit.SECONDS.toMillis(2);
305         stopFileExists = stopFile.exists();
306         if (stopFileExists) {
307             LogUtils.info("Stop file detected: %s", stopFile);
308             LogUtils.info("No further tasks will start");
309         }
310     }
311 
312     /**
313      * Creates the test data.
314      *
315      * <p>If the input file is null then a default list is created.
316      *
317      * @return the stress test data
318      * @throws ApplicationException if an error occurred during the file read.
319      */
320     private Iterable<StressTestData> createStressTestData() {
321         if (generatorsListFile == null) {
322             return new StressTestDataList("", trials);
323         }
324         // Read data into a list
325         try (BufferedReader reader = Files.newBufferedReader(generatorsListFile.toPath())) {
326             return ListCommand.readStressTestData(reader);
327         } catch (final IOException ex) {
328             throw new ApplicationException("Failed to read generators list: " + generatorsListFile, ex);
329         }
330     }
331 
332     /**
333      * Prints the stress test data if the verbosity allows. This is used to debug the list
334      * of generators to be tested.
335      *
336      * @param stressTestData List of generators to be tested.
337      */
338     private static void printStressTestData(Iterable<StressTestData> stressTestData) {
339         if (!LogUtils.isLoggable(LogUtils.LogLevel.DEBUG)) {
340             return;
341         }
342         try {
343             final StringBuilder sb = new StringBuilder("Testing generators").append(System.lineSeparator());
344             ListCommand.writeStressTestData(sb, stressTestData);
345             LogUtils.debug(sb.toString());
346         } catch (final IOException ex) {
347             throw new ApplicationException("Failed to show list of generators", ex);
348         }
349     }
350 
351     /**
352      * Creates the tasks and starts the processes.
353      *
354      * @param stressTestData List of generators to be tested.
355      */
356     private void runStressTest(Iterable<StressTestData> stressTestData) {
357         final List<String> command = ProcessUtils.buildSubProcessCommand(executable, executableArguments);
358 
359         LogUtils.info("Set-up stress test ...");
360 
361         // Check existing output files before starting the tasks.
362         final String basePath = fileOutputPrefix.getAbsolutePath();
363         checkExistingOutputFiles(basePath, stressTestData);
364 
365         final int parallelTasks = getParallelTasks();
366 
367         final ProgressTracker progressTracker = new ProgressTracker(parallelTasks);
368         final List<Runnable> tasks = createTasks(command, basePath, stressTestData, progressTracker);
369 
370         // Run tasks with parallel execution.
371         final ExecutorService service = Executors.newFixedThreadPool(parallelTasks);
372 
373         LogUtils.info("Running stress test ...");
374         LogUtils.info("Shutdown by creating stop file: %s",  stopFile);
375         progressTracker.setTotal(tasks.size());
376         final List<Future<?>> taskList = submitTasks(service, tasks);
377 
378         // Wait for completion (ignoring return value).
379         try {
380             for (final Future<?> f : taskList) {
381                 try {
382                     f.get();
383                 } catch (final ExecutionException ex) {
384                     // Log the error. Do not re-throw as other tasks may be processing that
385                     // can still complete successfully.
386                     LogUtils.error(ex.getCause(), ex.getMessage());
387                 }
388             }
389         } catch (final InterruptedException ex) {
390             // Restore interrupted state...
391             Thread.currentThread().interrupt();
392             throw new ApplicationException("Unexpected interruption: " + ex.getMessage(), ex);
393         } finally {
394             // Terminate all threads.
395             service.shutdown();
396         }
397 
398         LogUtils.info("Finished stress test");
399     }
400 
401     /**
402      * Check for existing output files.
403      *
404      * @param basePath The base path to the output results files.
405      * @param stressTestData List of generators to be tested.
406      * @throws ApplicationException If an output file exists and the output mode is error
407      */
408     private void checkExistingOutputFiles(String basePath,
409                                           Iterable<StressTestData> stressTestData) {
410         if (outputMode == StressTestCommand.OutputMode.ERROR) {
411             for (final StressTestData testData : stressTestData) {
412                 for (int trial = 1; trial <= testData.getTrials(); trial++) {
413                     // Create the output file
414                     final File output = createOutputFile(basePath, testData, trial);
415                     if (output.exists()) {
416                         throw new ApplicationException(createExistingFileMessage(output));
417                     }
418                 }
419             }
420         }
421     }
422 
423     /**
424      * Creates the named output file.
425      *
426      * <p>Note: The trial will be combined with the trial offset to create the file name.
427      *
428      * @param basePath The base path to the output results files.
429      * @param testData The test data.
430      * @param trial The trial.
431      * @return the file
432      */
433     private File createOutputFile(String basePath,
434                                   StressTestData testData,
435                                   int trial) {
436         return new File(String.format("%s%s_%d", basePath, testData.getId(), trial + trialOffset));
437     }
438 
439     /**
440      * Creates the existing file message.
441      *
442      * @param output The output file.
443      * @return the message
444      */
445     private static String createExistingFileMessage(File output) {
446         return "Existing output file: " + output;
447     }
448 
449     /**
450      * Gets the number of parallel tasks. This uses the number of available processors and
451      * the number of threads to use per task.
452      *
453      * <pre>
454      * threadsPerTask = applicationThreads + (ignoreJavaThread ? 0 : 1)
455      * parallelTasks = ceil(processors / threadsPerTask)
456      * </pre>
457      *
458      * @return the parallel tasks
459      */
460     private int getParallelTasks() {
461         // Avoid zeros in the fraction numberator and denominator
462         final int availableProcessors = Math.max(1, processors);
463         final int threadsPerTask = Math.max(1, applicationThreads + (ignoreJavaThread ? 0 : 1));
464         final int parallelTasks = (int) Math.ceil((double) availableProcessors / threadsPerTask);
465         LogUtils.debug("Parallel tasks = %d (%d / %d)",
466             parallelTasks, availableProcessors, threadsPerTask);
467         return parallelTasks;
468     }
469 
470     /**
471      * Create the tasks for the test data. The output file for the sub-process will be
472      * constructed using the base path, the test identifier and the trial number.
473      *
474      * @param command The command for the test application.
475      * @param basePath The base path to the output results files.
476      * @param stressTestData List of generators to be tested.
477      * @param progressTracker Progress tracker.
478      * @return the list of tasks
479      */
480     private List<Runnable> createTasks(List<String> command,
481                                        String basePath,
482                                        Iterable<StressTestData> stressTestData,
483                                        ProgressTracker progressTracker) {
484         final List<Runnable> tasks = new ArrayList<>();
485         for (final StressTestData testData : stressTestData) {
486             for (int trial = 1; trial <= testData.getTrials(); trial++) {
487                 // Create the output file
488                 final File output = createOutputFile(basePath, testData, trial);
489                 if (output.exists()) {
490                     // In case the file was created since the last check
491                     if (outputMode == StressTestCommand.OutputMode.ERROR) {
492                         throw new ApplicationException(createExistingFileMessage(output));
493                     }
494                     // Log the decision
495                     LogUtils.info("%s existing output file: %s", outputMode, output);
496                     if (outputMode == StressTestCommand.OutputMode.SKIP) {
497                         continue;
498                     }
499                 }
500                 // Create the generator. Explicitly create a seed so it can be recorded.
501                 final byte[] seed = createSeed(testData.getRandomSource());
502                 UniformRandomProvider rng = testData.createRNG(seed);
503 
504                 // Upper or lower bits from 64-bit generators must be created first.
505                 // This will throw if not a 64-bit generator.
506                 if (longHighBits) {
507                     rng = RNGUtils.createLongUpperBitsIntProvider(rng);
508                 } else if (longLowBits) {
509                     rng = RNGUtils.createLongLowerBitsIntProvider(rng);
510                 }
511 
512                 // Combination generators. Mainly used for testing.
513                 // These operations maintain the native output type (int/long).
514                 if (xorHashCode) {
515                     rng = RNGUtils.createHashCodeProvider(rng);
516                 }
517                 if (xorThreadLocalRandom) {
518                     rng = RNGUtils.createThreadLocalRandomProvider(rng);
519                 }
520                 if (xorRandomSource != null) {
521                     rng = RNGUtils.createXorProvider(
522                             RandomSource.create(xorRandomSource),
523                             rng);
524                 }
525                 if (reverseBits) {
526                     rng = RNGUtils.createReverseBitsProvider(rng);
527                 }
528 
529                 // -------
530                 // Note: Manipulation of the byte order for the platform is done during output.
531                 // -------
532 
533                 // Run the test
534                 final Runnable r = new StressTestTask(testData.getRandomSource(), rng, seed,
535                                                       output, command, this, progressTracker);
536                 tasks.add(r);
537             }
538         }
539         return tasks;
540     }
541 
542     /**
543      * Creates the seed. This will call {@link RandomSource#createSeed()} unless a hex seed has
544      * been explicitly specified on the command line.
545      *
546      * @param randomSource Random source.
547      * @return the seed
548      */
549     private byte[] createSeed(RandomSource randomSource) {
550         if (byteSeed != null) {
551             try {
552                 return Hex.decodeHex(byteSeed);
553             } catch (IllegalArgumentException ex) {
554                 throw new ApplicationException("Invalid hex seed: " + ex.getMessage(), ex);
555             }
556         }
557         return randomSource.createSeed();
558     }
559 
560     /**
561      * Submit the tasks to the executor service.
562      *
563      * @param service The executor service.
564      * @param tasks The list of tasks.
565      * @return the list of submitted tasks
566      */
567     private static List<Future<?>> submitTasks(ExecutorService service,
568                                                List<Runnable> tasks) {
569         final List<Future<?>> taskList = new ArrayList<>(tasks.size());
570         tasks.forEach(r -> taskList.add(service.submit(r)));
571         return taskList;
572     }
573 
574     /**
575      * Class for reporting total progress of tasks to the console.
576      *
577      * <p>This stores the start and end time of tasks to allow it to estimate the time remaining
578      * for all the tests.
579      */
580     static class ProgressTracker {
581         /** The interval at which to report progress (in milliseconds). */
582         private static final long PROGRESS_INTERVAL = 500;
583 
584         /** The total. */
585         private int total;
586         /** The level of parallelisation. */
587         private final int parallelTasks;
588         /** The task id. */
589         private int taskId;
590         /** The start time of tasks (in milliseconds from the epoch). */
591         private long[] startTimes;
592         /** The durations of all completed tasks (in milliseconds). This is sorted. */
593         private long[] sortedDurations;
594         /** The number of completed tasks. */
595         private int completed;
596         /** The timestamp of the next progress report. */
597         private long nextReportTimestamp;
598 
599         /**
600          * Create a new instance. The total number of tasks must be initialised before use.
601          *
602          * @param parallelTasks The number of parallel tasks.
603          */
604         ProgressTracker(int parallelTasks) {
605             this.parallelTasks = parallelTasks;
606         }
607 
608         /**
609          * Sets the total number of tasks to track.
610          *
611          * @param total The total tasks.
612          */
613         void setTotal(int total) {
614             this.total = total;
615             startTimes = new long[total];
616             sortedDurations = new long[total];
617         }
618 
619         /**
620          * Submit a task for progress tracking. The task start time is recorded and the
621          * task is allocated an identifier.
622          *
623          * @return the task Id
624          */
625         int submitTask() {
626             int id;
627             synchronized (this) {
628                 final long current = System.currentTimeMillis();
629                 id = taskId++;
630                 startTimes[id] = current;
631                 reportProgress(current);
632             }
633             return id;
634         }
635 
636         /**
637          * Signal that a task has completed. The task duration will be returned.
638          *
639          * @param id Task Id.
640          * @return the task time in milliseconds
641          */
642         long endTask(int id) {
643             long duration;
644             synchronized (this) {
645                 final long current = System.currentTimeMillis();
646                 duration = current - startTimes[id];
647                 sortedDurations[completed++] = duration;
648                 reportProgress(current);
649             }
650             return duration;
651         }
652 
653         /**
654          * Report the progress. This uses the current state and should be done within a
655          * synchronized block.
656          *
657          * @param current Current time (in milliseconds).
658          */
659         private void reportProgress(long current) {
660             // Determine the current state of tasks
661             final int pending = total - taskId;
662             final int running = taskId - completed;
663 
664             // Report progress in the following conditions:
665             // - All tasks have completed (i.e. the end); or
666             // - The current timestamp is above the next reporting time and either:
667             // -- The number of running tasks is equal to the level of parallel tasks
668             //    (i.e. the system is running at capacity, so not the end of a task but the start
669             //    of a new one)
670             // -- There are no pending tasks (i.e. the final submission or the end of a final task)
671             if (completed >= total ||
672                 (current >= nextReportTimestamp && (running == parallelTasks || pending == 0))) {
673                 // Report
674                 nextReportTimestamp = current + PROGRESS_INTERVAL;
675                 final StringBuilder sb = createStringBuilderWithTimestamp(current, pending, running, completed);
676                 try (Formatter formatter = new Formatter(sb)) {
677                     formatter.format(" (%.2f%%)", 100.0 * completed / total);
678                     appendRemaining(sb, current, pending, running);
679                     LogUtils.info(sb.toString());
680                 }
681             }
682         }
683 
684         /**
685          * Creates the string builder for the progress message with a timestamp prefix.
686          *
687          * <pre>
688          * [HH:mm:ss] Pending [pending]. Running [running]. Completed [completed]
689          * </pre>
690          *
691          * @param current Current time (in milliseconds)
692          * @param pending Pending tasks.
693          * @param running Running tasks.
694          * @param completed Completed tasks.
695          * @return the string builder
696          */
697         private static StringBuilder createStringBuilderWithTimestamp(long current,
698             int pending, int running, int completed) {
699             final StringBuilder sb = new StringBuilder(80);
700             // Use local time to adjust for timezone
701             final LocalDateTime time = LocalDateTime.ofInstant(
702                 Instant.ofEpochMilli(current), ZoneId.systemDefault());
703             sb.append('[');
704             append00(sb, time.getHour()).append(':');
705             append00(sb, time.getMinute()).append(':');
706             append00(sb, time.getSecond());
707             return sb.append("] Pending ").append(pending)
708                      .append(". Running ").append(running)
709                      .append(". Completed ").append(completed);
710         }
711 
712         /**
713          * Compute an estimate of the time remaining and append to the progress. Updates
714          * the estimated time of arrival (ETA).
715          *
716          * @param sb String Builder.
717          * @param current Current time (in milliseconds)
718          * @param pending Pending tasks.
719          * @param running Running tasks.
720          * @return the string builder
721          */
722         private StringBuilder appendRemaining(StringBuilder sb, long current, int pending, int running) {
723             final long millis = getRemainingTime(current, pending, running);
724             if (millis == 0) {
725                 // Unknown.
726                 return sb;
727             }
728 
729             // HH:mm:ss format
730             sb.append(". Remaining = ");
731             hms(sb, millis);
732             return sb;
733         }
734 
735         /**
736          * Gets the remaining time (in milliseconds).
737          *
738          * @param current Current time (in milliseconds)
739          * @param pending Pending tasks.
740          * @param running Running tasks.
741          * @return the remaining time
742          */
743         private long getRemainingTime(long current, int pending, int running) {
744             final long taskTime = getEstimatedTaskTime();
745             if (taskTime == 0) {
746                 // No estimate possible
747                 return 0;
748             }
749 
750             // The start times are sorted. This method assumes the most recent start times
751             // are still running tasks.
752             // If this is wrong (more recently submitted tasks finished early) the result
753             // is the estimate is too high. This could be corrected by storing the tasks
754             // that have finished and finding the times of only running tasks.
755 
756             // The remaining time is:
757             //   The time for all running tasks to finish
758             // + The time for pending tasks to run
759 
760             // The id of the most recently submitted task.
761             // Guard with a minimum index of zero to get a valid index.
762             final int id = Math.max(0, taskId - 1);
763 
764             // If there is a running task assume the youngest task is still running
765             // and estimate the time left.
766             long millis = (running == 0) ? 0 : getTimeRemaining(taskTime, current, startTimes[id]);
767 
768             // If additional tasks must also be submitted then the time must include
769             // the estimated time for running tasks to finish before new submissions
770             // in the batch can be made.
771             //                   now
772             // s1 --------------->|
773             //      s2 -----------|-------->
774             //          s3 -------|------------>
775             //                    s4 -------------->
776             //
777 
778             // Assume parallel batch execution.
779             // E.g. 3 additional tasks with parallelisation 4 is 0 batches
780             int batches = pending / parallelTasks;
781             millis += batches * taskTime;
782 
783             // Compute the expected end time of the final batch based on it starting when
784             // a currently running task ends.
785             // E.g. 3 remaining tasks requires the end time of the 3rd oldest running task.
786             int remainder = pending % parallelTasks;
787             if (remainder != 0) {
788                 // Guard with a minimum index of zero to get a valid index.
789                 final int nthOldest = Math.max(0, id - parallelTasks + remainder);
790                 millis += getTimeRemaining(taskTime, current, startTimes[nthOldest]);
791             }
792 
793             return millis;
794         }
795 
796         /**
797          * Gets the estimated task time.
798          *
799          * @return the estimated task time
800          */
801         private long getEstimatedTaskTime() {
802             Arrays.sort(sortedDurations, 0, completed);
803 
804             // Return median of small lists. If no tasks have finished this returns zero.
805             // as the durations is zero initialised.
806             if (completed < 4) {
807                 return sortedDurations[completed / 2];
808             }
809 
810             // Dieharder and BigCrush run in approximately constant time.
811             // Speed varies with the speed of the RNG by about 2-fold, and
812             // for Dieharder it may repeat suspicious tests.
813             // PractRand may fail very fast for bad generators which skews
814             // using the mean or even the median. So look at the longest
815             // running tests.
816 
817             // Find long running tests (>50% of the max run-time)
818             int upper = completed - 1;
819             final long halfMax = sortedDurations[upper] / 2;
820             // Binary search for the approximate cut-off
821             int lower = 0;
822             while (lower + 1 < upper) {
823                 int mid = (lower + upper) >>> 1;
824                 if (sortedDurations[mid] < halfMax) {
825                     lower = mid;
826                 } else {
827                     upper = mid;
828                 }
829             }
830             // Use the median of all tasks within approximately 50% of the max.
831             return sortedDurations[(upper + completed - 1) / 2];
832         }
833 
834         /**
835          * Gets the time remaining for the task.
836          *
837          * @param taskTime Estimated task time.
838          * @param current Current time.
839          * @param startTime Start time.
840          * @return the time remaining
841          */
842         private static long getTimeRemaining(long taskTime, long current, long startTime) {
843             final long endTime = startTime + taskTime;
844             // Ensure the time is positive in the case where the estimate is too low.
845             return Math.max(0, endTime - current);
846         }
847 
848         /**
849          * Append the milliseconds using {@code HH::mm:ss} format.
850          *
851          * @param sb String Builder.
852          * @param millis Milliseconds.
853          * @return the string builder
854          */
855         static StringBuilder hms(StringBuilder sb, final long millis) {
856             final long hours = TimeUnit.MILLISECONDS.toHours(millis);
857             long minutes = TimeUnit.MILLISECONDS.toMinutes(millis);
858             long seconds = TimeUnit.MILLISECONDS.toSeconds(millis);
859             // Truncate to interval [0,59]
860             seconds -= TimeUnit.MINUTES.toSeconds(minutes);
861             minutes -= TimeUnit.HOURS.toMinutes(hours);
862 
863             append00(sb, hours).append(':');
864             append00(sb, minutes).append(':');
865             return append00(sb, seconds);
866         }
867 
868         /**
869          * Append the ticks to the string builder in the format {@code %02d}.
870          *
871          * @param sb String Builder.
872          * @param ticks Ticks.
873          * @return the string builder
874          */
875         static StringBuilder append00(StringBuilder sb, long ticks) {
876             if (ticks == 0) {
877                 sb.append("00");
878             } else {
879                 if (ticks < 10) {
880                     sb.append('0');
881                 }
882                 sb.append(ticks);
883             }
884             return sb;
885         }
886     }
887 
888     /**
889      * Pipes random numbers to the standard input of an analyzer executable.
890      */
891     private static class StressTestTask implements Runnable {
892         /** Comment prefix. */
893         private static final String C = "# ";
894         /** New line. */
895         private static final String N = System.lineSeparator();
896         /** The date format. */
897         private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
898         /** The SI units for bytes in increments of 10^3. */
899         private static final String[] SI_UNITS = {"B", "kB", "MB", "GB", "TB", "PB", "EB"};
900         /** The SI unit base for bytes (10^3). */
901         private static final long SI_UNIT_BASE = 1000;
902 
903         /** The random source. */
904         private final RandomSource randomSource;
905         /** RNG to be tested. */
906         private final UniformRandomProvider rng;
907         /** The seed used to create the RNG. */
908         private final byte[] seed;
909         /** Output report file of the sub-process. */
910         private final File output;
911         /** The sub-process command to run. */
912         private final List<String> command;
913         /** The stress test command. */
914         private final StressTestCommand cmd;
915         /** The progress tracker. */
916         private final ProgressTracker progressTracker;
917 
918         /** The count of bytes used by the sub-process. */
919         private long bytesUsed;
920 
921         /**
922          * Creates the task.
923          *
924          * @param randomSource The random source.
925          * @param rng RNG to be tested.
926          * @param seed The seed used to create the RNG.
927          * @param output Output report file.
928          * @param command The sub-process command to run.
929          * @param cmd The run command.
930          * @param progressTracker The progress tracker.
931          */
932         StressTestTask(RandomSource randomSource,
933                        UniformRandomProvider rng,
934                        byte[] seed,
935                        File output,
936                        List<String> command,
937                        StressTestCommand cmd,
938                        ProgressTracker progressTracker) {
939             this.randomSource = randomSource;
940             this.rng = rng;
941             this.seed = seed;
942             this.output = output;
943             this.command = command;
944             this.cmd = cmd;
945             this.progressTracker = progressTracker;
946         }
947 
948         /** {@inheritDoc} */
949         @Override
950         public void run() {
951             if (cmd.isStopFileExists()) {
952                 // Do nothing
953                 return;
954             }
955 
956             try {
957                 printHeader();
958 
959                 Object exitValue;
960                 long millis;
961                 final int taskId = progressTracker.submitTask();
962                 if (cmd.dryRun) {
963                     // Do not do anything. Ignore the runtime.
964                     exitValue = "N/A";
965                     progressTracker.endTask(taskId);
966                     millis = 0;
967                 } else {
968                     // Run the sub-process
969                     exitValue = runSubProcess();
970                     millis = progressTracker.endTask(taskId);
971                 }
972 
973                 printFooter(millis, exitValue);
974 
975             } catch (final IOException ex) {
976                 throw new ApplicationException("Failed to run task: " + ex.getMessage(), ex);
977             }
978         }
979 
980         /**
981          * Run the analyzer sub-process command.
982          *
983          * @return The exit value.
984          * @throws IOException Signals that an I/O exception has occurred.
985          */
986         private Integer runSubProcess() throws IOException {
987             // Start test suite.
988             final ProcessBuilder builder = new ProcessBuilder(command);
989             builder.redirectOutput(ProcessBuilder.Redirect.appendTo(output));
990             builder.redirectErrorStream(true);
991             final Process testingProcess = builder.start();
992 
993             // Use a custom data output to write the RNG.
994             try (RngDataOutput sink = RNGUtils.createDataOutput(rng, cmd.raw64,
995                 testingProcess.getOutputStream(), cmd.bufferSize, cmd.byteOrder)) {
996                 for (;;) {
997                     sink.write(rng);
998                     bytesUsed++;
999                 }
1000             } catch (final IOException ignored) {
1001                 // Hopefully getting here when the analyzing software terminates.
1002             }
1003 
1004             bytesUsed *= cmd.bufferSize;
1005 
1006             // Get the exit value.
1007             // Wait for up to 60 seconds.
1008             // If an application does not exit after this time then something is wrong.
1009             // Dieharder and TestU01 BigCrush exit within 1 second.
1010             // PractRand has been observed to take longer than 1 second. It calls std::exit(0)
1011             // when failing a test so the length of time may be related to freeing memory.
1012             return ProcessUtils.getExitValue(testingProcess, TimeUnit.SECONDS.toMillis(60));
1013         }
1014 
1015         /**
1016          * Prints the header.
1017          *
1018          * @throws IOException if there was a problem opening or writing to the
1019          * {@code output} file.
1020          */
1021         private void printHeader() throws IOException {
1022             final StringBuilder sb = new StringBuilder(200);
1023             sb.append(C).append(N)
1024                 .append(C).append("RandomSource: ").append(randomSource.name()).append(N)
1025                 .append(C).append("RNG: ").append(rng.toString()).append(N)
1026                 .append(C).append("Seed: ").append(Hex.encodeHex(seed)).append(N)
1027                 .append(C).append(N)
1028 
1029             // Match the output of 'java -version', e.g.
1030             // java version "1.8.0_131"
1031             // Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
1032             // Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
1033             .append(C).append("Java: ").append(System.getProperty("java.version")).append(N);
1034             appendNameAndVersion(sb, "Runtime", "java.runtime.name", "java.runtime.version");
1035             appendNameAndVersion(sb, "JVM", "java.vm.name", "java.vm.version", "java.vm.info");
1036 
1037             sb.append(C).append("OS: ").append(System.getProperty("os.name"))
1038                 .append(' ').append(System.getProperty("os.version"))
1039                 .append(' ').append(System.getProperty("os.arch")).append(N)
1040                 .append(C).append("Native byte-order: ").append(ByteOrder.nativeOrder()).append(N)
1041                 .append(C).append("Output byte-order: ").append(cmd.byteOrder).append(N);
1042             if (rng instanceof RandomLongSource) {
1043                 sb.append(C).append("64-bit output: ").append(cmd.raw64).append(N);
1044             }
1045             sb.append(C).append(N)
1046                 .append(C).append("Analyzer: ");
1047             for (final String s : command) {
1048                 sb.append(s).append(' ');
1049             }
1050             sb.append(N)
1051               .append(C).append(N);
1052 
1053             appendDate(sb, "Start").append(C).append(N);
1054 
1055             write(sb, output, cmd.outputMode == StressTestCommand.OutputMode.APPEND);
1056         }
1057 
1058         /**
1059          * Prints the footer.
1060          *
1061          * @param millis Duration of the run (in milliseconds).
1062          * @param exitValue The process exit value.
1063          * @throws IOException if there was a problem opening or writing to the
1064          * {@code output} file.
1065          */
1066         private void printFooter(long millis,
1067                                  Object exitValue) throws IOException {
1068             final StringBuilder sb = new StringBuilder(200);
1069             sb.append(C).append(N);
1070 
1071             appendDate(sb, "End").append(C).append(N);
1072 
1073             sb.append(C).append("Exit value: ").append(exitValue).append(N)
1074                 .append(C).append("Bytes used: ").append(bytesUsed)
1075                           .append(" >= 2^").append(log2(bytesUsed))
1076                           .append(" (").append(bytesToString(bytesUsed)).append(')').append(N)
1077                 .append(C).append(N);
1078 
1079             final double duration = millis * 1e-3 / 60;
1080             sb.append(C).append("Test duration: ").append(duration).append(" minutes").append(N)
1081                 .append(C).append(N);
1082 
1083             write(sb, output, true);
1084         }
1085 
1086         /**
1087          * Write the string builder to the output file.
1088          *
1089          * @param sb The string builder.
1090          * @param output The output file.
1091          * @param append Set to {@code true} to append to the file.
1092          * @throws IOException Signals that an I/O exception has occurred.
1093          */
1094         private static void write(StringBuilder sb,
1095                                   File output,
1096                                   boolean append) throws IOException {
1097             try (BufferedWriter w = append ?
1098                     Files.newBufferedWriter(output.toPath(), StandardOpenOption.APPEND) :
1099                     Files.newBufferedWriter(output.toPath())) {
1100                 w.write(sb.toString());
1101             }
1102         }
1103 
1104         /**
1105          * Append prefix and then name and version from System properties, finished with
1106          * a new line. The format is:
1107          *
1108          * <pre>{@code # <prefix>: <name> (build <version>[, <info>, ...])}</pre>
1109          *
1110          * @param sb The string builder.
1111          * @param prefix The prefix.
1112          * @param nameKey The name key.
1113          * @param versionKey The version key.
1114          * @param infoKeys The additional information keys.
1115          * @return the StringBuilder.
1116          */
1117         private static StringBuilder appendNameAndVersion(StringBuilder sb,
1118                                                           String prefix,
1119                                                           String nameKey,
1120                                                           String versionKey,
1121                                                           String... infoKeys) {
1122             appendPrefix(sb, prefix)
1123                 .append(System.getProperty(nameKey, "?"))
1124                 .append(" (build ")
1125                 .append(System.getProperty(versionKey, "?"));
1126             for (final String key : infoKeys) {
1127                 final String value = System.getProperty(key, "");
1128                 if (!value.isEmpty()) {
1129                     sb.append(", ").append(value);
1130                 }
1131             }
1132             return sb.append(')').append(N);
1133         }
1134 
1135         /**
1136          * Append a comment with the current date to the {@link StringBuilder}, finished with
1137          * a new line. The format is:
1138          *
1139          * <pre>{@code # <prefix>: yyyy-MM-dd HH:mm:ss}</pre>
1140          *
1141          * @param sb The StringBuilder.
1142          * @param prefix The prefix used before the formatted date, e.g. "Start".
1143          * @return the StringBuilder.
1144          */
1145         private static StringBuilder appendDate(StringBuilder sb,
1146                                                 String prefix) {
1147             // Use local date format. It is not thread safe.
1148             final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
1149             return appendPrefix(sb, prefix).append(dateFormat.format(new Date())).append(N);
1150         }
1151 
1152         /**
1153          * Append a comment with the current date to the {@link StringBuilder}.
1154          *
1155          * <pre>
1156          * {@code # <prefix>: yyyy-MM-dd HH:mm:ss}
1157          * </pre>
1158          *
1159          * @param sb The StringBuilder.
1160          * @param prefix The prefix used before the formatted date, e.g. "Start".
1161          * @return the StringBuilder.
1162          */
1163         private static StringBuilder appendPrefix(StringBuilder sb,
1164                                                   String prefix) {
1165             return sb.append(C).append(prefix).append(": ");
1166         }
1167 
1168         /**
1169          * Convert bytes to a human readable string. Example output:
1170          *
1171          * <pre>
1172          *                              SI
1173          *                   0:        0 B
1174          *                  27:       27 B
1175          *                 999:      999 B
1176          *                1000:     1.0 kB
1177          *                1023:     1.0 kB
1178          *                1024:     1.0 kB
1179          *                1728:     1.7 kB
1180          *              110592:   110.6 kB
1181          *             7077888:     7.1 MB
1182          *           452984832:   453.0 MB
1183          *         28991029248:    29.0 GB
1184          *       1855425871872:     1.9 TB
1185          * 9223372036854775807:     9.2 EB   (Long.MAX_VALUE)
1186          * </pre>
1187          *
1188          * @param bytes the bytes
1189          * @return the string
1190          * @see <a
1191          * href="https://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java">How
1192          *      to convert byte size into human readable format in java?</a>
1193          */
1194         static String bytesToString(long bytes) {
1195             // When using the smallest unit no decimal point is needed, because it's the exact number.
1196             if (bytes < ONE_THOUSAND) {
1197                 return bytes + " " + SI_UNITS[0];
1198             }
1199 
1200             final int exponent = (int) (Math.log(bytes) / Math.log(SI_UNIT_BASE));
1201             final String unit = SI_UNITS[exponent];
1202             return String.format(Locale.US, "%.1f %s", bytes / Math.pow(SI_UNIT_BASE, exponent), unit);
1203         }
1204 
1205         /**
1206          * Return the log2 of a {@code long} value rounded down to a power of 2.
1207          *
1208          * @param x the value
1209          * @return {@code floor(log2(x))}
1210          */
1211         static int log2(long x) {
1212             return 63 - Long.numberOfLeadingZeros(x);
1213         }
1214     }
1215 }