1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.testtype.suite;
17 
18 import com.android.tradefed.config.Configuration;
19 import com.android.tradefed.config.ConfigurationException;
20 import com.android.tradefed.config.IConfiguration;
21 import com.android.tradefed.config.IDeviceConfiguration;
22 import com.android.tradefed.config.OptionCopier;
23 import com.android.tradefed.invoker.TestInformation;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.targetprep.ITargetPreparer;
26 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
27 import com.android.tradefed.testtype.IAbiReceiver;
28 import com.android.tradefed.testtype.IRemoteTest;
29 import com.android.tradefed.testtype.IShardableTest;
30 
31 import java.lang.reflect.InvocationTargetException;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 
39 /**
40  * Helper to split a list of modules represented by {@link IConfiguration} into a list of execution
41  * units represented by {@link ModuleDefinition}.
42  *
43  * <p>Each configuration may generate 1 or more {@link ModuleDefinition} depending on its options
44  * and test types:
45  *
46  * <ul>
47  *   <li>A non-shardable {@link IConfiguration} will generate a single {@link ModuleDefinition}.
48  *   <li>A shardable {@link IConfiguration} will generate a number of ModuleDefinition linked to the
49  *       {@link IRemoteTest} properties:
50  *       <ul>
51  *         <li>A non - {@link IShardableTest} will generate a single ModuleDefinition.
52  *         <li>A {@link IShardableTest} generates one ModuleDefinition per tests returned by {@link
53  *             IShardableTest#split()}.
54  *       </ul>
55  *
56  * </ul>
57  */
58 public class ModuleSplitter {
59 
60     /**
61      * Set an upper limit to how much a given module can be sharded. This can avoid over-granular
62      * intra-module sharding.
63      */
64     private static final int MAX_MODULE_LOCAL_SHARDING = 8;
65 
66     /**
67      * Create a List of executable unit {@link ModuleDefinition}s based on the map of configuration
68      * that was loaded.
69      *
70      * @param testInfo the current {@link TestInformation} to proceed with sharding.
71      * @param runConfig {@link LinkedHashMap} loaded from {@link ITestSuite#loadTests()}.
72      * @param suitePreparersPerDevice map of suite level preparers per test device.
73      * @param shardCount a shard count hint to help with sharding.
74      * @param dynamicModule Whether or not module can be shared in pool or must be independent
75      *     (strict sharding).
76      * @param intraModuleSharding Whether or not to shard within the modules.
77      * @return List of {@link ModuleDefinition}
78      */
splitConfiguration( TestInformation testInfo, LinkedHashMap<String, IConfiguration> runConfig, Map<String, List<ITargetPreparer>> suitePreparersPerDevice, int shardCount, boolean dynamicModule, boolean intraModuleSharding)79     public static List<ModuleDefinition> splitConfiguration(
80             TestInformation testInfo,
81             LinkedHashMap<String, IConfiguration> runConfig,
82             Map<String, List<ITargetPreparer>> suitePreparersPerDevice,
83             int shardCount,
84             boolean dynamicModule,
85             boolean intraModuleSharding) {
86         if (dynamicModule) {
87             // We maximize the sharding for dynamic to reduce time difference between first and
88             // last shard as much as possible. Overhead is low due to our test pooling.
89             shardCount *= 2;
90         }
91         List<ModuleDefinition> runModules = new ArrayList<>();
92         for (Entry<String, IConfiguration> configMap : runConfig.entrySet()) {
93             // Check that it's a valid configuration for suites, throw otherwise.
94             ValidateSuiteConfigHelper.validateConfig(configMap.getValue());
95 
96             try {
97                 createAndAddModule(
98                         testInfo,
99                         runModules,
100                         configMap.getKey(),
101                         configMap.getValue(),
102                         shardCount,
103                         dynamicModule,
104                         intraModuleSharding,
105                         suitePreparersPerDevice);
106             } catch (RuntimeException e) {
107                 CLog.e("Exception while creating module for '%s'", configMap.getKey());
108                 throw e;
109             }
110         }
111         return runModules;
112     }
113 
createAndAddModule( TestInformation testInfo, List<ModuleDefinition> currentList, String moduleName, IConfiguration config, int shardCount, boolean dynamicModule, boolean intraModuleSharding, Map<String, List<ITargetPreparer>> suitePreparersPerDevice)114     private static void createAndAddModule(
115             TestInformation testInfo,
116             List<ModuleDefinition> currentList,
117             String moduleName,
118             IConfiguration config,
119             int shardCount,
120             boolean dynamicModule,
121             boolean intraModuleSharding,
122             Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
123         List<IRemoteTest> tests = config.getTests();
124         // Get rid of the IRemoteTest reference on the shared configuration. It will not be used
125         // to run.
126         config.setTests(new ArrayList<>());
127 
128         if (config.getConfigurationDescription().isNotIRemoteTestShardable()) {
129             // If it cannot be sharded at all, then put all the tests together.
130             ModuleDefinition module =
131                     new ModuleDefinition(
132                             moduleName,
133                             tests,
134                             clonePreparersMap(config),
135                             clonePreparersMap(suitePreparersPerDevice),
136                             clonePreparers(config.getMultiTargetPreparers()),
137                             config);
138             currentList.add(module);
139             clearPreparersFromConfig(config);
140             return;
141         }
142 
143         // If this particular configuration module is declared as 'not shardable' we take it whole
144         // but still split the individual IRemoteTest in a pool.
145         if (!intraModuleSharding
146                 || config.getConfigurationDescription().isNotShardable()
147                 || (!dynamicModule
148                         && config.getConfigurationDescription().isNotStrictShardable())) {
149             for (int i = 0; i < tests.size(); i++) {
150                 if (dynamicModule) {
151                     ModuleDefinition module =
152                             new ModuleDefinition(
153                                     moduleName,
154                                     tests,
155                                     clonePreparersMap(config),
156                                     clonePreparersMap(suitePreparersPerDevice),
157                                     clonePreparers(config.getMultiTargetPreparers()),
158                                     config);
159                     currentList.add(module);
160                 } else {
161                     addModuleToListFromSingleTest(
162                             currentList, tests.get(i), moduleName, config, suitePreparersPerDevice);
163                 }
164             }
165             clearPreparersFromConfig(config);
166             return;
167         }
168 
169         // If configuration is possibly shardable we attempt to shard it.
170         for (IRemoteTest test : tests) {
171             if (test instanceof IShardableTest) {
172                 int shard = Math.min(MAX_MODULE_LOCAL_SHARDING, shardCount);
173                 Collection<IRemoteTest> shardedTests =
174                         ((IShardableTest) test).split(shard, testInfo);
175                 if (shardedTests != null) {
176                     // Test did shard we put the shard pool in ModuleDefinition which has a polling
177                     // behavior on the pool.
178                     if (dynamicModule) {
179                         for (int i = 0; i < shardedTests.size(); i++) {
180                             ModuleDefinition module =
181                                     new ModuleDefinition(
182                                             moduleName,
183                                             shardedTests,
184                                             clonePreparersMap(config),
185                                             clonePreparersMap(suitePreparersPerDevice),
186                                             clonePreparers(config.getMultiTargetPreparers()),
187                                             config);
188                             currentList.add(module);
189                         }
190                     } else {
191                         // We create independent modules with each sharded test.
192                         for (IRemoteTest moduleTest : shardedTests) {
193                             addModuleToListFromSingleTest(
194                                     currentList,
195                                     moduleTest,
196                                     moduleName,
197                                     config,
198                                     suitePreparersPerDevice);
199                         }
200                     }
201                     continue;
202                 }
203             }
204             // test is not shardable or did not shard
205             addModuleToListFromSingleTest(
206                     currentList, test, moduleName, config, suitePreparersPerDevice);
207         }
208         clearPreparersFromConfig(config);
209     }
210 
211     /**
212      * Helper to add a new {@link ModuleDefinition} to our list of Modules from a single {@link
213      * IRemoteTest}.
214      */
addModuleToListFromSingleTest( List<ModuleDefinition> currentList, IRemoteTest test, String moduleName, IConfiguration config, Map<String, List<ITargetPreparer>> suitePreparersPerDevice)215     private static void addModuleToListFromSingleTest(
216             List<ModuleDefinition> currentList,
217             IRemoteTest test,
218             String moduleName,
219             IConfiguration config,
220             Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
221         List<IRemoteTest> testList = new ArrayList<>();
222         testList.add(test);
223         ModuleDefinition module =
224                 new ModuleDefinition(
225                         moduleName,
226                         testList,
227                         clonePreparersMap(config),
228                         clonePreparersMap(suitePreparersPerDevice),
229                         clonePreparers(config.getMultiTargetPreparers()),
230                         config);
231         currentList.add(module);
232     }
233 
234     /**
235      * Deep clone a list of {@link ITargetPreparer} or {@link IMultiTargetPreparer}. We are ensured
236      * to find a default constructor with no arguments since that's the expectation from Tradefed
237      * when loading configuration. Cloning preparers is required since they may be stateful and we
238      * cannot share instance across devices.
239      */
clonePreparers(List<T> preparerList)240     private static <T> List<T> clonePreparers(List<T> preparerList) {
241         List<T> clones = new ArrayList<>();
242         for (T prep : preparerList) {
243             try {
244                 @SuppressWarnings("unchecked")
245                 T clone = (T) prep.getClass().getDeclaredConstructor().newInstance();
246                 OptionCopier.copyOptions(prep, clone);
247                 // Ensure we copy the Abi too.
248                 if (clone instanceof IAbiReceiver) {
249                     ((IAbiReceiver) clone).setAbi(((IAbiReceiver) prep).getAbi());
250                 }
251                 clones.add(clone);
252             } catch (InstantiationException
253                     | IllegalAccessException
254                     | ConfigurationException
255                     | InvocationTargetException
256                     | NoSuchMethodException e) {
257                 throw new RuntimeException(e);
258             }
259         }
260         return clones;
261     }
262 
263     /** Deep cloning of potentially multi-device preparers. */
clonePreparersMap(IConfiguration config)264     private static Map<String, List<ITargetPreparer>> clonePreparersMap(IConfiguration config) {
265         Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
266         for (IDeviceConfiguration holder : config.getDeviceConfig()) {
267             List<ITargetPreparer> preparers = new ArrayList<>();
268             res.put(holder.getDeviceName(), preparers);
269             preparers.addAll(clonePreparers(holder.getTargetPreparers()));
270         }
271         return res;
272     }
273 
274     /** Deep cloning of potentially multi-device preparers. */
clonePreparersMap( Map<String, List<ITargetPreparer>> suitePreparersPerDevice)275     private static Map<String, List<ITargetPreparer>> clonePreparersMap(
276             Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
277         Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
278         for (String device : suitePreparersPerDevice.keySet()) {
279             List<ITargetPreparer> preparers = new ArrayList<>();
280             res.put(device, preparers);
281             preparers.addAll(clonePreparers(suitePreparersPerDevice.get(device)));
282         }
283         return res;
284     }
285 
clearPreparersFromConfig(IConfiguration config)286     private static void clearPreparersFromConfig(IConfiguration config) {
287         try {
288             for (IDeviceConfiguration holder : config.getDeviceConfig()) {
289                 holder.removeObjectType(Configuration.TARGET_PREPARER_TYPE_NAME);
290             }
291             config.setMultiTargetPreparers(new ArrayList<>());
292         } catch (ConfigurationException e) {
293             throw new RuntimeException(e);
294         }
295     }
296 }
297