/*---------------------------------------------------------------------*\
 *
 * aicas GmbH, Karlsruhe
 *
 * $Source: /CVSROOT/aicas/www/download/Jitter.java,v $
 * $Revision: 1.4 $
 * $Author: borutta $
 * Contents: Java source for Jitter analysis
 *
\*---------------------------------------------------------------------*/

import javax.realtime.RealtimeThread;
import javax.realtime.PeriodicParameters;
import javax.realtime.RelativeTime;
import javax.realtime.AbsoluteTime;
import javax.realtime.Clock;

/**
 * Analyses the jitter of periodic RealtimeThreads
 *
 * @author Fridtjof Siebert (siebert@aicas.com)
 */
public class Jitter {
  // --------------------------- constants --------------------------------
  
  /** 
   * parameter to control amount of allocation performed by load thread
   */
  static final int LOAD_THREAD_ALLOC_SIZE = 3;
  
  /**
   *  start period in ns: 0.5sec
   */
  static final long START_PERIOD = 500000000L;

  /**
   * number of iterations for start period
   */
  static final long START_ITERATIONS = 3;

  /**
   * expected time required for one iteration
   */
  static final long DURATION_OF_ONE_ITERATION = START_PERIOD * START_ITERATIONS;

  public static final String NEWLINE = System.getProperty("line.separator", "\n");
  
  /**
   * factors to reduce period length. Period for iteration i is 
   *
   * periodFactors[i % periodFactors.length] / 100 / (10 ** i / periodFactors.length)
   *
   * (see getPeriod()). 
   */
  static final int[] periodFactors = { 100, 80, 60, 40, 30, 20, 15 }; 

  static final Clock clock = Clock.getRealtimeClock();

  // --------------------------- variables --------------------------------
  
  /**
   * numer of current test run, used to stop if test is hanging for too long
   */
  static int       iteration = 0;
  
  /**
   * true to output elapsed time in each period (can also be set by command line option -verbose)
   */
  static boolean   verbose = false;

  /**
   * true to deactivate background thread (can also be set by command line option -noLoadThread)
   */
  static boolean   noLoadThread = false;

  /**
   * smallest period with acceptable jitter, in nanos
   */
  static long      min_period = 0;

  // ------------------------ native functions ----------------------------

  // ---------------------------- methods ---------------------------------

  /**
   * start a low-priority thread that performs memory allocations of
   * small and large objects.
   *
   * @return the newly created and running thread. 
   */
  private static Thread startLoadThread() {
    Thread result; 

    result = new Thread("Low Priority Load Thread") {
	public void run() {
	  do {
	    try {
	      alloc1();
	      test=null;
	      alloc2();
	      
	      /* this yield is only needed if we the unterlying OS
	       * is not an RTOS (i.e., it does not respect thread
	       * priorities) and the sync thread is disabled. In
	       * this case, the loadThread may run even though its
	       * priority is lower than the main thread and it will
	       * never be woken up.
	       */
	      Thread.yield(); 
	      
	    } catch (Exception e) {
	      e.printStackTrace();
	      System.exit(1);
	    }
	  } while (iteration >= 0);
	}
	volatile Object[] test = new Object[128]; 
	volatile Object[] qu_head = new Object[3];
	volatile Object[] qu_tail = qu_head;
	int qu_len = 0;
	void alloc1() {
	  // allocate many small objects. 
	  // keep some (every 512th) in a queue to force compaction.
	  test = new Object[128]; 
	  for(int i=0; i<128; i++) {
	    for(int j=0; j<LOAD_THREAD_ALLOC_SIZE*117; j++) {
	      Object[] a = new Object[3];
	      if ((j&0x1ff) == 0) {
		a[0] = qu_head;
		a[1] = null;
		qu_head[1] = a;
		qu_head = a;
		if (qu_len < LOAD_THREAD_ALLOC_SIZE*16*3) {
		  qu_len++;
		} else {
		  qu_tail = (Object[]) qu_tail[1];
		  qu_tail[0] = null;
		}
	      }  else {
		a[0] = test[i];
		test[i] = a;
	      }
	    }
	  }
	}
	void alloc2() {
	  // allocate few very large objects.
	  test = new Object[LOAD_THREAD_ALLOC_SIZE*128*1024/2];
	}
      };
    result.setDaemon(true); 
    result.setPriority(1); 

    System.out.println("Start load thread...");
    result.start(); 

    return result; 
  }


  /**
   * start a normal-priority thread that will terminate the
   * application if one iteration runs longer that 3 times the
   * expected time.
   *
   * @return the newly created and running thread. 
   */
  private static Thread startTimeoutThread() {
    Thread result; 

    result = new Thread("Timeout Detection Thread") {
	public void run() {
	  int last_iteration;
	  do {
	    last_iteration = iteration; 
	    synchronized (this) {
	      try {this.wait(3*DURATION_OF_ONE_ITERATION/1000000); } catch (Throwable t) { }
	    }
	    if (iteration < 0) {
	      return;
	    }
	  } while (iteration != last_iteration);
	  System.out.println("***** timeout!");
	  System.exit(1);
	}
      };
    result.setDaemon(true); 
    result.start(); 
    return result; 
  }


  /**
   * determine the period le length for given iteration
   *
   * @param iteration the iteration, 0 for first, 1 for second, etc.
   *
   * @return the period in nsec.
   */
  private static long getPeriod(int iteration) {
    long result; 

    result = START_PERIOD * periodFactors[iteration%periodFactors.length]/100;
    while (iteration >= periodFactors.length) {
      result = result / 10; 
      iteration = iteration - periodFactors.length;
    }
    return result;
  }


  /**
   * measure jitter for given iteration
   *
   * @param iteration the iteration, 0 for first, 1 for second, etc.
   *
   * @return true iff there was no overrun and jitter was unter 50%
   * for this iteration.
   */
  private static boolean testPeriod(int iteration) {
    final boolean[] result     = new boolean[1];
    final long      period     = getPeriod(iteration);
    final int       iterations = (int) (START_PERIOD * START_ITERATIONS / period); /* number of iterations */
    
    final RelativeTime periodTime = new RelativeTime((long) (period / 1000000) /* ms */,
						     (int ) (period % 1000000) /* ns */);
    
    /* allow 3 periods of delay before we start the thread: */
    RelativeTime start  = new RelativeTime((long) (3*period / 1000000) /* ms */, 
					   (int ) (3*period % 1000000) /* ns */);
    
    PeriodicParameters periodicParameters = new PeriodicParameters(start,periodTime, null,null,null,null);
    
    /* create periodic thread: */
    RealtimeThread realtimeThread = new RealtimeThread(null,periodicParameters) {
	public void run()
	{
	  /* variables to determine min, max, average and sigma: */
	  long min = 0x7fffFFFF; 
	  long max = 0; 
	  long sum = 0; 
	  long squares = 0; 
	  AbsoluteTime last = null;/* time of last iteration */
	  int n=0;
          while ((n<=iterations) && waitForNextPeriod()) {
	    AbsoluteTime time = clock.getTime();  /* current time */
	    if (n>0) {
	      RelativeTime elaps = time.subtract(last); 
	      long elapsed = 
		((elaps.getMilliseconds())*1000000)+
		((elaps.getNanoseconds ())        ); 
	      if (elapsed > max) { max = elapsed; }
	      if (elapsed < min) { min = elapsed; }
	      RelativeTime diff = elaps.subtract(periodTime);
	      long delta = 
		((diff.getMilliseconds())*1000000)+
		((diff.getNanoseconds ())        ); 
	      if (verbose) {
		System.out.println("Period "+n+" time elapsed: "+d2s(elapsed));
	      }
	      sum = sum + elapsed; 
	      squares = squares + delta*delta;
	    }
	    last = time;
	    n++;
          }
	  if (n<=iterations) {
	    System.out.println("Overrun for period "+d2s(period)+" after "+n+" iterations!"); 
	    result[0] = true; 
	  } else {
	    long sigma = (long) Math.sqrt(squares/iterations);
	    System.out.println("Period " + d2s(period) +":"+
			       " min="+d2s(min)+" ("+pc(min,period)+")"+
			       " max="+d2s(max)+" ("+pc(max,period)+")"+
			       " average="+d2s(sum/iterations)+
			       " sigma="+d2s(sigma)+" ("+pc(sigma,period)+")"+
			       " ("+iterations+" iterations)");
	    if (sigma >= period/2) {
	      result[0] = true; 
	    } else {
	      min_period = period;
	    }
	  }
	}
      };
    
    /* start periodic thread: */
    realtimeThread.start();
    try { realtimeThread.join(); } catch (Throwable t) { }
    return result[0];
  }


  /**
   * d2s converts a time given in nanos to a human readable string
   *
   * @param d a time in nanos
   *
   * @return a string of the form "XXX.XXxs", e.g., " 3.34s ",
   * "123.45us", etc.
   */
  static String d2s(long d) {
    String Result, u, s;

    s = " ";
    if (d<0) {
      s = "-";
      d = -d; 
    }
    if        (d<      1000) { d = d * 100;      u = "ns";
    } else if (d<   1000000) { d = d / 10;       u = "us";
    } else if (d<1000000000) { d = d / 10000;    u = "ms";
    } else                   { d = d / 10000000; u = "s ";
    }
    Result = 
      s+
      ((d<10000) ? " " : ""+(d/10000 % 10))+
      ((d<1000 ) ? " " : ""+(d/1000  % 10))+
      (d/100   % 10)+"."+
      (d/10    % 10)+
      (d       % 10)+
      u;
    return Result;
  }


  /**
   * pc returns a percentage string of d/e
   *
   * @param d dividend
   *
   * @param e divisor
   *
   * @return the percentage, e.g., "  1%", " 12%", "123%".
   */
  static String pc(long d, long e) {
    d = (1000*d/e+5)/10;
    if (d<10) {
      return "  "+d+"%";
    } else if (d<100) {
      return " "+d+"%";
    } else {
      return ""+d+"%";
    }
  }

  /**
   * main Jitter
   *
   * @param args command line arguments
   */
  public static void main(String[] args)
  {
    iteration = 0; 

    /* parse arguments */
    for (int i=0; i<args.length; i++) {
      if (args[i].equals("-verbose")) {
	verbose = true; 
      } else if (args[i].equals("-noLoadThread")) {
	noLoadThread = true; 
      } else {
        System.err.println("Unknown argument '"+args[i]+"'!");
	System.out.println("Usage: Jitter [-verbose] [-noLoadThread]"); 
        System.exit(5);
      }
    }

    RelativeTime res_t = clock.getResolution();
    long res = 
      ((res_t.getMilliseconds())*1000000)+
      ((res_t.getNanoseconds ())        ); 

    /* load thread runs without pause at low priority and continuously
     * allocates memory of different sizes.
     */
    Thread loadThread = noLoadThread
      ? null
      : startLoadThread(); 

    /* exit thread will kill this test if we hang for too long */
    Thread exit = startTimeoutThread(); 

    System.out.println("Jitter test: "+System.getProperty("java.vm.name")+" "+
		       System.getProperty("java.vm.version")+
		       " time resolution "+d2s(res)+
		       " will take about "+d2s(DURATION_OF_ONE_ITERATION)+" per iteration."); 

    while (!testPeriod(iteration)) {
      iteration++;
      synchronized (exit) { exit.notify(); }
    }

    /* terminate timeout detection thread: */
    iteration = -1;
    synchronized (exit) { exit.notify(); }
    try { exit.join(); } catch (Throwable t) { }
    if (loadThread!=null) {
      try { loadThread.join(); } catch (Throwable t) { }
    }

    System.out.println("Jitter analysis complete. Average jitter below 50% for periods "+d2s(min_period)+" and higher.");
    System.out.println(Jitter.NEWLINE +
		       "Values displayed are:" + Jitter.NEWLINE +
		       "Period    : The desired period for a periodic thread" + Jitter.NEWLINE + 
		       "min       : minimum actual period measured, percentage of desired period" + Jitter.NEWLINE +
		       "max       : maximum actual period measured, percentage of desired period" + Jitter.NEWLINE + 
		       "average   : average measured duration of one period" + Jitter.NEWLINE +
		       "sigma     : std.-derivation of measured periods, percentage of desired period" + Jitter.NEWLINE +
		       "iterations: number of iterations measured" + Jitter.NEWLINE); 
  }
}
