001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package org.apache.commons.performance;
019
020 import java.util.logging.Logger;
021
022 import org.apache.commons.math.random.RandomData;
023 import org.apache.commons.math.random.RandomDataImpl;
024 import org.apache.commons.math.stat.descriptive.SummaryStatistics;
025
026 /**
027 * <p>Base for performance / load test clients.
028 * The run method executes init, then setup-execute-cleanup in a loop,
029 * gathering performance statistics, with time between executions based
030 * on configuration parameters. The <code>finish</code> method is executed once
031 * at the end of a run. See {@link #nextDelay()} for details on
032 * inter-arrival time computation.</p>
033 *
034 * <p>Subclasses <strong>must</strong> implement <code>execute</code>, which
035 * is the basic client request action that is executed, and timed,
036 * repeatedly. If per-request setup is required, and you do not want the time
037 * associated with this setup to be included in the reported timings, implement
038 * <code>setUp</code> and put the setup code there. Similarly for
039 * <code>cleanUp</code>. Initialization code that needs to be executed once
040 * only, before any requests are initiated, should be put into
041 * <code>init</code> and cleanup code that needs to be executed only once
042 * at the end of a simulation should be put into <code>finish.</code></p>
043 *
044 * <p>By default, the only statistics accumulated are for the latency of the
045 * <code>execute</code> method. Additional metrics can be captured and added
046 * to the {@link Statistics} for the running thread.</p>
047 *
048 */
049 public abstract class ClientThread implements Runnable {
050
051 // Inter-arrival time configuration parameters
052 /** Minimum mean time between requests */
053 private long minDelay;
054 /** Maximum mean time between requests */
055 private long maxDelay;
056 /** Standard deviation of delay distribution */
057 private double sigma;
058 /** Delay type - determines how next start times are computed */
059 private String delayType;
060 /** Ramp length for cyclic mean delay */
061 private long rampPeriod;
062 /** Peak length for cyclic mean delay */
063 private long peakPeriod;
064 /** Trough length for cyclic mean delay */
065 private long troughPeriod;
066 /** Cycle type */
067 private final String cycleType;
068 /** Ramp type */
069 private String rampType;
070
071 /** Number of iterations */
072 private final long iterations;
073
074 // State data
075 /** Start time of run */
076 private long startTime;
077 /** Start time of current period */
078 private long periodStart;
079 /** Last mean delay */
080 private double lastMean;
081 /** Cycle state constants */
082 protected static final int RAMPING_UP = 0;
083 protected static final int RAMPING_DOWN = 1;
084 protected static final int PEAK_LOAD = 2;
085 protected static final int TROUGH_LOAD = 3;
086 /** Cycle state */
087 private int cycleState = RAMPING_UP;
088 /** Number of errors */
089 private long numErrors = 0;
090 /** Number of misses */
091 private long numMisses = 0;
092
093 /** Random data generator */
094 protected RandomData randomData = new RandomDataImpl();
095 /** Statistics container */
096 protected Statistics stats;
097 /** Logger shared by client threads */
098 protected Logger logger;
099
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 }