/*
 *
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package org.jboss.cache;


import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.transaction.DummyTransactionManager;
import org.jgroups.util.Util;

import javax.transaction.Transaction;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.util.ArrayList;

/**
 * Replicated synchronous mode performance test for transactional TreeCache.
 *
 * @author Bela Ban
 * @version $Revision: 2015 $
 * @author <a href="mailto:bwang@jboss.org">Ben Wang</a> May 20 2003
 */
public class ReplicatedSyncPerfTest extends TestCase {
   TreeCache cache1_, cache2_, cache3_;
   final String groupName_="TreeCacheTestGroup";
   Transaction tx;

   ArrayList nodeList_;
   // (4, 4) combination will generate 340 nodes.
   static final int depth_=3;
   static final int children_=4;


   public ReplicatedSyncPerfTest(String name) {
      super(name);
   }

   public void setUp() throws Exception {
      super.setUp();
      nodeList_=nodeGen(depth_, children_);
   }

   public void tearDown() throws Exception {
      super.tearDown();
      destroyCache(cache1_);
      destroyCache(cache2_);
      destroyCache(cache3_);
      if(tx != null)
         tx.rollback();
   }

   TreeCache createCache(IsolationLevel level) throws Exception {
      TreeCache cache=new TreeCache();
      PropertyConfigurator config=new PropertyConfigurator();
      config.configure(cache, "META-INF/replSync-service.xml"); // use generic syncRepl xml
      cache.setIsolationLevel(level);
      cache.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
      cache.setSyncCommitPhase(true);
      cache.setSyncRollbackPhase(true);
      cache.setLockAcquisitionTimeout(2000);
      return cache;
   }

   void destroyCache(TreeCache cache) throws Exception {
      if(cache != null) {
         // cache.remove("/");
         cache.stopService();
      }
   }

   private Transaction startTransaction() throws Exception {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      return mgr.getTransaction();
   }


   /**
    * Executes a lot of put() operations against the cache
    */
   public void testPuts() throws Exception {
      log("=== 1 cache with transaction (no concurrent access) many puts ===");
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.setCacheMode(TreeCache.REPL_SYNC);
      cache1_.startService();

      int nOps=10000, realOps=0, failedOps=0;
      long time1=System.currentTimeMillis();

      try {
         for(int i=0; i < nOps; i++) {
            if(i % 1000 == 0 && i > 0)
               System.out.println(i);
            tx=startTransaction();
            cache1_.put("/hello/world", "name", "Bela Ban");
            tx.commit();
            realOps++;
         }
      }
      catch(Throwable t) {
         failedOps++;
         t.printStackTrace();
         System.out.println("tree is " + cache1_.printLockInfo());
         if(tx != null)
            tx.rollback();
      }
      long time2=System.currentTimeMillis();
      assertEquals("failedOps=" + failedOps, nOps, realOps);

      double d=(double)(time2 - time1) / realOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + realOps
          + " operations. Average per ops is: " + d + " msec.");
   }




   public void testRandomPuts() throws Exception {
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.setCacheMode(TreeCache.REPL_SYNC);
      cache1_.startService();

      int nOps=5000, realOps=0, failedOps=0;
      long time1=System.currentTimeMillis();
      String fqn;

      long key;
      log("=== 1 cache with transaction (no concurrent access) " + nOps + " puts ===");

      try {
         for(int i=0; i < nOps; i++) {
            if(i % 1000 == 0 && i > 0)
               System.out.println(i);
            tx=startTransaction();
            key=Util.random(nOps);
            fqn="/random/" + key;
            cache1_.put(fqn, "name", "Bela Ban");
            tx.commit();
            assertEquals(0, cache1_.getNumberOfLocksHeld());
            realOps++;
         }
      }
      catch(Throwable t) {
         failedOps++;
         t.printStackTrace();
         System.out.println("cache1_ locks: " + cache1_.printLockInfo());
         if(tx != null)
            tx.rollback();
      }
      long time2=System.currentTimeMillis();
      assertEquals("failedOps=" + failedOps, nOps, realOps);

      double d=(double)(time2 - time1) / realOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + realOps
          + " operations. Average per ops is: " + d + " msec.");
   }

   public void testOneCacheTx() throws Exception {
      log("=== 1 cache with transaction (no concurrent access) ===");
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.startService();

      // Formating
      DecimalFormat form=new DecimalFormat("#.00");
      FieldPosition fieldPos=new FieldPosition(0);
      StringBuffer dumbStr=new StringBuffer();
      boolean hasTx=true;
      boolean oneTxOnly=false;

      System.out.println("sync comm phase: " + cache1_.getSyncCommitPhase() + ", sync rollback phase: " +
                         cache1_.getSyncRollbackPhase());

      // Step 1. Add entries to the cache
      System.out.println("-- before add: number of locks held is " + cache1_.getNumberOfLocksHeld());
      long time1=System.currentTimeMillis();
      int nOps=add(cache1_, hasTx, oneTxOnly);
      long time2=System.currentTimeMillis();
      System.out.println("-- after add: number of locks held is " + cache1_.getNumberOfLocksHeld());
      double d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 2. Query the cache
      System.out.println("-- before get: number of locks held is " + cache1_.getNumberOfLocksHeld());
      time1=System.currentTimeMillis();
      nOps=get(cache1_, hasTx, oneTxOnly);
      time2=System.currentTimeMillis();
      System.out.println("-- after get: number of locks held is " + cache1_.getNumberOfLocksHeld());
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _get is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 3. Remove entries from the cache
      System.out.println("-- before remove: number of locks held is " + cache1_.getNumberOfLocksHeld());
      time1=System.currentTimeMillis();
      nOps=remove(cache1_, hasTx, oneTxOnly);
      time2=System.currentTimeMillis();
      System.out.println("-- after remove: number of locks held is " + cache1_.getNumberOfLocksHeld());
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _remove is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
   }

   public void test2CachesTx() throws Exception {
      log("=== 2 caches with transaction (no concurrent access) ===");
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache2_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.startService();
      cache2_.startService();

      // Formating
      DecimalFormat form=new DecimalFormat("#.00");
      FieldPosition fieldPos=new FieldPosition(0);
      StringBuffer dumbStr=new StringBuffer();
      boolean hasTx=true;
      boolean oneTxOnly=false;

      // Step 1. Add entries to the cache
      long time1=System.currentTimeMillis();
      int nOps=add(cache1_, hasTx, oneTxOnly);
      long time2=System.currentTimeMillis();
      double d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 2. Query the cache
      time1=System.currentTimeMillis();
      nOps=get(cache1_, hasTx, oneTxOnly);
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _get is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 3. Remove entries from the cache
      time1=System.currentTimeMillis();
      nOps=remove(cache2_, hasTx, oneTxOnly);  // Note we remove nodes from cache2.
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _remove is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
   }

   public void test2CachesOneTxOnly() throws Exception {
      log("=== 2 caches with single transaction only (no concurrent access) ===");
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache2_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.startService();
      cache2_.startService();

      // Formating
      DecimalFormat form=new DecimalFormat("#.00");
      FieldPosition fieldPos=new FieldPosition(0);
      StringBuffer dumbStr=new StringBuffer();
      boolean hasTx=true;
      boolean oneTxOnly=true;

      // Step 1. Add entries to the cache
      long time1=System.currentTimeMillis();
      int nOps=add(cache1_, hasTx, oneTxOnly);
      long time2=System.currentTimeMillis();
      double d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 2. Query the cache
      time1=System.currentTimeMillis();
      nOps=get(cache1_, hasTx, oneTxOnly);
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _get is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 3. Remove entries from the cache
      time1=System.currentTimeMillis();
      nOps=remove(cache2_, hasTx, oneTxOnly);  // Note we remove nodes from cache2.
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _remove is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
   }

   public void test3CachesTx() throws Exception {
      log("=== 3 caches with transaction (no concurrent access) ===");
      cache1_=createCache(IsolationLevel.REPEATABLE_READ);
      cache2_=createCache(IsolationLevel.REPEATABLE_READ);
      cache3_=createCache(IsolationLevel.REPEATABLE_READ);
      cache1_.startService();
      cache2_.startService();
      cache3_.startService();

      // Formating
      DecimalFormat form=new DecimalFormat("#.00");
      FieldPosition fieldPos=new FieldPosition(0);
      StringBuffer dumbStr=new StringBuffer();
      boolean hasTx=true;
      boolean oneTxOnly=false;

      // Step 1. Add entries to the cache
      long time1=System.currentTimeMillis();
      int nOps=add(cache1_, hasTx, oneTxOnly);
      long time2=System.currentTimeMillis();
      double d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _add is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 2. Query the cache
      time1=System.currentTimeMillis();
      nOps=get(cache2_, hasTx, oneTxOnly);   // Note query is from cache2
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _get is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
      dumbStr=new StringBuffer();

      // Step 3. Remove entries from the cache
      time1=System.currentTimeMillis();
      nOps=remove(cache3_, hasTx, oneTxOnly);   // Note remove is from cache3
      time2=System.currentTimeMillis();
      d=(double)(time2 - time1) / nOps;
      log("Time elapsed for _remove is " + (time2 - time1) + " with " + nOps
          + " operations. Average per ops is: " + form.format(d, dumbStr, fieldPos) +
          " msec.");
   }


   private int add(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      try {
         return _add(cache, hasTx, oneTxOnly);
      }
      catch(Throwable t) {
         t.printStackTrace();
         System.out.println("cache locks: " + cache.printLockInfo());
         return 0;
         // throw (Exception)t;
      }
   }

   private int _add(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      if(hasTx && oneTxOnly) {
         tx=startTransaction();
      }

      for(int i=0; i < nodeList_.size(); i++) {
         String key=Integer.toString(i);
         String value=Integer.toString(i);
         if(hasTx && !oneTxOnly) {
            tx=startTransaction();
            cache.put((String)nodeList_.get(i), key, value);
            tx.commit();
         }
         else {
            cache.put((String)nodeList_.get(i), key, value);
         }
      }

      if(hasTx && oneTxOnly) {
         tx.commit();
      }

      return nodeList_.size();
   }

   private int get(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      try {
         return _get(cache, hasTx, oneTxOnly);
      }
      catch(Throwable t) {
         t.printStackTrace();
         System.out.println("cache locks: " + cache.printLockInfo());
         throw (Exception)t;
      }
   }

   private int _get(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      if(hasTx && oneTxOnly) {
         tx=startTransaction();
      }

      for(int i=0; i < nodeList_.size(); i++) {
         String key=Integer.toString(i);
         if(hasTx && !oneTxOnly) {
            tx=startTransaction();
            cache.get((String)nodeList_.get(i), key);
            tx.commit();
         }
         else {
            cache.get((String)nodeList_.get(i), key);
         }
      }

      if(hasTx && oneTxOnly) {
         tx.commit();
      }

      return nodeList_.size();
   }


   private int remove(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      try {
         return _remove(cache, hasTx, oneTxOnly);
      }
      catch(Throwable t) {
         t.printStackTrace();
         System.out.println("cache locks: " + cache.printLockInfo());
         throw (Exception)t;
      }
   }

   private int _remove(TreeCache cache, boolean hasTx, boolean oneTxOnly) throws Exception {
      if(hasTx && oneTxOnly) {
         tx=startTransaction();
      }

      for(int i=0; i < nodeList_.size(); i++) {
         String key=Integer.toString(i);
         if(hasTx && !oneTxOnly) {
            tx=startTransaction();
            cache.remove((String)nodeList_.get(i), key);
            tx.commit();
         }
         else {
            cache.remove((String)nodeList_.get(i), key);
         }
      }

      if(hasTx && oneTxOnly) {
         tx.commit();
      }

      return nodeList_.size();
   }

   /**
    * Generate the tree nodes quasi-exponentially. I.e., depth is the level
    * of the hierarchy and children is the number of children under each node.
    */
   private ArrayList nodeGen(int depth, int children) {
      ArrayList strList=new ArrayList();
      ArrayList oldList=new ArrayList();
      ArrayList newList=new ArrayList();

      oldList.add("/");
      newList.add("/");
      strList.add("/");

      while(depth > 0) {
         // Trying to produce node name at this depth.
         newList=new ArrayList();
         for(int i=0; i < oldList.size(); i++) {
            for(int j=0; j < children; j++) {
               String tmp=(String)oldList.get(i);
               tmp+=Integer.toString(j);
               if(depth != 1) tmp+="/";
               newList.add(tmp);
            }
         }
         strList.addAll(newList);
         oldList=newList;
         depth--;
      }

      log("Nodes generated: " + strList.size());
      return strList;
   }

   public static Test suite() throws Exception {
      return new TestSuite(ReplicatedSyncPerfTest.class);
   }

   private void log(String str) {
//     System.out.println(this.getClass().getName() +": " +str);
      System.out.println(str);
   }

}
