/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.test.perf.test;
import org.jboss.test.perf.interfaces.*;

import java.util.*;

public class Thrasher implements Runnable {

  static final int ACTION_SINGLE_READ  = 0;
  static final int ACTION_SINGLE_WRITE = 1;
  static final int ACTION_BLOCK_READ   = 2;
  static final int ACTION_BLOCK_WRITE  = 3;
  static final int ACTION_FAILURE      = 4;
  static final int ACTION_COUNT        = 5;
  
  static int[] timers   = new int[ACTION_COUNT];
  static int[] counters = new int[ACTION_COUNT];

  static final String[] ACTION_LABELS = {
    "single reads ",
    "single writes",
    "block reads  ",
    "block writes ",
    "**FAILURES** ",
  };

  static final int ARG_THREADS		=  0;
  static final int ARG_ITERATIONS	=  1;
  static final int ARG_READ_PERC	=  2;
  static final int ARG_BLOCK_PERC	=  3;
  static final int ARG_BLOCK_MIN	=  4;
  static final int ARG_BLOCK_MAX	=  5;
  static final int ARG_SLEEP_MIN	=  6;
  static final int ARG_SLEEP_MAX	=  7;
  static final int ARG_ID_MIN		=  8;
  static final int ARG_ID_MAX		=  9;
  static final int ARG_COUNT		= 10;

  static final String[] ARG_LABELS = {
    "threads   ",	"How many threads to run",
    "iterations",	"How many iterations per thread",
    "read_perc ",	"What percentage of access is readonly",
    "block_perc",	"What percentage of access is block oriented",
    "block_min ",	"The minimum block size per access (inclusive)",
    "block_max ",	"The maximum block size per access (exclusive)",
    "sleep_min ",	"The minimum sleep period between accesses in ms (inclusive)",
    "sleep_max ",	"The maximum sleep period between accesses in ms (exclusive)",
    "id_min    ",	"The minimum id accessed (inclusive)",
    "id_max    ",	"The maximum id accessed (exclusive)",
  };

  static final int[] argValues = new int[ARG_COUNT];

  static private void usage() {
    System.out.println("Usage: java Thrasher <jndi-name> [-options]\n\n" + 
                       "  where options include:");
    for(int a = 0; a < ARG_COUNT; a++) {
      System.out.println("    -" + ARG_LABELS[a * 2] + 
                         " <n> \t" + ARG_LABELS[a * 2 + 1]);
    }
    System.exit(1);
  }

  public static void main(String[] args) throws Exception {

    if(args.length < 1) {
      usage();
    }
    String jndiName = args[0];

    int[] argValues = new int[ARG_COUNT];
    argValues[ARG_THREADS]    = 1;
    argValues[ARG_ITERATIONS] = 20;
    argValues[ARG_READ_PERC]  = 90;
    argValues[ARG_BLOCK_PERC] = 10;
    argValues[ARG_BLOCK_MIN]  = 2;
    argValues[ARG_BLOCK_MAX]  = 10;
    argValues[ARG_SLEEP_MIN]  = 0;
    argValues[ARG_SLEEP_MAX]  = 0;
    argValues[ARG_ID_MIN]     = 0;
    argValues[ARG_ID_MAX]     = 100;

    for(int i = 1; i < args.length; i++) {
      boolean found = false;
      for(int a = 0; a < ARG_COUNT; a++) {
        String argLabel = "-" + ARG_LABELS[a * 2].trim();
        if(args[i].equals(argLabel)) {
          found = true;
          if(++i >= args.length) {
            System.out.println("Value expected for argument: " + argLabel);
            System.exit(1);
          }
          int value;
          try {
            value = Integer.parseInt(args[i]);
          }
          catch(NumberFormatException e) {
            System.out.println("Numeric value expected for argument: " + argLabel);
            System.out.println("  error: " + e);
            System.exit(1);
            return;
          }
          argValues[a] = value;
          break;
        }
      }
      if(!found) {
        usage();
      }
    } 
    Thrasher[] thrashers = new Thrasher[argValues[ARG_THREADS]];
    {
      Thread[] threads = new Thread[argValues[ARG_THREADS]];
      for(int i = 0; i < argValues[ARG_THREADS]; i++) {
        thrashers[i] = new Thrasher(jndiName, argValues);
        threads[i] = new Thread(thrashers[i], "Thrasher[" + i + "]");
        if(i < 5 ||
           i >= argValues[ARG_THREADS] - 5) {
          System.out.println("Starting thread: " + threads[i]);
        }
        threads[i].start();
      }
      for(int i = 0; i < argValues[ARG_THREADS]; i++) {
        threads[i].join();
      }
    }
    System.out.println("Configuration:");
    for(int a = 0; a < ARG_COUNT; a++) {
      System.out.println("\t" + ARG_LABELS[a * 2] + "\t" + argValues[a]);
    }
    {
      System.out.println("Actions:");
      for(int i = 0; i < ACTION_COUNT; i++) {
        if(counters[i] != 0) {
          float avg = (float) timers[i] / counters[i];
          // round the value...
          avg = (long) (avg * 10) / 10f;
          System.out.println("\t" + counters[i] + "\t" +
                             ACTION_LABELS[i] + "\tavg:\t" + 
                             avg + "\tms/Tx");
        }
      }
    }
    System.out.println("Performance:");
    long totalTime = 0;
    for(int i = 0; i < argValues[ARG_THREADS]; i++) {
      long thrasherTime = thrashers[i]._elapsedTime;
      totalTime += thrasherTime;
      if(i < 5 ||
         i >= argValues[ARG_THREADS] - 5) {
        // only print out the first and last 5 enties...
        float msPerTx = (float) thrasherTime / argValues[ARG_ITERATIONS];
        float txPerSec = 1000 / msPerTx;
        // round the values...
        msPerTx = (long) (msPerTx * 10) / 10f;
        txPerSec = (long) (txPerSec * 1000) / 1000f;
        System.out.println("\tThread[" + i + "]: \t" + 
                           txPerSec + "\tTx/s \t" +
                           msPerTx + "\tms/Tx");
      }
    }
    {
      float msPerTx = (float) totalTime / argValues[ARG_THREADS] / argValues[ARG_ITERATIONS];
      float txPerSec = 1000 / msPerTx;
      // round the values...
      msPerTx = (long) (msPerTx * 10) / 10f;
      txPerSec = (long) (txPerSec * 1000) / 1000f;
      System.out.println("\tAverage: \t" + 
                         txPerSec + "\tTx/s \t" +
                         msPerTx + "\tms/Tx");
      float throughput = txPerSec * argValues[ARG_THREADS];
      // round the value...
      throughput = (long) (throughput * 1000) / 1000f;
      System.out.println("\tThroughput: \t" + throughput + "\tTx/s");
    }
  }

  private String _jndiName;
  private int[] _args;
  private long _elapsedTime;

  private Thrasher(String jndiName, int[] args) {
    _jndiName = jndiName;
    _args = args;
  }

  public void run() {
    try { 
      javax.naming.Context context = new javax.naming.InitialContext();
      Object ref = context.lookup("Session");
      SessionHome sessionHome = (SessionHome) ref;
      /** CHANGES: Note that WebLogic does not support
       ** Spec Compliant PortableRemoteObject way of
       ** narrow
        //(SessionHome) javax.rmi.PortableRemoteObject.narrow(ref, SessionHome.class);
       **/
      Session session = sessionHome.create(_jndiName);
      java.util.Random random = new java.util.Random();
      for(int i = 0; i < _args[ARG_ITERATIONS]; i++) {
        boolean doBlocks   = random.nextFloat() < _args[ARG_BLOCK_PERC] / 100f;
        boolean doReadonly = random.nextFloat() < _args[ARG_READ_PERC]  / 100f;
        int id = getRandom(random, _args[ARG_ID_MIN], _args[ARG_ID_MAX]);
        int blockSize = doBlocks ? getRandom(random, _args[ARG_BLOCK_MIN], _args[ARG_BLOCK_MAX]) : 0;
        long before = System.currentTimeMillis();
        int action;
        if(doBlocks) {
          if(id + blockSize > _args[ARG_ID_MAX]) {
            // if the blocksize were going to overrun the border, shift the start back
            id = _args[ARG_ID_MAX] - blockSize;
          }
          if(doReadonly) {
            session.read(id, id + blockSize);
            action = ACTION_BLOCK_READ;
          }
          else {
            session.write(id, id + blockSize);
            action = ACTION_BLOCK_WRITE;
          }
        }
        else {
          if(doReadonly) {
            session.read(id);
            action = ACTION_SINGLE_READ;
          }
          else {
            session.write(id);
            action = ACTION_SINGLE_WRITE;
          }
        }
        long after = System.currentTimeMillis();
        _elapsedTime += after - before;
        timers[action] += after - before;
        counters[action]++;
        int sleep = getRandom(random, _args[ARG_SLEEP_MIN], _args[ARG_SLEEP_MAX]);
        if(sleep != 0) {
          try {
            Thread.currentThread().sleep(sleep);
          }
          catch(InterruptedException e) {
            // continue...
          }
        }
      }
      session.remove();
    }
    catch(Exception e) {
      counters[ACTION_FAILURE]++;
      e.printStackTrace();
    }
  }

  static int getRandom(java.util.Random random, int min, int max) {
    // first get a random non-negative number
    int result = Math.abs(random.nextInt());
    // get it into the specified range
    int range = max - min;
    if(range <= 0) {
      return min;
    }
    result %= range;
    result += min;
    return result;
  }

}
