/*
 *  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.mmtk.utility.heap;

import org.mmtk.plan.Plan;
import org.mmtk.utility.*;
import org.mmtk.utility.options.Options;

import org.mmtk.vm.VM;

import org.vmmagic.pragma.*;
import org.vmmagic.unboxed.*;

/**
 * This class is responsible for growing and shrinking the
 * heap size by observing heap utilization and GC load.
 */
@Uninterruptible public abstract class HeapGrowthManager implements Constants {

  /**
   * The initial heap size (-Xms) in bytes
   */
  private static Extent initialHeapSize;

  /**
   * The maximum heap size (-Xms) in bytes
   */
  private static Extent maxHeapSize;

  /**
   * The current heap size in bytes
   */
  private static Extent currentHeapSize;


  private static final double[][] generationalFunction =    {{0.00, 0.00, 0.10, 0.30, 0.60, 0.80, 1.00},
      { 0.00, 0.90, 0.90, 0.95, 1.00, 1.00, 1.00 },
      { 0.01, 0.90, 0.90, 0.95, 1.00, 1.00, 1.00 },
      { 0.02, 0.95, 0.95, 1.00, 1.00, 1.00, 1.00 },
      { 0.07, 1.00, 1.00, 1.10, 1.15, 1.20, 1.20 },
      { 0.15, 1.00, 1.00, 1.20, 1.25, 1.35, 1.30 },
      { 0.40, 1.00, 1.00, 1.25, 1.30, 1.50, 1.50 },
      { 1.00, 1.00, 1.00, 1.25, 1.30, 1.50, 1.50 } };

  private static final double[][] nongenerationalFunction = {{0.00, 0.00, 0.10, 0.30, 0.60, 0.80, 1.00},
      { 0.00, 0.90, 0.90, 0.95, 1.00, 1.00, 1.00 },
      { 0.02, 0.90, 0.90, 0.95, 1.00, 1.00, 1.00 },
      { 0.05, 0.95, 0.95, 1.00, 1.00, 1.00, 1.00 },
      { 0.15, 1.00, 1.00, 1.10, 1.15, 1.20, 1.20 },
      { 0.30, 1.00, 1.00, 1.20, 1.25, 1.35, 1.30 },
      { 0.50, 1.00, 1.00, 1.25, 1.30, 1.50, 1.50 },
      { 1.00, 1.00, 1.00, 1.25, 1.30, 1.50, 1.50 } };

  /**
   * An encoding of the function used to manage heap size.
   * The xaxis represents the live ratio at the end of a major collection.
   * The yaxis represents the GC load (GC time/total time).
   * The interior of the matrix represents a ratio to shrink or grow
   * the heap for a given pair of live ratio and GC load.
   * The constraints on the matrix are:
   * <ul>
   * <li> function[0][0] is ignored.
   * <li> All numbers in the first row must monotonically increase and
   *      must be in the range from 0 to 1 inclusive.</li>
   * <li> All numbers in the first column must monotonically increase
   *      and must be in the range from 0 to 1 inclusive.</li>
   * <li> There must be 0 and 1 values specified in both dimensions.
   * <li> For all interior points in the matrix, the value must be
   *      greater than the liveRatio for that column.</li>
   * </ul>
   */
  private static final double[][] function =
    VM.activePlan.constraints().generational() ? generationalFunction : nongenerationalFunction;

  private static long endLastMajorGC;
  private static double accumulatedGCTime;

  /**
   * Initialize heap size parameters and the mechanisms
   * used to adaptively change heap size.
   */
  public static void boot(Extent initial, Extent max) {
    initialHeapSize = initial;
    maxHeapSize = max;
    if (initialHeapSize.GT(maxHeapSize))
      maxHeapSize = initialHeapSize;
    currentHeapSize = initialHeapSize;
    if (VM.VERIFY_ASSERTIONS) sanityCheck();
    endLastMajorGC = VM.statistics.nanoTime();
  }

  /**
   * @return the current heap size in bytes
   */
  public static Extent getCurrentHeapSize() {
    return currentHeapSize;
  }

  /**
   * Return the max heap size in bytes (as set by -Xmx).
   *
   * @return The max heap size in bytes (as set by -Xmx).
   */
  public static Extent getMaxHeapSize() {
    return maxHeapSize;
  }

  /**
   * Return the initial heap size in bytes (as set by -Xms).
   *
   * @return The initial heap size in bytes (as set by -Xms).
   */
  public static Extent getInitialHeapSize() {
    return initialHeapSize;
  }

  /**
   * Forcibly grow the heap by the given number of bytes.
   * Used to provide headroom when handling an OutOfMemory
   * situation.
   * @param size number of bytes to grow the heap
   */
  public static void overrideGrowHeapSize(Extent size) {
    currentHeapSize = currentHeapSize.plus(size);
  }

  /**
   * Record the time taken by the current GC;
   * used to compute gc load, one of the inputs
   * into the heap size management function
   */
  public static void recordGCTime(double time) {
    accumulatedGCTime += time;
  }

  /**
   * Reset timers used to compute gc load
   */
  public static void reset() {
    endLastMajorGC = VM.statistics.nanoTime();
    accumulatedGCTime = 0;
  }

  /**
   * Decide how to grow/shrink the heap to respond
   * to application's memory usage.
   * @return true if heap size was changed, false otherwise
   */
  public static boolean considerHeapSize() {
    Extent oldSize = currentHeapSize;
    Extent reserved = Plan.reservedMemory();
    double liveRatio = reserved.toLong() / ((double) currentHeapSize.toLong());
    double ratio = computeHeapChangeRatio(liveRatio);
    Extent newSize = Word.fromIntSignExtend((int)(ratio * (double) (oldSize.toLong()>>LOG_BYTES_IN_MBYTE))).lsh(LOG_BYTES_IN_MBYTE).toExtent(); // do arith in MB to avoid overflow
    if (newSize.LT(reserved)) newSize = reserved;
    newSize = newSize.plus(BYTES_IN_MBYTE - 1).toWord().rshl(LOG_BYTES_IN_MBYTE).lsh(LOG_BYTES_IN_MBYTE).toExtent(); // round to next megabyte
    if (newSize.GT(maxHeapSize)) newSize = maxHeapSize;
    if (newSize.NE(oldSize) && newSize.GT(Extent.zero())) {
      // Heap size is going to change
      currentHeapSize = newSize;
      if (Options.verbose.getValue() >= 2) {
        Log.write("GC Message: Heap changed from "); Log.writeDec(oldSize.toWord().rshl(LOG_BYTES_IN_KBYTE));
        Log.write("KB to "); Log.writeDec(newSize.toWord().rshl(LOG_BYTES_IN_KBYTE));
        Log.writeln("KB");
      }
      return true;
    } else {
      return false;
    }
  }

  private static double computeHeapChangeRatio(double liveRatio) {
    // (1) compute GC load.
    long totalNanos = VM.statistics.nanoTime() - endLastMajorGC;
    double totalTime = VM.statistics.nanosToMillis(totalNanos);
    double gcLoad = accumulatedGCTime / totalTime;

    if (liveRatio > 1) {
      // Perhaps indicates bad bookkeeping in MMTk?
      Log.write("GCWarning: Live ratio greater than 1: ");
      Log.writeln(liveRatio);
      liveRatio = 1;
    }
    if (gcLoad > 1) {
      if (gcLoad > 1.0001) {
        Log.write("GC Error: GC load was greater than 1!! ");
        Log.writeln(gcLoad);
        Log.write("GC Error:\ttotal time (ms) "); Log.writeln(totalTime);
        Log.write("GC Error:\tgc time (ms) "); Log.writeln(accumulatedGCTime);
      }
      gcLoad = 1;
    }
    if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(liveRatio >= 0);
    if (VM.VERIFY_ASSERTIONS && gcLoad < -0.0) {
      Log.write("gcLoad computed to be "); Log.writeln(gcLoad);
      Log.write("\taccumulateGCTime was (ms) "); Log.writeln(accumulatedGCTime);
      Log.write("\ttotalTime was (ms) "); Log.writeln(totalTime);
      if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(false);
    }

    if (Options.verbose.getValue() > 2) {
      Log.write("Live ratio "); Log.writeln(liveRatio);
      Log.write("GCLoad     "); Log.writeln(gcLoad);
    }

    // (2) Find the 4 points surrounding gcLoad and liveRatio
    int liveRatioUnder = 1;
    int liveRatioAbove = function[0].length - 1;
    int gcLoadUnder = 1;
    int gcLoadAbove = function.length - 1;
    while (true) {
      if (function[0][liveRatioUnder+1] >= liveRatio) break;
      liveRatioUnder++;
    }
    while (true) {
      if (function[0][liveRatioAbove-1] <= liveRatio) break;
      liveRatioAbove--;
    }
    while (true) {
      if (function[gcLoadUnder+1][0] >= gcLoad) break;
      gcLoadUnder++;
    }
    while (true) {
      if (function[gcLoadAbove-1][0] <= gcLoad) break;
      gcLoadAbove--;
    }

    // (3) Compute the heap change ratio
    double factor = function[gcLoadUnder][liveRatioUnder];
    double liveRatioFraction =
      (liveRatio - function[0][liveRatioUnder]) /
      (function[0][liveRatioAbove] - function[0][liveRatioUnder]);
    double liveRatioDelta =
      function[gcLoadUnder][liveRatioAbove] - function[gcLoadUnder][liveRatioUnder];
    factor += (liveRatioFraction * liveRatioDelta);
    double gcLoadFraction =
      (gcLoad - function[gcLoadUnder][0]) /
      (function[gcLoadAbove][0] - function[gcLoadUnder][0]);
    double gcLoadDelta =
      function[gcLoadAbove][liveRatioUnder] - function[gcLoadUnder][liveRatioUnder];
    factor += (gcLoadFraction * gcLoadDelta);

    if (Options.verbose.getValue() > 2) {
      Log.write("Heap adjustment factor is ");
      Log.writeln(factor);
    }
    return factor;
  }

  /**
   * Check that function satisfies the invariants
   */
  private static void sanityCheck() {
    // Check live ratio
    double[] liveRatio = function[0];
    if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(liveRatio[1] == 0);
    if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(liveRatio[liveRatio.length-1] == 1);
    for (int i = 2; i < liveRatio.length; i++) {
      if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(liveRatio[i-1] < liveRatio[i]);
      for (int j = 1; j < function.length; j++) {
        if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(function[j][i] >= 1 || function[j][i] > liveRatio[i]);
      }
    }

    // Check GC load
    if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(function[1][0] == 0);
    int len = function.length;
    if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(function[len-1][0] == 1);
    for (int i = 2; i < len; i++) {
      if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(function[i-1][0] < function[i][0]);
    }

    // Check that we have a rectangular matrix
    for (int i = 1; i < function.length; i++) {
      if (VM.VERIFY_ASSERTIONS) VM.assertions._assert(function[i-1].length == function[i].length);
    }
  }


}
