package org.prevayler.demos.scalability;
import org.prevayler.foundation.Cool;
import org.prevayler.foundation.StopWatch;
import java.text.DecimalFormat;
import java.util.LinkedList;
import java.util.List;

/** 
 * Represents a single run of a scalability test. To understand the implementation of this class, you must be familiar with Prevayler's Scalability Test (run org.prevayler.test.scalability.ScalabilityTest).
 */
abstract class ScalabilityTestRun {
  static private final long ROUND_DURATION_MILLIS=1000 * 20;
  private final ScalabilityTestSubject subject;
  protected final int numberOfObjects;
  private double bestRoundOperationsPerSecond;
  private int bestRoundThreads;
  private final List connectionCache=new LinkedList();
  private long operationCount=0;
  private long lastOperation=0;
  private boolean isRoundFinished;
  private int activeRoundThreads=0;
  /** 
 * @return Example: "123.12 operations/second (12 threads)".
 */
  public String getResult(){
    return toResultString(bestRoundOperationsPerSecond,bestRoundThreads);
  }
  public double getOperationsPerSecond(){
    return bestRoundOperationsPerSecond;
  }
  protected ScalabilityTestRun(  ScalabilityTestSubject subject,  int numberOfObjects,  int minThreads,  int maxThreads){
    if (minThreads > maxThreads)     throw new IllegalArgumentException("The minimum number of threads cannot be greater than the maximum number.");
    if (minThreads < 1)     throw new IllegalArgumentException("The minimum number of threads cannot be smaller than one.");
    this.subject=subject;
    this.numberOfObjects=numberOfObjects;
    out("\n\n========= Running " + name() + " ("+ (maxThreads - minThreads + 1)+ " rounds). Subject: "+ subject.name()+ "...");
    prepare();
    out("Each round will take approx. " + ROUND_DURATION_MILLIS / 1000 + " seconds to run...");
    performTest(minThreads,maxThreads);
    out("\n----------- BEST ROUND: " + getResult());
  }
  protected void prepare(){
    subject.replaceAllRecords(numberOfObjects);
    System.gc();
  }
  /** 
 * @return The name of the test to be executed. Example: "Prevayler Query Test".
 */
  protected abstract String name();
  private void performTest(  int minThreads,  int maxThreads){
    int threads=minThreads;
    while (threads <= maxThreads) {
      double operationsPerSecond=performRound(threads);
      if (operationsPerSecond > bestRoundOperationsPerSecond) {
        bestRoundOperationsPerSecond=operationsPerSecond;
        bestRoundThreads=threads;
      }
      threads++;
    }
  }
  /** 
 * @return The number of operations the test managed to execute per second with the given number of threads.
 */
  private double performRound(  int threads){
    long initialOperationCount=operationCount;
    StopWatch stopWatch=StopWatch.start();
    startThreads(threads);
    sleep();
    stopThreads();
    double secondsEllapsed=stopWatch.secondsEllapsed();
    double operationsPerSecond=(operationCount - initialOperationCount) / secondsEllapsed;
    out("\nMemory used: " + Runtime.getRuntime().totalMemory());
    subject.reportResourcesUsed(System.out);
    out("Seconds ellapsed: " + secondsEllapsed);
    out("--------- Round Result: " + toResultString(operationsPerSecond,threads));
    return operationsPerSecond;
  }
  private void startThreads(  int threads){
    isRoundFinished=false;
    int i=1;
    while (i <= threads) {
      startThread(lastOperation + i,threads);
      i++;
    }
  }
  private void startThread(  final long startingOperation,  final int operationIncrement){
    (new Thread(){
      public void run(){
        try {
          Object connection=acquireConnection();
          long operation=startingOperation;
          while (!isRoundFinished) {
            executeOperation(connection,operation);
            operation+=operationIncrement;
          }
synchronized (connectionCache) {
            connectionCache.add(connection);
            operationCount+=(operation - startingOperation) / operationIncrement;
            if (lastOperation < operation)             lastOperation=operation;
            activeRoundThreads--;
          }
        }
 catch (        OutOfMemoryError err) {
          outOfMemory();
        }
      }
    }
).start();
    activeRoundThreads++;
  }
  protected abstract void executeOperation(  Object connection,  long operation);
  private Object acquireConnection(){
synchronized (connectionCache) {
      return connectionCache.isEmpty() ? subject.createTestConnection() : connectionCache.remove(0);
    }
  }
  private void stopThreads(){
    isRoundFinished=true;
    while (activeRoundThreads != 0) {
      Thread.yield();
    }
  }
  static private String toResultString(  double operationsPerSecond,  int threads){
    String operations=new DecimalFormat("0.00").format(operationsPerSecond);
    return "" + operations + " operations/second ("+ threads+ " threads)";
  }
  static void outOfMemory(){
    System.gc();
    out("\n\nOutOfMemoryError.\n" + "===========================================================\n" + "The VM must be started with a sufficient maximum heap size.\n"+ "Example for Linux and Windows:  java -Xmx512000000 ...\n\n");
  }
  static private void sleep(){
    Cool.sleep(ROUND_DURATION_MILLIS);
  }
  static private void out(  Object obj){
    System.out.println(obj);
  }
}
