View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.performance;
19  
20  import java.util.logging.Logger;
21  
22  import org.apache.commons.math.random.RandomData;
23  import org.apache.commons.math.random.RandomDataImpl;
24  import org.apache.commons.math.stat.descriptive.SummaryStatistics;
25  
26  /**
27   * <p>Base for performance / load test clients. 
28   * The run method executes init, then setup-execute-cleanup in a loop,
29   * gathering performance statistics, with time between executions based
30   * on configuration parameters. The <code>finish</code> method is executed once
31   * at the end of a run. See {@link #nextDelay()} for details on
32   * inter-arrival time computation.</p>
33   * 
34   * <p>Subclasses <strong>must</strong> implement <code>execute</code>, which
35   * is the basic client request action that is executed, and timed, 
36   * repeatedly. If per-request setup is required, and you do not want the time
37   * associated with this setup to be included in the reported timings, implement 
38   * <code>setUp</code> and put the setup code there.  Similarly for 
39   * <code>cleanUp</code>. Initialization code that needs to be executed once
40   * only, before any requests are initiated, should be put into 
41   * <code>init</code> and cleanup code that needs to be executed only once
42   * at the end of a simulation should be put into <code>finish.</code></p>
43   * 
44   * <p>By default, the only statistics accumulated are for the latency of the 
45   * <code>execute</code> method. Additional metrics can be captured and added
46   * to the {@link Statistics} for the running thread.</p>
47   * 
48   */
49  public abstract class ClientThread implements Runnable {
50  
51      // Inter-arrival time configuration parameters 
52      /** Minimum mean time between requests */
53      private long minDelay;
54      /** Maximum mean time between requests */
55      private long maxDelay;
56      /** Standard deviation of delay distribution */
57      private double sigma;
58      /** Delay type - determines how next start times are computed */
59      private String delayType;
60      /** Ramp length for cyclic mean delay */
61      private long rampPeriod;
62      /** Peak length for cyclic mean delay */
63      private long peakPeriod;
64      /** Trough length for cyclic mean delay */
65      private long troughPeriod;
66      /** Cycle type */
67      private final String cycleType;
68      /** Ramp type */
69      private String rampType;
70      
71      /** Number of iterations */
72      private final long iterations;
73      
74      // State data
75      /** Start time of run */
76      private long startTime;
77      /** Start time of current period */
78      private long periodStart;
79      /** Last mean delay */
80      private double lastMean;
81      /** Cycle state constants */
82      protected static final int RAMPING_UP = 0;
83      protected static final int RAMPING_DOWN = 1;
84      protected static final int PEAK_LOAD = 2;
85      protected static final int TROUGH_LOAD = 3;
86      /** Cycle state */
87      private int cycleState = RAMPING_UP;
88      /** Number of errors */
89      private long numErrors = 0;
90      /** Number of misses */
91      private long numMisses = 0;
92      
93      /** Random data generator */
94      protected RandomData randomData = new RandomDataImpl();
95      /** Statistics container */
96      protected Statistics stats;
97      /** Logger shared by client threads */
98      protected Logger logger;
99      
100     /**
101      * Create a client thread.
102      * 
103      * @param iterations number of iterations
104      * @param minDelay minimum mean time between client requests
105      * @param maxDelay maximum mean time between client requests
106      * @param sigma standard deviation of time between client requests
107      * @param delayType distribution of time between client requests
108      * @param rampPeriod ramp period of cycle for cyclic load
109      * @param peakPeriod peak period of cycle for cyclic load
110      * @param troughPeriod trough period of cycle for cyclic load
111      * @param cycleType type of cycle for mean delay
112      * @param rampType type of ramp (linear or random jumps)
113      * @param logger common logger shared by all clients
114      * @param stats Statistics instance to add results to
115      */
116     public ClientThread(long iterations, long minDelay, long maxDelay,
117             double sigma, String delayType, long rampPeriod, long peakPeriod,
118             long troughPeriod, String cycleType,
119             String rampType, Logger logger,
120             Statistics stats) {
121         this.iterations = iterations;
122         this.minDelay = minDelay;
123         this.maxDelay = maxDelay;
124         this.sigma = sigma;
125         this.delayType = delayType;
126         this.peakPeriod = peakPeriod;
127         this.rampPeriod = rampPeriod;
128         this.troughPeriod = troughPeriod;
129         this.cycleType = cycleType;
130         this.rampType = rampType;
131         this.logger = logger;
132         this.stats = stats;
133     }
134     
135     public void run() {
136         try {
137             init();
138         } catch (Exception ex) {
139             logger.severe("init failed.");
140             ex.printStackTrace();
141             return;
142         }
143         long start = 0;
144         startTime = System.currentTimeMillis();
145         long lastStart = startTime;
146         periodStart = System.currentTimeMillis();
147         lastMean = (double) maxDelay; // Ramp up, if any, starts here
148         SummaryStatistics responseStats = new SummaryStatistics();
149         for (int i = 0; i < iterations; i++) {
150             try {
151                 setUp();
152                 // Generate next interarrival time. If that is in the
153                 // past, go right away and log a miss; otherwise wait.
154                 long elapsed = System.currentTimeMillis() - lastStart;
155                 long nextDelay = nextDelay();
156                 if (elapsed > nextDelay) {
157                     numMisses++;
158                 } else {
159                     try {
160                         Thread.sleep(nextDelay - elapsed);
161                     } catch (InterruptedException ex) {
162                         logger.info("Sleep interrupted");
163                     }
164                 }
165                 
166                 // Fire the request and measure response time
167                 start = System.currentTimeMillis();
168                 execute();
169             } catch (Exception ex) {
170                 ex.printStackTrace();
171                 numErrors++;
172             } finally {
173                 try {
174                     responseStats.addValue(System.currentTimeMillis() - start);
175                     lastStart = start;
176                     cleanUp();
177                 } catch (Exception e) {
178                     e.printStackTrace();                    
179                 }
180             }
181         }
182         
183         try {
184             finish();
185         } catch (Exception ex) {
186             logger.severe("finalize failed.");
187             ex.printStackTrace();
188             return;
189         }
190         
191         // Use thread name as process name
192         String process = Thread.currentThread().getName();
193         
194         // Record latency statistics
195         stats.addStatistics(responseStats, process, "latency");
196         
197         // Log accumulated statistics for this thread
198         logger.info(stats.displayProcessStatistics(process) + 
199           "Number of misses: " + numMisses + "\n" +
200           "Number or errors: " + numErrors + "\n"); 
201     }
202     
203     /** Executed once at the beginning of the run */
204     protected void init() throws Exception {}
205     
206     /** Executed at the beginning of each iteration */
207     protected void setUp() throws Exception {}
208     
209     /** Executed in finally block of iteration try-catch */
210     protected void cleanUp() throws Exception {}
211     
212     /** Executed once after the run finishes */
213     protected void finish() throws Exception {}
214     
215     /** 
216      * Core iteration code.  Timings are based on this,
217      *  so keep it tight.
218      */
219     public abstract void execute() throws Exception;
220     
221     /**
222      * <p>Computes the next interarrival time (time to wait between requests)
223      * based on configured values for min/max delay, delay type, cycle type, 
224      * ramp type and period. Currently supports constant (always returning
225      * <code>minDelay</code> delay time), Poisson and Gaussian distributed
226      * random time delays, linear and random ramps, and oscillating / 
227      * non-oscillating cycle types.</p>
228      * 
229      * <p><strong>loadType</strong> determines whether returned times are
230      * deterministic or random. If <code>loadType</code> is not "constant",
231      * a random value with the specified distribution and mean determined by
232      * the other parameters is returned. For "gaussian" <code>loadType</code>,
233      * <code>sigma</code> is used as used as the standard deviation. </p>
234      * 
235      * <p><strong>cycleType</strong> determines how the returned times vary
236      * over time. "oscillating", means times ramp up and down between 
237      * <code>minDelay</code> and <code>maxDelay.</code> Ramp type is controlled
238      * by <code>rampType.</code>  Linear <code>rampType</code> means the means
239      * increase or decrease linearly over the time of the period.  Random
240      * makes random jumps up or down toward the next peak or trough. "None" for
241      * <code>rampType</code> under oscillating <code>cycleType</code> makes the
242      * means alternate between peak (<code>minDelay</code>) and trough 
243      *(<code>maxDelay</code>) with no ramp between. </p>
244      * 
245      * <p>Oscillating loads cycle through RAMPING_UP, PEAK_LOAD, RAMPING_DOWN
246      * and TROUGH_LOAD states, with the amount of time spent in each state
247      * determined by <code>rampPeriod</code> (time spent increasing on the way
248      * up and decreasing on the way down), <code>peakPeriod</code> (time spent
249      * at peak load, i.e., <code>minDelay</code> mean delay) and 
250      * <code>troughPeriod</code> (time spent at minimum load, i.e., 
251      * <code>maxDelay</code> mean delay). All times are specified in
252      * milliseconds. </p>
253      * 
254      * <p><strong>Examples:</strong><ol>
255      * 
256      * <li>Given<pre>
257      * delayType = "constant"
258      * minDelay = 250
259      * maxDelay = 500
260      * cycleType = "oscillating"
261      * rampType = "linear" 
262      * rampPeriod = 10000
263      * peakPeriod = 20000
264      * troughPeriod = 30000</pre> load will start at one request every 500 ms,
265      * which is "trough load." Load then ramps up linearly over the next 10
266      * seconds unil it reaches one request per 250 milliseconds, which is 
267      * "peak load."  Peak load is sustained for 20 seconds and then load ramps
268      * back down, again taking 10 seconds to get down to "trough load," which
269      * is sustained for 30 seconds.  The cycle then repeats.</li>
270      * 
271      * <li><pre>
272      * delayType = "gaussian"
273      * minDelay = 250
274      * maxDelay = 500
275      * cycleType = "oscillating"
276      * rampType = "linear" 
277      * rampPeriod = 10000
278      * peakPeriod = 20000
279      * troughPeriod = 30000
280      * sigma = 100 </pre> produces a load pattern similar to example 1, but in
281      * this case the computed delay value is fed into a gaussian random number
282      * generator as the mean and 100 as the standard deviation - i.e., 
283      * <code>nextDelay</code> returns random, gaussian distributed values with
284      * means moving according to the cyclic pattern in example 1.</li>
285      * 
286      * <li><pre>
287      * delayType = "constant"
288      * minDelay = 250
289      * maxDelay = 500
290      * cycleType = "none"
291      * rampType = "linear" 
292      * rampPeriod = 10000</pre> produces a load pattern that increases linearly
293      * from one request every 500ms to one request every 250ms and then stays
294      * constant at that level until the run is over.  Other parameters are
295      * ignored in this case.</li>
296      * 
297      * <li><pre>
298      * delayType = "poisson"
299      * minDelay = 250
300      * maxDelay = 500
301      * cycleType = "none"
302      * rampType = "none" 
303      * </pre> produces inter-arrival times that are poisson distributed with
304      * mean 250ms. Note that when rampType is "none," the value of 
305      * <code>minDelay</code> is used as the (constant) mean delay.</li></ol>
306      * 
307      * @return next value for delay
308      */
309     protected long nextDelay() throws ConfigurationException {
310         double targetDelay = 0; 
311         double dMinDelay = (double) minDelay;
312         double dMaxDelay = (double) maxDelay;
313         double delayDifference = dMaxDelay - dMinDelay;
314         long currentTime = System.currentTimeMillis();
315         if (cycleType.equals("none")) {
316             if (rampType.equals("none") || 
317                     (currentTime - startTime) > rampPeriod) { // ramped up
318                 targetDelay = dMinDelay;
319             } else if (rampType.equals("linear")) { // single period linear
320                 double prop = 
321                     (double) (currentTime - startTime) / (double) rampPeriod;
322                 targetDelay =  dMaxDelay - delayDifference * prop;
323             } else { // Random jumps down to delay - single period
324                 // TODO: govern size of jumps as in oscillating
325                 // Where we last were as proportion of way down to minDelay
326                 double lastProp = 
327                     (dMaxDelay - lastMean) / delayDifference;
328                 // Make a random jump toward 1 (1 = all the way down)
329                 double prop = randomData.nextUniform(lastProp, 1);
330                 targetDelay = dMaxDelay - delayDifference * prop;
331             }
332         } else if (cycleType.equals("oscillating")) {
333             // First change cycle state if we need to
334             adjustState(currentTime);
335             targetDelay = computeCyclicDelay(
336                     currentTime, dMinDelay, dMaxDelay);
337         } else {
338             throw new ConfigurationException(
339                     "Cycle type not supported: " + cycleType);
340         }
341 
342         // Remember last mean for ramp up / down
343         lastMean = targetDelay;
344 
345         if (delayType.equals("constant")) { 
346             return Math.round(targetDelay);
347         }
348 
349         // Generate and return random deviate
350         if (delayType.equals("gaussian")) {
351             return Math.round(randomData.nextGaussian(targetDelay, sigma));
352         } else { // must be Poisson
353             return randomData.nextPoisson(targetDelay);
354         } 
355     }
356     
357     /**
358      * Adjusts cycleState, periodStart and lastMean if a cycle state
359      * transition needs to happen.
360      * 
361      * @param currentTime current time
362      */
363     protected void adjustState(long currentTime) {
364         long timeInPeriod = currentTime - periodStart;
365         if ( ((cycleState == RAMPING_UP || cycleState == RAMPING_DOWN) && 
366                 timeInPeriod < rampPeriod) ||
367              (cycleState == PEAK_LOAD && timeInPeriod < peakPeriod) ||
368              (cycleState == TROUGH_LOAD && timeInPeriod < troughPeriod)) {
369             return; // No state change
370         }
371         switch (cycleState) {
372             case RAMPING_UP: 
373                 if (peakPeriod > 0) {
374                     cycleState = PEAK_LOAD;
375                 } else {
376                     cycleState = RAMPING_DOWN;
377                 }
378                 lastMean = (double) minDelay;
379                 periodStart = currentTime;
380                 break;
381             
382             case RAMPING_DOWN: 
383                 if (troughPeriod > 0) {
384                     cycleState = TROUGH_LOAD;
385                 } else {
386                     cycleState = RAMPING_UP;
387                 }
388                 lastMean = (double) maxDelay;
389                 periodStart = currentTime;
390                 break;
391             
392             case PEAK_LOAD: 
393                 if (rampPeriod > 0) {
394                     cycleState = RAMPING_DOWN;
395                     lastMean = (double) minDelay;
396                 } else {
397                     cycleState = TROUGH_LOAD;
398                     lastMean = (double) maxDelay;
399                 }
400                 periodStart = currentTime;
401                 break;
402             
403             case TROUGH_LOAD: 
404                 if (rampPeriod > 0) {
405                     cycleState = RAMPING_UP;
406                     lastMean = (double) maxDelay;
407                 } else {
408                     cycleState = PEAK_LOAD;
409                     lastMean = (double) minDelay;
410                 }
411                 periodStart = currentTime;
412                 break;
413             
414             default: 
415                 throw new IllegalStateException(
416                         "Illegal cycle state: " + cycleState);
417         }
418     }
419     
420     protected double computeCyclicDelay(
421             long currentTime, double min, double max) {
422         
423         // Constant load states
424         if (cycleState == PEAK_LOAD) { 
425             return min;
426         } 
427         if (cycleState == TROUGH_LOAD) {
428             return max;
429         } 
430 
431         // No ramp - stay at min or max load during ramp
432         if (rampType.equals("none")) { // min or max, no ramp
433             if (cycleState == RAMPING_UP) {
434                 return max;
435             } else {
436                 return min;
437             }
438         } 
439 
440         // Linear ramp type and ramping up or down
441         double diff = max - min;
442         if (rampType.equals("linear")) {
443             double prop = 
444                 (double)(currentTime - periodStart) / (double) rampPeriod;
445             if (cycleState == RAMPING_UP) {
446                 return max - diff * prop; 
447             } else {
448                 return min + diff * prop;
449             }
450         } else { // random jumps down, then back up
451             // Where we last were as proportion of way down to minDelay
452             double lastProp = 
453                 (max - lastMean) / diff;
454             // Where we would be if this were a linear ramp
455             double linearProp = 
456                 (double)(currentTime - periodStart) / (double) rampPeriod;
457             // Need to govern size of jumps, otherwise "convergence"
458             // can be too fast - use linear ramp as governor
459             if ((cycleState == RAMPING_UP && (lastProp > linearProp)) || 
460                     (cycleState == RAMPING_DOWN && 
461                             ((1 - lastProp) > linearProp))) 
462                 lastProp = (cycleState == RAMPING_UP) ? linearProp : 
463                     (1 - linearProp);
464             double prop = 0;
465             if (cycleState == RAMPING_UP) { // Random jump toward 1
466                 prop = randomData.nextUniform(lastProp, 1);
467             } else { // Random jump toward 0
468                 prop = randomData.nextUniform(0, lastProp);
469             }
470             // Make sure sequence is monotone
471             if (cycleState == RAMPING_UP) {
472                 return Math.min(lastMean, max - diff * prop);
473             } else {
474                 return Math.max(lastMean, min + diff * prop);
475             }
476         }
477     }
478 
479     public long getMinDelay() {
480         return minDelay;
481     }
482 
483     public long getMaxDelay() {
484         return maxDelay;
485     }
486 
487     public double getSigma() {
488         return sigma;
489     }
490 
491     public String getDelayType() {
492         return delayType;
493     }
494 
495     public long getRampPeriod() {
496         return rampPeriod;
497     }
498 
499     public long getPeakPeriod() {
500         return peakPeriod;
501     }
502 
503     public long getTroughPeriod() {
504         return troughPeriod;
505     }
506 
507     public String getCycleType() {
508         return cycleType;
509     }
510 
511     public String getRampType() {
512         return rampType;
513     }
514 
515     public long getIterations() {
516         return iterations;
517     }
518 
519     public long getStartTime() {
520         return startTime;
521     }
522 
523     public long getPeriodStart() {
524         return periodStart;
525     }
526 
527     public double getLastMean() {
528         return lastMean;
529     }
530 
531     public int getCycleState() {
532         return cycleState;
533     }
534 
535     public long getNumErrors() {
536         return numErrors;
537     }
538 
539     public long getNumMisses() {
540         return numMisses;
541     }
542 
543     public Statistics getStats() {
544         return stats;
545     }
546 
547     public void setStartTime(long startTime) {
548         this.startTime = startTime;
549     }
550 
551     public void setPeriodStart(long periodStart) {
552         this.periodStart = periodStart;
553     }
554 
555     public void setLastMean(double lastMean) {
556         this.lastMean = lastMean;
557     }
558 
559     public void setCycleState(int cycleState) {
560         this.cycleState = cycleState;
561     }
562 
563     public void setNumErrors(long numErrors) {
564         this.numErrors = numErrors;
565     }
566 
567     public void setNumMisses(long numMisses) {
568         this.numMisses = numMisses;
569     }
570 
571     public void setRampType(String rampType) {
572         this.rampType = rampType;
573     }
574 
575     public void setMinDelay(long minDelay) {
576         this.minDelay = minDelay;
577     }
578 
579     public void setMaxDelay(long maxDelay) {
580         this.maxDelay = maxDelay;
581     }
582 
583     public void setSigma(double sigma) {
584         this.sigma = sigma;
585     }
586 
587     public void setDelayType(String delayType) {
588         this.delayType = delayType;
589     }
590 
591     public void setRampPeriod(long rampPeriod) {
592         this.rampPeriod = rampPeriod;
593     }
594 
595     public void setPeakPeriod(long peakPeriod) {
596         this.peakPeriod = peakPeriod;
597     }
598 
599     public void setTroughPeriod(long troughPeriod) {
600         this.troughPeriod = troughPeriod;
601     }
602     
603 }