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 hash code 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 hash code (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 }