/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.mapred.gridmix.emulators.resourceusage; import java.io.IOException; import java.util.Random; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.mapred.gridmix.Progressive; import org.apache.hadoop.util.ResourceCalculatorPlugin; import org.apache.hadoop.tools.rumen.ResourceUsageMetrics; /** * <p>A {@link ResourceUsageEmulatorPlugin} that emulates the cumulative CPU * usage by performing certain CPU intensive operations. Performing such CPU * intensive operations essentially uses up some CPU. Every * {@link ResourceUsageEmulatorPlugin} is configured with a feedback module i.e * a {@link ResourceCalculatorPlugin}, to monitor the resource usage.</p> * * <p>{@link CumulativeCpuUsageEmulatorPlugin} emulates the CPU usage in steps. * The frequency of emulation can be configured via * {@link #CPU_EMULATION_PROGRESS_INTERVAL}. * CPU usage values are matched via emulation only on the interval boundaries. * </p> * * {@link CumulativeCpuUsageEmulatorPlugin} is a wrapper program for managing * the CPU usage emulation feature. It internally uses an emulation algorithm * (called as core and described using {@link CpuUsageEmulatorCore}) for * performing the actual emulation. Multiple calls to this core engine should * use up some amount of CPU.<br> * * <p>{@link CumulativeCpuUsageEmulatorPlugin} provides a calibration feature * via {@link #initialize(Configuration, ResourceUsageMetrics, * ResourceCalculatorPlugin, Progressive)} to calibrate * the plugin and its core for the underlying hardware. As a result of * calibration, every call to the emulation engine's core should roughly use up * 1% of the total usage value to be emulated. This makes sure that the * underlying hardware is profiled before use and that the plugin doesn't * accidently overuse the CPU. With 1% as the unit emulation target value for * the core engine, there will be roughly 100 calls to the engine resulting in * roughly 100 calls to the feedback (resource usage monitor) module. * Excessive usage of the feedback module is discouraged as * it might result into excess CPU usage resulting into no real CPU emulation. * </p> */ public class CumulativeCpuUsageEmulatorPlugin implements ResourceUsageEmulatorPlugin { protected CpuUsageEmulatorCore emulatorCore; private ResourceCalculatorPlugin monitor; private Progressive progress; private boolean enabled = true; private float emulationInterval; // emulation interval private long targetCpuUsage = 0; private float lastSeenProgress = 0; private long lastSeenCpuUsage = 0; // Configuration parameters public static final String CPU_EMULATION_PROGRESS_INTERVAL = "gridmix.emulators.resource-usage.cpu.emulation-interval"; private static final float DEFAULT_EMULATION_FREQUENCY = 0.1F; // 10 times /** * This is the core CPU usage emulation algorithm. This is the core engine * which actually performs some CPU intensive operations to consume some * amount of CPU. Multiple calls of {@link #compute()} should help the * plugin emulate the desired level of CPU usage. This core engine can be * calibrated using the {@link #calibrate(ResourceCalculatorPlugin, long)} * API to suit the underlying hardware better. It also can be used to optimize * the emulation cycle. */ public interface CpuUsageEmulatorCore { /** * Performs some computation to use up some CPU. */ public void compute(); /** * Allows the core to calibrate itself. */ public void calibrate(ResourceCalculatorPlugin monitor, long totalCpuUsage); } /** * This is the core engine to emulate the CPU usage. The only responsibility * of this class is to perform certain math intensive operations to make sure * that some desired value of CPU is used. */ public static class DefaultCpuUsageEmulator implements CpuUsageEmulatorCore { // number of times to loop for performing the basic unit computation private int numIterations; private final Random random; /** * This is to fool the JVM and make it think that we need the value * stored in the unit computation i.e {@link #compute()}. This will prevent * the JVM from optimizing the code. */ protected double returnValue; /** * Initialized the {@link DefaultCpuUsageEmulator} with default values. * Note that the {@link DefaultCpuUsageEmulator} should be calibrated * (see {@link #calibrate(ResourceCalculatorPlugin, long)}) when initialized * using this constructor. */ public DefaultCpuUsageEmulator() { this(-1); } DefaultCpuUsageEmulator(int numIterations) { this.numIterations = numIterations; random = new Random(); } /** * This will consume some desired level of CPU. This API will try to use up * 'X' percent of the target cumulative CPU usage. Currently X is set to * 10%. */ public void compute() { for (int i = 0; i < numIterations; ++i) { performUnitComputation(); } } // Perform unit computation. The complete CPU emulation will be based on // multiple invocations to this unit computation module. protected void performUnitComputation() { //TODO can this be configurable too. Users/emulators should be able to // pick and choose what MATH operations to run. // Example : // BASIC : ADD, SUB, MUL, DIV // ADV : SQRT, SIN, COSIN.. // COMPO : (BASIC/ADV)* // Also define input generator. For now we can use the random number // generator. Later this can be changed to accept multiple sources. int randomData = random.nextInt(); int randomDataCube = randomData * randomData * randomData; double randomDataCubeRoot = Math.cbrt(randomData); returnValue = Math.log(Math.tan(randomDataCubeRoot * Math.exp(randomDataCube)) * Math.sqrt(randomData)); } /** * This will calibrate the algorithm such that a single invocation of * {@link #compute()} emulates roughly 1% of the total desired resource * usage value. */ public void calibrate(ResourceCalculatorPlugin monitor, long totalCpuUsage) { long initTime = monitor.getProcResourceValues().getCumulativeCpuTime(); long defaultLoopSize = 0; long finalTime = initTime; //TODO Make this configurable while (finalTime - initTime < 100) { // 100 ms ++defaultLoopSize; performUnitComputation(); //perform unit computation finalTime = monitor.getProcResourceValues().getCumulativeCpuTime(); } long referenceRuntime = finalTime - initTime; // time for one loop = (final-time - init-time) / total-loops float timePerLoop = ((float)referenceRuntime) / defaultLoopSize; // compute the 1% of the total CPU usage desired //TODO Make this configurable long onePercent = totalCpuUsage / 100; // num-iterations for 1% = (total-desired-usage / 100) / time-for-one-loop numIterations = Math.max(1, (int)((float)onePercent/timePerLoop)); System.out.println("Calibration done. Basic computation runtime : " + timePerLoop + " milliseconds. Optimal number of iterations (1%): " + numIterations); } } public CumulativeCpuUsageEmulatorPlugin() { this(new DefaultCpuUsageEmulator()); } /** * For testing. */ public CumulativeCpuUsageEmulatorPlugin(CpuUsageEmulatorCore core) { emulatorCore = core; } // Note that this weighing function uses only the current progress. In future, // this might depend on progress, emulation-interval and expected target. private float getWeightForProgressInterval(float progress) { // we want some kind of exponential growth function that gives less weight // on lower progress boundaries but high (exact emulation) near progress // value of 1. // so here is how the current growth function looks like // progress weight // 0.1 0.0001 // 0.2 0.0016 // 0.3 0.0081 // 0.4 0.0256 // 0.5 0.0625 // 0.6 0.1296 // 0.7 0.2401 // 0.8 0.4096 // 0.9 0.6561 // 1.0 1.000 return progress * progress * progress * progress; } private synchronized long getCurrentCPUUsage() { return monitor.getProcResourceValues().getCumulativeCpuTime(); } @Override public float getProgress() { return Math.min(1f, ((float)getCurrentCPUUsage())/targetCpuUsage); } @Override //TODO Multi-threading for speedup? public void emulate() throws IOException, InterruptedException { if (enabled) { float currentProgress = progress.getProgress(); if (lastSeenProgress < currentProgress && ((currentProgress - lastSeenProgress) >= emulationInterval || currentProgress == 1)) { // Estimate the final cpu usage // // Consider the following // Cl/Cc/Cp : Last/Current/Projected Cpu usage // Pl/Pc/Pp : Last/Current/Projected progress // Then // (Cp-Cc)/(Pp-Pc) = (Cc-Cl)/(Pc-Pl) // Solving this for Cp, we get // Cp = Cc + (1-Pc)*(Cc-Cl)/Pc-Pl) // Note that (Cc-Cl)/(Pc-Pl) is termed as 'rate' in the following // section long currentCpuUsage = getCurrentCPUUsage(); // estimate the cpu usage rate float rate = (currentCpuUsage - lastSeenCpuUsage) / (currentProgress - lastSeenProgress); long projectedUsage = currentCpuUsage + (long)((1 - currentProgress) * rate); if (projectedUsage < targetCpuUsage) { // determine the correction factor between the current usage and the // expected usage and add some weight to the target long currentWeighedTarget = (long)(targetCpuUsage * getWeightForProgressInterval(currentProgress)); while (getCurrentCPUUsage() < currentWeighedTarget) { emulatorCore.compute(); // sleep for 100ms try { Thread.sleep(100); } catch (InterruptedException ie) { String message = "CumulativeCpuUsageEmulatorPlugin got interrupted. Exiting."; throw new RuntimeException(message); } } } // set the last seen progress lastSeenProgress = progress.getProgress(); // set the last seen usage lastSeenCpuUsage = getCurrentCPUUsage(); } } } @Override public void initialize(Configuration conf, ResourceUsageMetrics metrics, ResourceCalculatorPlugin monitor, Progressive progress) { // get the target CPU usage targetCpuUsage = metrics.getCumulativeCpuUsage(); if (targetCpuUsage <= 0 ) { enabled = false; return; } else { enabled = true; } this.monitor = monitor; this.progress = progress; emulationInterval = conf.getFloat(CPU_EMULATION_PROGRESS_INTERVAL, DEFAULT_EMULATION_FREQUENCY); // calibrate the core cpu-usage utility emulatorCore.calibrate(monitor, targetCpuUsage); // initialize the states lastSeenProgress = 0; lastSeenCpuUsage = 0; } }