/*
 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
 *
 *  This file is licensed to You under the Common Public License (CPL);
 *  You may not use this file except in compliance with the License. You
 *  may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/cpl1.0.php
 *
 *  See the COPYRIGHT.txt file distributed with this work for information
 *  regarding copyright ownership.
 */
package org.jikesrvm.adaptive.controller;

import org.jikesrvm.VM;
import org.jikesrvm.adaptive.recompilation.CompilerDNA;
import org.jikesrvm.adaptive.util.AOSLogging;
import org.jikesrvm.classloader.NormalMethod;
import org.jikesrvm.compilers.common.CompiledMethod;

/**
 * This class encapsulates the analytic model used by the controller
 * to guide multi-level recompilation decisions.  An early version of
 * this model is described in the OOPSLA'2000 paper, but we've made
 * some improvements since then...
 *
 * @see MultiLevelAdaptiveModel
 */
abstract class AnalyticModel extends RecompilationStrategy {

  //---- Interface ------
  // Code that inherits from AnalyticModel must define the
  // following behavior

  /**
   * Initialize the set of "optimization choices" that the
   * cost-benefit model will consider when using will consider when
   * using adaptive compilation.
   */
  abstract void populateRecompilationChoices();

  /**
   * Compute the set of optimization choices that should be
   * considered by the cost-benefit model, given the previous compiler.
   *
   * @param prevCompiler The compiler compiler that was used to
   *                     comile cmpMethod
   * @param cmpMethod The compiled method being considered
   */
  abstract RecompilationChoice[] getViableRecompilationChoices(int prevCompiler, CompiledMethod cmpMethod);

  // -----------------------------------------------------
  // Below code that is (currently) common to all recompilation
  // strategies that use the analytic model.

  /**
   * Initialize the analytic model:
   *
   *  NOTE: The call to super.init() uses the command line options to
   *  set up the optimization plans, so this must be run after the
   *  command line options are available.
   */
  void init() {
    // Do the common initialization first
    super.init();

    // setup the recompilation choices that are available to the
    // analytic model
    populateRecompilationChoices();
  }

  /**
   * This method is the main decision making loop for all
   * recompilation strategies that use the analytic model.
   * <p>
   * Given a HotMethodRecompilationEvent, this code will determine
   * IF the method should be recompiled, and if so, HOW to perform
   * the recompilation, i.e., what compilation plan should be used.
   * The method returns a controller plan, which contains the compilation
   * plan and other goodies.
   *
   * @param cmpMethod the compiled method of interest
   * @param hme       the HotMethodRecompilationEvent
   * @return the controller plan to be used or NULL, if no
   *                   compilation is to be performed.  */
  ControllerPlan considerHotMethod(CompiledMethod cmpMethod, HotMethodEvent hme) {
    // Compiler used for the previous compilation
    int prevCompiler = getPreviousCompiler(cmpMethod);
    if (prevCompiler == -1) {
      return null; // Not a method that we can recompile (trap, JNI).
    }

    ControllerPlan plan = ControllerMemory.findMatchingPlan(cmpMethod);

    // for a outdated hot method from baseline, we consider OSR,
    // and execute plan in the routine, no more action here
    if (considerOSRRecompilation(cmpMethod, hme, plan)) return null;

    if (!considerForRecompilation(hme, plan)) return null;

    // Now we know the compiler that generated the method (prevCompiler) and
    // that the method is a potential candidate for additional recompilation.
    // So, next decide what, if anything, should be done now.
    // We consider doing nothing (ie leaving the method at the current
    // opt level, which incurs no  compilation cost), and recompiling the
    // method at each greater compilation level.
    double futureTimeForMethod = futureTimeForMethod(hme);

    // initialize bestAction as doing nothing, which means we'll
    // spend just as much time in the method in the future as we have so far.
    RecompilationChoice bestActionChoice = null;
    double bestActionTime = futureTimeForMethod;
    double bestCost = 0.0;

    AOSLogging.recordControllerEstimateCostDoNothing(cmpMethod.getMethod(),
                                                        CompilerDNA.getOptLevel(prevCompiler),
                                                        bestActionTime);

    // Get a vector of optimization choices to consider
    RecompilationChoice[] recompilationChoices = getViableRecompilationChoices(prevCompiler, cmpMethod);

    // Consider all choices in the vector of possibilities
    NormalMethod meth = (NormalMethod) hme.getMethod();
    for (RecompilationChoice choice : recompilationChoices) {
      // Get the cost and benefit of this choice
      double cost = choice.getCost(meth);
      double futureExecutionTime = choice.getFutureExecutionTime(prevCompiler, futureTimeForMethod);

      double curActionTime = cost + futureExecutionTime;

      AOSLogging.recordControllerEstimateCostOpt(cmpMethod.getMethod(), choice.toString(), cost, curActionTime);

      if (curActionTime < bestActionTime) {
        bestActionTime = curActionTime;
        bestActionChoice = choice;
        bestCost = cost;
      }
    }

    // if the best action is the previous than we don't need to recompile
    if (bestActionChoice == null) {
      plan = null;
    } else {
      plan =
          bestActionChoice.makeControllerPlan(cmpMethod, prevCompiler, futureTimeForMethod, bestActionTime, bestCost);
    }
    return plan;
  }

  /* check if a compiled method is outdated, then decide if it needs OSR from BASE to OPT
   */
  boolean considerOSRRecompilation(CompiledMethod cmpMethod, HotMethodEvent hme, ControllerPlan plan) {
    boolean outdatedBaseline = false;
    if (plan == null) {
      // if plan is null, this method was not compiled by AOS; it was
      // either in the boot image or compiled by the initial baseline
      // compiler.  In either case, if we've completed any recompilation
      // then the compiled method is outdated.
      outdatedBaseline =
          ControllerMemory.planWithStatus(cmpMethod.getMethod(), ControllerPlan.COMPLETED) &&
          cmpMethod.getCompilerType() == CompiledMethod.BASELINE;
      if (outdatedBaseline) {
        AOSLogging.debug("outdated Baseline " + cmpMethod.getMethod() + "(" + cmpMethod.getId() + ")");
      }
    }

    // consider OSR option for old baseline-compiled activation
    if (outdatedBaseline) {
      if (!hme.getCompiledMethod().getSamplesReset()) {
        // the first time we see an outdated event, we clear the samples
        // associated with the cmid.
        hme.getCompiledMethod().setSamplesReset();
        Controller.methodSamples.reset(hme.getCMID());
        AOSLogging.debug(" Resetting method samples " + hme);
        return true;
      } else {
        plan = chooseOSRRecompilation(hme);
        // insert the plan to memory, which sets up state in the system to trigger
        // the OSR promotion
        if (plan != null) {
          ControllerMemory.insert(plan);
          // to coordinate with OSRListener, it marks cmpMethod as outdated
          if (VM.VerifyAssertions) {
            VM._assert(cmpMethod.getCompilerType() == CompiledMethod.BASELINE);
          }
          cmpMethod.setOutdated();
        }
        // we don't do any more action on the controller side.
        return true;
      }
    }
    return false;
  }

  /**
   * @param hme sample data for an outdated cmid
   * @return a plan representing recompilation with OSR, null if OSR not
   * justified.
   */
  private ControllerPlan chooseOSRRecompilation(HotMethodEvent hme) {
    if (!Controller.options.OSR_PROMOTION) return null;

    AOSLogging.debug(" Consider OSR for " + hme);

    ControllerPlan prev = ControllerMemory.findLatestPlan(hme.getMethod());

    if (prev.getStatus() == ControllerPlan.OSR_BASE_2_OPT) {
      AOSLogging.debug(" Already have an OSR promotion plan for this method");
      return null;
    }

    double millis = (double) (prev.getTimeCompleted() - prev.getTimeInitiated());
    double speedup = prev.getExpectedSpeedup();
    double futureTimeForMethod = futureTimeForMethod(hme);

    double futureTimeOptimized = futureTimeForMethod / speedup;

    AOSLogging.debug(" Estimated future time for method " + hme + " is " + futureTimeForMethod);
    AOSLogging.debug(" Estimated future time optimized " + hme + " is " + (futureTimeOptimized + millis));

    if (futureTimeForMethod > futureTimeOptimized + millis) {
      AOSLogging.recordOSRRecompilationDecision(prev);
      ControllerPlan p =
          new ControllerPlan(prev.getCompPlan(),
                                prev.getTimeCreated(),
                                hme.getCMID(),
                                prev.getExpectedSpeedup(),
                                millis,
                                prev.getPriority());
      // set up state to trigger osr
      p.setStatus(ControllerPlan.OSR_BASE_2_OPT);
      return p;
    } else {
      return null;
    }
  }

  /**
   * This function defines how the analytic model handles a
   * AINewHotEdgeEvent.  The basic idea is to use the model to
   * evaluate whether it would be better to do nothing or to recompile
   * at the same opt level, assuming there would be some "boost" after
   * performing inlining.
   */
  void considerHotCallEdge(CompiledMethod cmpMethod, AINewHotEdgeEvent event) {

    // Compiler used for the previous compilation
    int prevCompiler = getPreviousCompiler(cmpMethod);
    if (prevCompiler == -1) {
      return; // Not a method we can recompile (trap, JNI).
    }

    ControllerPlan plan = ControllerMemory.findMatchingPlan(cmpMethod);
    if (!considerForRecompilation(event, plan)) return;
    double prevCompileTime = cmpMethod.getCompilationTime();

    // Use the model to caclulate expected cost of (1) doing nothing
    // and (2) recompiling at the same opt level with the FDO boost
    double futureTimeForMethod = futureTimeForMethod(event);
    double futureTimeForFDOMethod = prevCompileTime + (futureTimeForMethod / event.getBoostFactor());

    int prevOptLevel = CompilerDNA.getOptLevel(prevCompiler);
    AOSLogging.recordControllerEstimateCostDoNothing(cmpMethod.getMethod(), prevOptLevel, futureTimeForMethod);
    AOSLogging.recordControllerEstimateCostOpt(cmpMethod.getMethod(),
                                                  "O" + prevOptLevel + "AI",
                                                  prevCompileTime,
                                                  futureTimeForFDOMethod);

    if (futureTimeForFDOMethod < futureTimeForMethod) {
      // Profitable to recompile with FDO, so do it.
      int optLevel = CompilerDNA.getOptLevel(prevCompiler);
      double priority = futureTimeForMethod - futureTimeForFDOMethod;
      plan =
          createControllerPlan(cmpMethod.getMethod(),
                               optLevel,
                               null,
                               cmpMethod.getId(),
                               event.getBoostFactor(),
                               futureTimeForFDOMethod,
                               priority);
      plan.execute();
    }
  }

  /**
   * How much time do we expect to spend in the method in the future if
   * we take no recompilation action?
   * The key assumption is that we'll spend just as much time
   * executing in the the method in the future as we have done so far
   * in the past.
   *
   * @param hme The HotMethodEvent in question
   * @return estimate of future execution time to be spent in this method
   */
  double futureTimeForMethod(HotMethodEvent hme) {
    double numSamples = hme.getNumSamples();
    double timePerSample = (double) VM.interruptQuantum;
    if (!VM.UseEpilogueYieldPoints) {
      // NOTE: we take two samples per timer interrupt, so we have to
      // adjust here (otherwise we'd give the method twice as much time
      // as it actually deserves).
      timePerSample /= 2.0;
    }
    if (Controller.options.mlCBS()) {
      // multiple method samples per timer interrupt. Divide accordingly.
      timePerSample /= (double) VM.CBSMethodSamplesPerTick;
    }
    double timeInMethodSoFar = numSamples * timePerSample;
    return timeInMethodSoFar;
  }
}
