View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.junitcore.pc;
20  
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Iterator;
24  
25  import org.apache.maven.surefire.api.testset.TestSetFailedException;
26  import org.apache.maven.surefire.junitcore.JUnitCoreParameters;
27  import org.junit.runner.Description;
28  
29  /**
30   * An algorithm which configures {@link ParallelComputer} with allocated thread resources by given
31   * {@link org.apache.maven.surefire.junitcore.JUnitCoreParameters}.
32   * The {@code AbstractSurefireMojo} has to provide correct combinations of thread-counts and
33   * configuration parameter {@code parallel}.
34   *
35   * @author Tibor Digana (tibor17)
36   * @see org.apache.maven.surefire.junitcore.pc.ParallelComputerBuilder
37   * @since 2.16
38   */
39  final class ParallelComputerUtil {
40      private static final Collection<Description> UNUSED_DESCRIPTIONS = Arrays.asList(
41              null, Description.createSuiteDescription("null"), Description.TEST_MECHANISM, Description.EMPTY);
42  
43      private static int availableProcessors = Runtime.getRuntime().availableProcessors();
44  
45      private ParallelComputerUtil() {
46          throw new IllegalStateException("Suppresses calling constructor, ensuring non-instantiability.");
47      }
48  
49      /*
50       * For testing purposes.
51       */
52      static void overrideAvailableProcessors(int availableProcessors) {
53          ParallelComputerUtil.availableProcessors = availableProcessors;
54      }
55  
56      /*
57       * For testing purposes.
58       */
59      static void setDefaultAvailableProcessors() {
60          ParallelComputerUtil.availableProcessors = Runtime.getRuntime().availableProcessors();
61      }
62  
63      static Concurrency resolveConcurrency(JUnitCoreParameters params, RunnerCounter counts)
64              throws TestSetFailedException {
65          if (!params.isParallelismSelected()) {
66              throw new TestSetFailedException("Unspecified parameter '" + JUnitCoreParameters.PARALLEL_KEY + "'.");
67          }
68  
69          if (!params.isUseUnlimitedThreads() && !hasThreadCount(params) && !hasThreadCounts(params)) {
70              throw new TestSetFailedException("Unspecified thread-count(s). "
71                      + "See the parameters "
72                      + JUnitCoreParameters.USEUNLIMITEDTHREADS_KEY + ", "
73                      + JUnitCoreParameters.THREADCOUNT_KEY + ", "
74                      + JUnitCoreParameters.THREADCOUNTSUITES_KEY + ", "
75                      + JUnitCoreParameters.THREADCOUNTCLASSES_KEY + ", "
76                      + JUnitCoreParameters.THREADCOUNTMETHODS_KEY + ".");
77          }
78  
79          if (params.isUseUnlimitedThreads()) {
80              return concurrencyForUnlimitedThreads(params);
81          } else if (hasThreadCount(params)) {
82              if (hasThreadCounts(params)) {
83                  return isLeafUnspecified(params)
84                          ? concurrencyFromAllThreadCountsButUnspecifiedLeafCount(params, counts)
85                          : concurrencyFromAllThreadCounts(params);
86              } else {
87                  return estimateConcurrency(params, counts);
88              }
89          } else {
90              return concurrencyFromThreadCounts(params);
91          }
92      }
93  
94      static boolean isUnusedDescription(Description examined) {
95          if (UNUSED_DESCRIPTIONS.contains(examined)) {
96              return true;
97          } else {
98              // UNUSED_DESCRIPTIONS ensures that "examined" cannot be null
99              for (Description unused : UNUSED_DESCRIPTIONS) {
100                 if (unused != null && unused.getDisplayName().equals(examined.getDisplayName())) {
101                     return true;
102                 }
103             }
104             return false;
105         }
106     }
107 
108     static void removeUnusedDescriptions(Collection<Description> examined) {
109         for (Iterator<Description> it = examined.iterator(); it.hasNext(); ) {
110             if (isUnusedDescription(it.next())) {
111                 it.remove();
112             }
113         }
114     }
115 
116     private static Concurrency concurrencyForUnlimitedThreads(JUnitCoreParameters params) {
117         Concurrency concurrency = new Concurrency();
118         concurrency.suites = params.isParallelSuites() ? threadCountSuites(params) : 0;
119         concurrency.classes = params.isParallelClasses() ? threadCountClasses(params) : 0;
120         concurrency.methods = params.isParallelMethods() ? threadCountMethods(params) : 0;
121         concurrency.capacity = Integer.MAX_VALUE;
122         return concurrency;
123     }
124 
125     private static Concurrency estimateConcurrency(JUnitCoreParameters params, RunnerCounter counts) {
126         final Concurrency concurrency = new Concurrency();
127         final int parallelEntities = countParallelEntities(params);
128         concurrency.capacity = multiplyByCoreCount(params, params.getThreadCount());
129         if (parallelEntities == 1 || counts == null || counts.classes == 0) {
130             // Estimate parallel thread counts.
131             double ratio = 1d / parallelEntities;
132             int threads = multiplyByCoreCount(params, ratio * params.getThreadCount());
133             concurrency.suites = params.isParallelSuites() ? minSuites(threads, counts) : 0;
134             concurrency.classes = params.isParallelClasses() ? minClasses(threads, counts) : 0;
135             concurrency.methods = params.isParallelMethods() ? minMethods(threads, counts) : 0;
136             if (parallelEntities == 1) {
137                 concurrency.capacity = 0;
138             } else {
139                 adjustLeaf(params, concurrency);
140             }
141         } else {
142             // Try to allocate suites+classes+methods within threadCount,
143             concurrency.suites = params.isParallelSuites() ? toNonNegative(counts.suites) : 0;
144             concurrency.classes = params.isParallelClasses() ? toNonNegative(counts.classes) : 0;
145             concurrency.methods =
146                     params.isParallelMethods() ? toNonNegative(Math.ceil(counts.methods / (double) counts.classes)) : 0;
147             double sum = toNonNegative(concurrency.suites + concurrency.classes + concurrency.methods);
148             if (concurrency.capacity < sum && sum != 0) {
149                 // otherwise allocate them using the weighting factor < 1.
150                 double weight = concurrency.capacity / sum;
151                 concurrency.suites *= weight;
152                 concurrency.classes *= weight;
153                 concurrency.methods *= weight;
154             }
155             adjustLeaf(params, concurrency);
156         }
157         return concurrency;
158     }
159 
160     private static Concurrency concurrencyFromAllThreadCountsButUnspecifiedLeafCount(
161             JUnitCoreParameters params, RunnerCounter counts) {
162         Concurrency concurrency = new Concurrency();
163         concurrency.suites = params.isParallelSuites() ? params.getThreadCountSuites() : 0;
164         concurrency.suites = params.isParallelSuites() ? multiplyByCoreCount(params, concurrency.suites) : 0;
165         concurrency.classes = params.isParallelClasses() ? params.getThreadCountClasses() : 0;
166         concurrency.classes = params.isParallelClasses() ? multiplyByCoreCount(params, concurrency.classes) : 0;
167         concurrency.methods = params.isParallelMethods() ? params.getThreadCountMethods() : 0;
168         concurrency.methods = params.isParallelMethods() ? multiplyByCoreCount(params, concurrency.methods) : 0;
169         concurrency.capacity = multiplyByCoreCount(params, params.getThreadCount());
170 
171         if (counts != null) {
172             concurrency.suites = toNonNegative(Math.min(concurrency.suites, counts.suites));
173             concurrency.classes = toNonNegative(Math.min(concurrency.classes, counts.classes));
174         }
175 
176         setLeafInfinite(params, concurrency);
177 
178         return concurrency;
179     }
180 
181     private static Concurrency concurrencyFromAllThreadCounts(JUnitCoreParameters params) {
182         Concurrency concurrency = new Concurrency();
183         concurrency.suites = params.isParallelSuites() ? params.getThreadCountSuites() : 0;
184         concurrency.classes = params.isParallelClasses() ? params.getThreadCountClasses() : 0;
185         concurrency.methods = params.isParallelMethods() ? params.getThreadCountMethods() : 0;
186         concurrency.capacity = params.getThreadCount();
187         double all = sumThreadCounts(concurrency);
188 
189         concurrency.suites = params.isParallelSuites()
190                 ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.suites / all))
191                 : 0;
192 
193         concurrency.classes = params.isParallelClasses()
194                 ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.classes / all))
195                 : 0;
196 
197         concurrency.methods = params.isParallelMethods()
198                 ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.methods / all))
199                 : 0;
200 
201         concurrency.capacity = multiplyByCoreCount(params, concurrency.capacity);
202         adjustPrecisionInLeaf(params, concurrency);
203         return concurrency;
204     }
205 
206     private static Concurrency concurrencyFromThreadCounts(JUnitCoreParameters params) {
207         Concurrency concurrency = new Concurrency();
208         concurrency.suites = params.isParallelSuites() ? threadCountSuites(params) : 0;
209         concurrency.classes = params.isParallelClasses() ? threadCountClasses(params) : 0;
210         concurrency.methods = params.isParallelMethods() ? threadCountMethods(params) : 0;
211         concurrency.capacity = toNonNegative(sumThreadCounts(concurrency));
212         return concurrency;
213     }
214 
215     private static int countParallelEntities(JUnitCoreParameters params) {
216         int count = 0;
217         if (params.isParallelSuites()) {
218             count++;
219         }
220 
221         if (params.isParallelClasses()) {
222             count++;
223         }
224 
225         if (params.isParallelMethods()) {
226             count++;
227         }
228         return count;
229     }
230 
231     private static void adjustPrecisionInLeaf(JUnitCoreParameters params, Concurrency concurrency) {
232         if (params.isParallelMethods()) {
233             concurrency.methods = concurrency.capacity - concurrency.suites - concurrency.classes;
234         } else if (params.isParallelClasses()) {
235             concurrency.classes = concurrency.capacity - concurrency.suites;
236         }
237     }
238 
239     private static void adjustLeaf(JUnitCoreParameters params, Concurrency concurrency) {
240         if (params.isParallelMethods()) {
241             concurrency.methods = Integer.MAX_VALUE;
242         } else if (params.isParallelClasses()) {
243             concurrency.classes = Integer.MAX_VALUE;
244         }
245     }
246 
247     private static void setLeafInfinite(JUnitCoreParameters params, Concurrency concurrency) {
248         if (params.isParallelMethods()) {
249             concurrency.methods = Integer.MAX_VALUE;
250         } else if (params.isParallelClasses()) {
251             concurrency.classes = Integer.MAX_VALUE;
252         } else if (params.isParallelSuites()) {
253             concurrency.suites = Integer.MAX_VALUE;
254         }
255     }
256 
257     private static boolean isLeafUnspecified(JUnitCoreParameters params) {
258         int maskOfParallel = params.isParallelSuites() ? 4 : 0;
259         maskOfParallel |= params.isParallelClasses() ? 2 : 0;
260         maskOfParallel |= params.isParallelMethods() ? 1 : 0;
261 
262         int maskOfConcurrency = params.getThreadCountSuites() > 0 ? 4 : 0;
263         maskOfConcurrency |= params.getThreadCountClasses() > 0 ? 2 : 0;
264         maskOfConcurrency |= params.getThreadCountMethods() > 0 ? 1 : 0;
265 
266         maskOfConcurrency &= maskOfParallel;
267 
268         int leaf = Integer.lowestOneBit(maskOfParallel);
269         return maskOfConcurrency == maskOfParallel - leaf;
270     }
271 
272     private static double sumThreadCounts(Concurrency concurrency) {
273         double sum = concurrency.suites;
274         sum += concurrency.classes;
275         sum += concurrency.methods;
276         return sum;
277     }
278 
279     private static boolean hasThreadCounts(JUnitCoreParameters jUnitCoreParameters) {
280         return (jUnitCoreParameters.isParallelSuites() && jUnitCoreParameters.getThreadCountSuites() > 0)
281                 || (jUnitCoreParameters.isParallelClasses() && jUnitCoreParameters.getThreadCountClasses() > 0)
282                 || (jUnitCoreParameters.isParallelMethods() && jUnitCoreParameters.getThreadCountMethods() > 0);
283     }
284 
285     private static boolean hasThreadCount(JUnitCoreParameters jUnitCoreParameters) {
286         return jUnitCoreParameters.getThreadCount() > 0;
287     }
288 
289     private static int threadCountMethods(JUnitCoreParameters jUnitCoreParameters) {
290         return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountMethods());
291     }
292 
293     private static int threadCountClasses(JUnitCoreParameters jUnitCoreParameters) {
294         return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountClasses());
295     }
296 
297     private static int threadCountSuites(JUnitCoreParameters jUnitCoreParameters) {
298         return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountSuites());
299     }
300 
301     private static int multiplyByCoreCount(JUnitCoreParameters jUnitCoreParameters, double threadsPerCore) {
302         double numberOfThreads = jUnitCoreParameters.isPerCoreThreadCount()
303                 ? threadsPerCore * (double) availableProcessors
304                 : threadsPerCore;
305 
306         return numberOfThreads > 0 ? toNonNegative(numberOfThreads) : Integer.MAX_VALUE;
307     }
308 
309     private static int minSuites(int threads, RunnerCounter counts) {
310         long count = counts == null ? Integer.MAX_VALUE : counts.suites;
311         return Math.min(threads, toNonNegative(count));
312     }
313 
314     private static int minClasses(int threads, RunnerCounter counts) {
315         long count = counts == null ? Integer.MAX_VALUE : counts.classes;
316         return Math.min(threads, toNonNegative(count));
317     }
318 
319     private static int minMethods(int threads, RunnerCounter counts) {
320         long count = counts == null ? Integer.MAX_VALUE : counts.methods;
321         return Math.min(threads, toNonNegative(count));
322     }
323 
324     private static int toNonNegative(long num) {
325         return (int) Math.min(num > 0 ? num : 0, Integer.MAX_VALUE);
326     }
327 
328     private static int toNonNegative(double num) {
329         return (int) Math.min(num > 0 ? num : 0, Integer.MAX_VALUE);
330     }
331 }