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 java.util.List;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.io.IOException;
23  import java.io.File;
24  import java.io.PrintWriter;
25  import java.io.FileWriter;
26  import java.io.DataOutputStream;
27  import java.util.concurrent.ExecutionException;
28  import java.util.concurrent.ExecutorService;
29  import java.util.concurrent.Executors;
30  import java.util.concurrent.Future;
31  
32  import org.apache.commons.rng.UniformRandomProvider;
33  
34  /**
35   * Class that can be used for testing a generator by piping the values
36   * returned by its {@link UniformRandomProvider#nextInt()} method to a
37   * program that reads {@code int} values from its standard input and
38   * writes an analysis report to standard output.
39   *
40   * The <a href="http://www.phy.duke.edu/~rgb/General/dieharder.php">
41   * "dieharder"</a> test suite is such a software.
42   *
43   * Example of command line, assuming that "examples.jar" specifies this
44   * class as the "main" class (see {@link #main(String[]) main} method):
45   * <pre><code>
46   *  $ java -jar examples.jar \
47   *    report/dh_ \
48   *    4 \
49   *    org.apache.commons.rng.examples.stress.GeneratorsList \
50   *    /usr/bin/dieharder -a -g 200 -Y 1 -k 2
51   * </code></pre>
52   */
53  public class RandomStressTester {
54      /** Comment prefix. */
55      private static final String C = "# ";
56      /** New line. */
57      private static final String N = "\n";
58      /** Command line. */
59      private final List<String> cmdLine;
60      /** Output prefix. */
61      private final String fileOutputPrefix;
62  
63      /**
64       * Creates the application.
65       *
66       * @param cmd Command line.
67       * @param outputPrefix Output prefix for file reports.
68       */
69      private RandomStressTester(List<String> cmd,
70                                 String outputPrefix) {
71          final File exec = new File(cmd.get(0));
72          if (!exec.exists() ||
73              !exec.canExecute()) {
74              throw new IllegalArgumentException("Program is not executable: " + exec);
75          }
76  
77          cmdLine = new ArrayList<String>(cmd);
78          fileOutputPrefix = outputPrefix;
79  
80          final File reportDir = new File(fileOutputPrefix).getParentFile();
81          if (!reportDir.exists() ||
82              !reportDir.isDirectory() ||
83              !reportDir.canWrite()) {
84              throw new IllegalArgumentException("Invalid output directory: " + reportDir);
85          }
86      }
87  
88      /**
89       * Program's entry point.
90       *
91       * @param args Application's arguments.
92       * The order is as follows:
93       * <ol>
94       *  <li>Output prefix: Filename prefix where the output of the analysis will
95       *   written to.  The appended suffix is the index of the instance within the
96       *   list of generators to be tested.</li>
97       *  <li>Number of threads to use concurrently: One thread will process one of
98       *    the generators to be tested.</li>
99       *  <li>Name of a class that implements {@code Iterable<UniformRandomProvider>}
100      *   (and defines a default constructor): Each generator of the list will be
101      *   tested by one instance of the analyzer program</li>
102      *  <li>Path to the executable: this is the analyzer software that reads 32-bits
103      *   integers from stdin.</li>
104      *  <li>All remaining arguments are passed to the executable.</li>
105      * </ol>
106      */
107     public static void main(String[] args) {
108         final String output = args[0];
109         final int numThreads = Integer.parseInt(args[1]);
110 
111         final Iterable<UniformRandomProvider> rngList = createGeneratorsList(args[2]);
112 
113         final List<String> cmdLine = new ArrayList<String>();
114         cmdLine.addAll(Arrays.asList(Arrays.copyOfRange(args, 3, args.length)));
115 
116         final RandomStressTester app = new RandomStressTester(cmdLine, output);
117 
118         try {
119             app.run(rngList, numThreads);
120         } catch (IOException e) {
121             throw new RuntimeException(e);
122         }
123     }
124 
125     /**
126      * Creates the tasks and starts the processes.
127      *
128      * @param generators List of generators to be analyzed.
129      * @param numConcurrentTasks Number of concurrent tasks.
130      * Twice as many threads will be started: one thread for the RNG and one
131      * for the analyzer.
132      * @throws IOException if an error occurs when writing to the disk.
133      */
134     private void run(Iterable<UniformRandomProvider> generators,
135                      int numConcurrentTasks)
136         throws IOException {
137         // Parallel execution.
138         final ExecutorService service = Executors.newFixedThreadPool(numConcurrentTasks);
139 
140         // Placeholder (output will be "null").
141         final List<Future<?>> execOutput = new ArrayList<Future<?>>();
142 
143         // Run tasks.
144         int count = 0;
145         for (UniformRandomProvider rng : generators) {
146             final File output = new File(fileOutputPrefix + (++count));
147             final Runnable r = new Task(rng, output);
148             execOutput.add(service.submit(r));
149         }
150 
151         // Wait for completion (ignoring return value).
152         try {
153             for (Future<?> f : execOutput) {
154                 try {
155                     f.get();
156                 } catch (ExecutionException e) {
157                     System.err.println(e.getCause().getMessage());
158                 }
159             }
160         } catch (InterruptedException ignored) {}
161 
162         // Terminate all threads.
163         service.shutdown();
164     }
165 
166     /**
167      * Creates the list of generators to be tested.
168      *
169      * @param name Name of the class that contains the generators to be
170      * analyzed.
171      * @return the list of generators.
172      * @throws IllegalStateException if an error occurs during instantiation.
173      */
174     private static Iterable<UniformRandomProvider> createGeneratorsList(String name) {
175         try {
176             return (Iterable<UniformRandomProvider>) Class.forName(name).newInstance();
177         } catch (ClassNotFoundException|
178                  InstantiationException|
179                  IllegalAccessException e) {
180             throw new RuntimeException(e);
181         }
182     }
183 
184     /**
185      * Pipes random numbers to the standard input of an analyzer.
186      */
187     private class Task implements Runnable {
188         /** Directory for reports of the tester processes. */
189         private final File output;
190         /** RNG to be tested. */
191         private final UniformRandomProvider rng;
192 
193         /**
194          * Creates the task.
195          *
196          * @param random RNG to be tested.
197          * @param report Report file.
198          */
199         Task(UniformRandomProvider random,
200              File report) {
201             rng = random;
202             output = report;
203         }
204 
205         /** {@inheritDoc} */
206         @Override
207             public void run() {
208             try {
209                 // Write header.
210                 printHeader(output, rng);
211 
212                 // Start test suite.
213                 final ProcessBuilder builder = new ProcessBuilder(cmdLine);
214                 builder.redirectOutput(ProcessBuilder.Redirect.appendTo(output));
215                 final Process testingProcess = builder.start();
216                 final DataOutputStream sink = new DataOutputStream(testingProcess.getOutputStream());
217 
218                 final long startTime = System.nanoTime();
219 
220                 try {
221                     while (true) {
222                         sink.writeInt(rng.nextInt());
223                     }
224                 } catch (IOException e) {
225                     // Hopefully getting here when the analyzing software terminates.
226                 }
227 
228                 final long endTime = System.nanoTime();
229 
230                 // Write footer.
231                 printFooter(output, endTime - startTime);
232 
233             } catch (IOException e) {
234                 throw new RuntimeException("Failed to start task: " + e.getMessage());
235             }
236         }
237     }
238 
239     /**
240      * @param output File.
241      * @param rng Generator being tested.
242      * @param cmdLine
243      * @throws IOException if there was a problem opening or writing to
244      * the {@code output} file.
245      */
246     private void printHeader(File output,
247                              UniformRandomProvider rng)
248         throws IOException {
249         final StringBuilder sb = new StringBuilder();
250         sb.append(C).append(N);
251         sb.append(C).append("RNG: ").append(rng.toString()).append(N);
252         sb.append(C).append(N);
253         sb.append(C).append("Java: ").append(System.getProperty("java.version")).append(N);
254         sb.append(C).append("Runtime: ").append(System.getProperty("java.runtime.version", "?")).append(N);
255         sb.append(C).append("JVM: ").append(System.getProperty("java.vm.name"))
256             .append(" ").append(System.getProperty("java.vm.version")).append(N);
257         sb.append(C).append("OS: ").append(System.getProperty("os.name"))
258             .append(" ").append(System.getProperty("os.version"))
259             .append(" ").append(System.getProperty("os.arch")).append(N);
260         sb.append(C).append(N);
261 
262         sb.append(C).append("Analyzer: ");
263         for (String s : cmdLine) {
264             sb.append(s).append(" ");
265         }
266         sb.append(N);
267         sb.append(C).append(N);
268 
269         final PrintWriter w = new PrintWriter(new FileWriter(output, true));
270         w.print(sb.toString());
271         w.close();
272     }
273 
274     /**
275      * @param output File.
276      * @param nanoTime Duration of the run.
277      * @throws IOException if there was a problem opening or writing to
278      * the {@code output} file.
279      */
280     private void printFooter(File output,
281                              long nanoTime)
282         throws IOException {
283         final PrintWriter w = new PrintWriter(new FileWriter(output, true));
284         w.println(C);
285 
286         final double duration = ((double) nanoTime) * 1e-9 / 60;
287         w.println(C + "Test duration: " + duration + " minutes");
288 
289         w.println(C);
290         w.close();
291     }
292 }