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