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, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 package org.apache.hadoop.mapred.gridmix.emulators.resourceusage;
19 
20 import java.io.IOException;
21 import java.util.Random;
22 
23 import org.apache.hadoop.conf.Configuration;
24 import org.apache.hadoop.mapred.gridmix.Progressive;
25 import org.apache.hadoop.tools.rumen.ResourceUsageMetrics;
26 import org.apache.hadoop.yarn.util.ResourceCalculatorPlugin;
27 
28 /**
29  * <p>A {@link ResourceUsageEmulatorPlugin} that emulates the cumulative CPU
30  * usage by performing certain CPU intensive operations. Performing such CPU
31  * intensive operations essentially uses up some CPU. Every
32  * {@link ResourceUsageEmulatorPlugin} is configured with a feedback module i.e
33  * a {@link ResourceCalculatorPlugin}, to monitor the resource usage.</p>
34  *
35  * <p>{@link CumulativeCpuUsageEmulatorPlugin} emulates the CPU usage in steps.
36  * The frequency of emulation can be configured via
37  * {@link #CPU_EMULATION_PROGRESS_INTERVAL}.
38  * CPU usage values are matched via emulation only on the interval boundaries.
39  * </p>
40  *
41  * {@link CumulativeCpuUsageEmulatorPlugin} is a wrapper program for managing
42  * the CPU usage emulation feature. It internally uses an emulation algorithm
43  * (called as core and described using {@link CpuUsageEmulatorCore}) for
44  * performing the actual emulation. Multiple calls to this core engine should
45  * use up some amount of CPU.<br>
46  *
47  * <p>{@link CumulativeCpuUsageEmulatorPlugin} provides a calibration feature
48  * via {@link #initialize(Configuration, ResourceUsageMetrics,
49  *                        ResourceCalculatorPlugin, Progressive)} to calibrate
50  *  the plugin and its core for the underlying hardware. As a result of
51  *  calibration, every call to the emulation engine's core should roughly use up
52  *  1% of the total usage value to be emulated. This makes sure that the
53  *  underlying hardware is profiled before use and that the plugin doesn't
54  *  accidently overuse the CPU. With 1% as the unit emulation target value for
55  *  the core engine, there will be roughly 100 calls to the engine resulting in
56  *  roughly 100 calls to the feedback (resource usage monitor) module.
57  *  Excessive usage of the feedback module is discouraged as
58  *  it might result into excess CPU usage resulting into no real CPU emulation.
59  *  </p>
60  */
61 public class CumulativeCpuUsageEmulatorPlugin
62 implements ResourceUsageEmulatorPlugin {
63   protected CpuUsageEmulatorCore emulatorCore;
64   private ResourceCalculatorPlugin monitor;
65   private Progressive progress;
66   private boolean enabled = true;
67   private float emulationInterval; // emulation interval
68   private long targetCpuUsage = 0;
69   private float lastSeenProgress = 0;
70   private long lastSeenCpuUsage = 0;
71 
72   // Configuration parameters
73   public static final String CPU_EMULATION_PROGRESS_INTERVAL =
74     "gridmix.emulators.resource-usage.cpu.emulation-interval";
75   private static final float DEFAULT_EMULATION_FREQUENCY = 0.1F; // 10 times
76 
77   /**
78    * This is the core CPU usage emulation algorithm. This is the core engine
79    * which actually performs some CPU intensive operations to consume some
80    * amount of CPU. Multiple calls of {@link #compute()} should help the
81    * plugin emulate the desired level of CPU usage. This core engine can be
82    * calibrated using the {@link #calibrate(ResourceCalculatorPlugin, long)}
83    * API to suit the underlying hardware better. It also can be used to optimize
84    * the emulation cycle.
85    */
86   public interface CpuUsageEmulatorCore {
87     /**
88      * Performs some computation to use up some CPU.
89      */
compute()90     public void compute();
91 
92     /**
93      * Allows the core to calibrate itself.
94      */
calibrate(ResourceCalculatorPlugin monitor, long totalCpuUsage)95     public void calibrate(ResourceCalculatorPlugin monitor,
96                           long totalCpuUsage);
97   }
98 
99   /**
100    * This is the core engine to emulate the CPU usage. The only responsibility
101    * of this class is to perform certain math intensive operations to make sure
102    * that some desired value of CPU is used.
103    */
104   public static class DefaultCpuUsageEmulator implements CpuUsageEmulatorCore {
105     // number of times to loop for performing the basic unit computation
106     private int numIterations;
107     private final Random random;
108 
109     /**
110      * This is to fool the JVM and make it think that we need the value
111      * stored in the unit computation i.e {@link #compute()}. This will prevent
112      * the JVM from optimizing the code.
113      */
114     protected double returnValue;
115 
116     /**
117      * Initialized the {@link DefaultCpuUsageEmulator} with default values.
118      * Note that the {@link DefaultCpuUsageEmulator} should be calibrated
119      * (see {@link #calibrate(ResourceCalculatorPlugin, long)}) when initialized
120      * using this constructor.
121      */
DefaultCpuUsageEmulator()122     public DefaultCpuUsageEmulator() {
123       this(-1);
124     }
125 
DefaultCpuUsageEmulator(int numIterations)126     DefaultCpuUsageEmulator(int numIterations) {
127       this.numIterations = numIterations;
128       random = new Random();
129     }
130 
131     /**
132      * This will consume some desired level of CPU. This API will try to use up
133      * 'X' percent of the target cumulative CPU usage. Currently X is set to
134      * 10%.
135      */
compute()136     public void compute() {
137       for (int i = 0; i < numIterations; ++i) {
138         performUnitComputation();
139       }
140     }
141 
142     // Perform unit computation. The complete CPU emulation will be based on
143     // multiple invocations to this unit computation module.
performUnitComputation()144     protected void performUnitComputation() {
145       //TODO can this be configurable too. Users/emulators should be able to
146       // pick and choose what MATH operations to run.
147       // Example :
148       //           BASIC : ADD, SUB, MUL, DIV
149       //           ADV   : SQRT, SIN, COSIN..
150       //           COMPO : (BASIC/ADV)*
151       // Also define input generator. For now we can use the random number
152       // generator. Later this can be changed to accept multiple sources.
153 
154       int randomData = random.nextInt();
155       int randomDataCube = randomData * randomData * randomData;
156       double randomDataCubeRoot = Math.cbrt(randomData);
157       returnValue = Math.log(Math.tan(randomDataCubeRoot
158                                       * Math.exp(randomDataCube))
159                              * Math.sqrt(randomData));
160     }
161 
162     /**
163      * This will calibrate the algorithm such that a single invocation of
164      * {@link #compute()} emulates roughly 1% of the total desired resource
165      * usage value.
166      */
calibrate(ResourceCalculatorPlugin monitor, long totalCpuUsage)167     public void calibrate(ResourceCalculatorPlugin monitor,
168                           long totalCpuUsage) {
169       long initTime = monitor.getCumulativeCpuTime();
170 
171       long defaultLoopSize = 0;
172       long finalTime = initTime;
173 
174       //TODO Make this configurable
175       while (finalTime - initTime < 100) { // 100 ms
176         ++defaultLoopSize;
177         performUnitComputation(); //perform unit computation
178         finalTime = monitor.getCumulativeCpuTime();
179       }
180 
181       long referenceRuntime = finalTime - initTime;
182 
183       // time for one loop = (final-time - init-time) / total-loops
184       float timePerLoop = ((float)referenceRuntime) / defaultLoopSize;
185 
186       // compute the 1% of the total CPU usage desired
187       //TODO Make this configurable
188       long onePercent = totalCpuUsage / 100;
189 
190       // num-iterations for 1% = (total-desired-usage / 100) / time-for-one-loop
191       numIterations = Math.max(1, (int)((float)onePercent/timePerLoop));
192 
193       System.out.println("Calibration done. Basic computation runtime : "
194           + timePerLoop + " milliseconds. Optimal number of iterations (1%): "
195           + numIterations);
196     }
197   }
198 
CumulativeCpuUsageEmulatorPlugin()199   public CumulativeCpuUsageEmulatorPlugin() {
200     this(new DefaultCpuUsageEmulator());
201   }
202 
203   /**
204    * For testing.
205    */
CumulativeCpuUsageEmulatorPlugin(CpuUsageEmulatorCore core)206   public CumulativeCpuUsageEmulatorPlugin(CpuUsageEmulatorCore core) {
207     emulatorCore = core;
208   }
209 
210   // Note that this weighing function uses only the current progress. In future,
211   // this might depend on progress, emulation-interval and expected target.
getWeightForProgressInterval(float progress)212   private float getWeightForProgressInterval(float progress) {
213     // we want some kind of exponential growth function that gives less weight
214     // on lower progress boundaries but high (exact emulation) near progress
215     // value of 1.
216     // so here is how the current growth function looks like
217     //    progress    weight
218     //      0.1       0.0001
219     //      0.2       0.0016
220     //      0.3       0.0081
221     //      0.4       0.0256
222     //      0.5       0.0625
223     //      0.6       0.1296
224     //      0.7       0.2401
225     //      0.8       0.4096
226     //      0.9       0.6561
227     //      1.0       1.000
228 
229     return progress * progress * progress * progress;
230   }
231 
getCurrentCPUUsage()232   private synchronized long getCurrentCPUUsage() {
233     return monitor.getCumulativeCpuTime();
234 
235   }
236 
237   @Override
getProgress()238   public float getProgress() {
239     return enabled
240            ? Math.min(1f, ((float)getCurrentCPUUsage())/targetCpuUsage)
241            : 1.0f;
242   }
243 
244   @Override
245   //TODO Multi-threading for speedup?
emulate()246   public void emulate() throws IOException, InterruptedException {
247     if (enabled) {
248       float currentProgress = progress.getProgress();
249       if (lastSeenProgress < currentProgress
250           && ((currentProgress - lastSeenProgress) >= emulationInterval
251               || currentProgress == 1)) {
252         // Estimate the final cpu usage
253         //
254         //   Consider the following
255         //     Cl/Cc/Cp : Last/Current/Projected Cpu usage
256         //     Pl/Pc/Pp : Last/Current/Projected progress
257         //   Then
258         //     (Cp-Cc)/(Pp-Pc) = (Cc-Cl)/(Pc-Pl)
259         //   Solving this for Cp, we get
260         //     Cp = Cc + (1-Pc)*(Cc-Cl)/Pc-Pl)
261         //   Note that (Cc-Cl)/(Pc-Pl) is termed as 'rate' in the following
262         //   section
263 
264         long currentCpuUsage = getCurrentCPUUsage();
265         // estimate the cpu usage rate
266         float rate = (currentCpuUsage - lastSeenCpuUsage)
267                      / (currentProgress - lastSeenProgress);
268         long projectedUsage =
269           currentCpuUsage + (long)((1 - currentProgress) * rate);
270 
271         if (projectedUsage < targetCpuUsage) {
272           // determine the correction factor between the current usage and the
273           // expected usage and add some weight to the target
274           long currentWeighedTarget =
275             (long)(targetCpuUsage
276                    * getWeightForProgressInterval(currentProgress));
277 
278           while (getCurrentCPUUsage() < currentWeighedTarget) {
279             emulatorCore.compute();
280             // sleep for 100ms
281             try {
282               Thread.sleep(100);
283             } catch (InterruptedException ie) {
284               String message =
285                 "CumulativeCpuUsageEmulatorPlugin got interrupted. Exiting.";
286               throw new RuntimeException(message);
287             }
288           }
289         }
290 
291         // set the last seen progress
292         lastSeenProgress = progress.getProgress();
293         // set the last seen usage
294         lastSeenCpuUsage = getCurrentCPUUsage();
295       }
296     }
297   }
298 
299   @Override
initialize(Configuration conf, ResourceUsageMetrics metrics, ResourceCalculatorPlugin monitor, Progressive progress)300   public void initialize(Configuration conf, ResourceUsageMetrics metrics,
301                          ResourceCalculatorPlugin monitor,
302                          Progressive progress) {
303     this.monitor = monitor;
304     this.progress = progress;
305 
306     // get the target CPU usage
307     targetCpuUsage = metrics.getCumulativeCpuUsage();
308     if (targetCpuUsage <= 0 ) {
309       enabled = false;
310       return;
311     } else {
312       enabled = true;
313     }
314 
315     emulationInterval =  conf.getFloat(CPU_EMULATION_PROGRESS_INTERVAL,
316                                        DEFAULT_EMULATION_FREQUENCY);
317 
318     // calibrate the core cpu-usage utility
319     emulatorCore.calibrate(monitor, targetCpuUsage);
320 
321     // initialize the states
322     lastSeenProgress = 0;
323     lastSeenCpuUsage = 0;
324   }
325 }
326