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 protected long minDelay;
054 /** Maxiimum mean time between requests */
055 protected long maxDelay;
056 /** Standard deviation of delay distribution */
057 protected double sigma;
058 /** Delay type - determines how next start times are computed */
059 protected String delayType;
060 /** Ramp length for cyclic mean delay */
061 protected long rampPeriod;
062 /** Peak length for cyclic mean delay */
063 protected long peakPeriod;
064 /** Trough length for cyclic mean delay */
065 protected long troughPeriod;
066 /** Cycle type */
067 protected String cycleType;
068 /** Ramp type */
069 protected String rampType;
070
071 /** Number of iterations */
072 protected long iterations;
073
074 // State data
075 /** Start time of run */
076 protected long startTime;
077 /** Start time of current period */
078 protected long periodStart;
079 /** Last mean delay */
080 protected 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 protected int cycleState = RAMPING_UP;
088
089 /** Random data generator */
090 protected RandomData randomData = new RandomDataImpl();
091 /** Statistics container */
092 protected Statistics stats = new Statistics();
093 /** Logger shared by client threads */
094 protected Logger logger = null;
095
096 /**
097 * Create a client thread.
098 *
099 * @param iterations number of iterations
100 * @param minDelay minumum mean time between client requests
101 * @param maxDelay maximum mean time between client requests
102 * @param delayType distribution of time between client requests
103 * @param rampPeriod ramp period of cycle for cyclic load
104 * @param peakOeriod peak period of cycle for cyclic load
105 * @param troughPeriod trough period of cycle for cyclic load
106 * @param cycleType type of cycle for mean delay
107 * @param rampType type of ramp (linear or random jumps)
108 * @param logger common logger shared by all clients
109 * @param statsList List of SummaryStatistics to add results to
110 */
111 public ClientThread(long iterations, long minDelay, long maxDelay,
112 double sigma, String delayType, long rampPeriod, long peakPeriod,
113 long troughPeriod, String cycleType,
114 String rampType, Logger logger,
115 Statistics stats) {
116 this.iterations = iterations;
117 this.minDelay = minDelay;
118 this.maxDelay = maxDelay;
119 this.sigma = sigma;
120 this.delayType = delayType;
121 this.peakPeriod = peakPeriod;
122 this.rampPeriod = rampPeriod;
123 this.troughPeriod = troughPeriod;
124 this.cycleType = cycleType;
125 this.rampType = rampType;
126 this.logger = logger;
127 this.stats = stats;
128 }
129
130 public void run() {
131 try {
132 init();
133 } catch (Exception ex) {
134 logger.severe("init failed.");
135 ex.printStackTrace();
136 return;
137 }
138 long start = 0;
139 startTime = System.currentTimeMillis();
140 long lastStart = startTime;
141 long numMisses = 0;
142 long numErrors = 0;
143 periodStart = System.currentTimeMillis();
144 lastMean = (double) maxDelay; // Ramp up, if any, starts here
145 SummaryStatistics responseStats = new SummaryStatistics();
146 for (int i = 0; i < iterations; i++) {
147 try {
148 setUp();
149 // Generate next interarrival time. If that is in the
150 // past, go right away and log a miss; otherwise wait.
151 long elapsed = System.currentTimeMillis() - lastStart;
152 long nextDelay = nextDelay();
153 if (elapsed > nextDelay) {
154 numMisses++;
155 } else {
156 try {
157 Thread.sleep(nextDelay - elapsed);
158 } catch (InterruptedException ex) {
159 logger.info("Sleep interrupted");
160 }
161 }
162
163 // Fire the request and measure response time
164 start = System.currentTimeMillis();
165 execute();
166 } catch (Exception ex) {
167 ex.printStackTrace();
168 numErrors++;
169 } finally {
170 try {
171 responseStats.addValue(System.currentTimeMillis() - start);
172 lastStart = start;
173 cleanUp();
174 } catch (Exception e) {
175 e.printStackTrace();
176 }
177 }
178 }
179
180 try {
181 finish();
182 } catch (Exception ex) {
183 logger.severe("finalize failed.");
184 ex.printStackTrace();
185 return;
186 }
187
188 // Use thread name as process name
189 String process = Thread.currentThread().getName();
190
191 // Record latency statistics
192 stats.addStatistics(responseStats, process, "latency");
193
194 // Log accumulated statistics for this thread
195 logger.info(stats.displayProcessStatistics(process) +
196 "Number of misses: " + numMisses + "\n" +
197 "Number or errors: " + numErrors + "\n");
198 }
199
200 /** Executed once at the beginning of the run */
201 protected void init() throws Exception {}
202
203 /** Executed at the beginning of each iteration */
204 protected void setUp() throws Exception {}
205
206 /** Executed in finally block of iteration try-catch */
207 protected void cleanUp() throws Exception {}
208
209 /** Executed once after the run finishes */
210 protected void finish() throws Exception {}
211
212 /**
213 * Core iteration code. Timings are based on this,
214 * so keep it tight.
215 */
216 public abstract void execute() throws Exception;
217
218 /**
219 * <p>Computes the next interarrival time (time to wait between requests)
220 * based on configured values for min/max delay, delay type, cycle type,
221 * ramp type and period. Currently supports constant (always returning
222 * <code>minDelay</code> delay time), Poisson and Gaussian distributed
223 * random time delays, linear and random ramps, and oscillating /
224 * non-oscillating cycle types.</p>
225 *
226 * <p><strong>loadType</strong> determines whether returned times are
227 * deterministic or random. If <code>loadType</code> is not "constant",
228 * a random value with the specified distribution and mean determined by
229 * the other parameters is returned. For "gaussian" <code>loadType</code>,
230 * <code>sigma</code> is used as used as the standard deviation. </p>
231 *
232 * <p><strong>cycleType</strong> determines how the returned times vary
233 * over time. "oscillating", means times ramp up and down between
234 * <code>minDelay</code> and <code>maxDelay.</code> Ramp type is controlled
235 * by <code>rampType.</code> Linear <code>rampType</code> means the means
236 * increase or decrease linearly over the time of the period. Random
237 * makes random jumps up or down toward the next peak or trough. "None" for
238 * <code>rampType</code> under oscillating <code>cycleType</code> makes the
239 * means alternate between peak (<code>minDelay</code>) and trough
240 *(<code>maxDelay</code>) with no ramp between. </p>
241 *
242 * <p>Oscillating loads cycle through RAMPING_UP, PEAK_LOAD, RAMPING_DOWN
243 * and TROUGH_LOAD states, with the amount of time spent in each state
244 * determined by <code>rampPeriod</code> (time spent increasing on the way
245 * up and decreasing on the way down), <code>peakPeriod</code> (time spent
246 * at peak load, i.e., <code>minDelay</code> mean delay) and
247 * <code>troughPeriod</code> (time spent at minimum load, i.e.,
248 * <code>maxDelay</code> mean delay). All times are specified in
249 * milliseconds. </p>
250 *
251 * <p><strong>Examples:</strong><ol>
252 *
253 * <li>Given<pre>
254 * delayType = "constant"
255 * minDelay = 250
256 * maxDelay = 500
257 * cycleType = "oscillating"
258 * rampType = "linear"
259 * rampPeriod = 10000
260 * peakPeriod = 20000
261 * troughPeriod = 30000</pre> load will start at one request every 500 ms,
262 * which is "trough load." Load then ramps up linearly over the next 10
263 * seconds unil it reaches one request per 250 milliseconds, which is
264 * "peak load." Peak load is sustained for 20 seconds and then load ramps
265 * back down, again taking 10 seconds to get down to "trough load," which
266 * is sustained for 30 seconds. The cycle then repeats.</li>
267 *
268 * <li><pre>
269 * delayType = "gaussian"
270 * minDelay = 250
271 * maxDelay = 500
272 * cycleType = "oscillating"
273 * rampType = "linear"
274 * rampPeriod = 10000
275 * peakPeriod = 20000
276 * troughPeriod = 30000
277 * sigma = 100 </pre> produces a load pattern similar to example 1, but in
278 * this case the computed delay value is fed into a gaussian random number
279 * generator as the mean and 100 as the standard deviation - i.e.,
280 * <code>nextDelay</code> returns random, gaussian distributed values with
281 * means moving according to the cyclic pattern in example 1.</li>
282 *
283 * <li><pre>
284 * delayType = "constant"
285 * minDelay = 250
286 * maxDelay = 500
287 * cycleType = "none"
288 * rampType = "linear"
289 * rampPeriod = 10000</pre> produces a load pattern that increases linearly
290 * from one request every 500ms to one request every 250ms and then stays
291 * constant at that level until the run is over. Other parameters are
292 * ignored in this case.</li>
293 *
294 * <li><pre>
295 * delayType = "poisson"
296 * minDelay = 250
297 * maxDelay = 500
298 * cycleType = "none"
299 * rampType = "none"
300 * </pre> produces inter-arrival times that are poisson distributed with
301 * mean 250ms. Note that when rampType is "none," the value of
302 * <code>minDelay</code> is used as the (constant) mean delay.</li></ol>
303 *
304 * @return next value for delay
305 */
306 protected long nextDelay() throws ConfigurationException {
307 double targetDelay = 0;
308 double dMinDelay = (double) minDelay;
309 double dMaxDelay = (double) maxDelay;
310 double delayDifference = dMaxDelay - dMinDelay;
311 long currentTime = System.currentTimeMillis();
312 if (cycleType.equals("none")) {
313 if (rampType.equals("none") ||
314 (currentTime - startTime) > rampPeriod) { // ramped up
315 targetDelay = dMinDelay;
316 } else if (rampType.equals("linear")) { // single period linear
317 double prop =
318 (double) (currentTime - startTime) / (double) rampPeriod;
319 targetDelay = dMaxDelay - delayDifference * prop;
320 } else { // Random jumps down to delay - single period
321 // TODO: govern size of jumps as in oscillating
322 // Where we last were as proportion of way down to minDelay
323 double lastProp =
324 (dMaxDelay - lastMean) / delayDifference;
325 // Make a random jump toward 1 (1 = all the way down)
326 double prop = randomData.nextUniform(lastProp, 1);
327 targetDelay = dMaxDelay - delayDifference * prop;
328 }
329 } else if (cycleType.equals("oscillating")) {
330 // First change cycle state if we need to
331 adjustState(currentTime);
332 targetDelay = computeCyclicDelay(
333 currentTime, dMinDelay, dMaxDelay);
334 } else {
335 throw new ConfigurationException(
336 "Cycle type not supported: " + cycleType);
337 }
338
339 // Remember last mean for ramp up / down
340 lastMean = targetDelay;
341
342 if (delayType.equals("constant")) {
343 return Math.round(targetDelay);
344 }
345
346 // Generate and return random deviate
347 if (delayType.equals("gaussian")) {
348 return Math.round(randomData.nextGaussian(targetDelay, sigma));
349 } else { // must be Poisson
350 return randomData.nextPoisson(targetDelay);
351 }
352 }
353
354 /**
355 * Adjusts cycleState, periodStart and lastMean if a cycle state
356 * transition needs to happen.
357 *
358 * @param currentTime current time
359 */
360 protected void adjustState(long currentTime) {
361 long timeInPeriod = currentTime - periodStart;
362 if ( ((cycleState == RAMPING_UP || cycleState == RAMPING_DOWN) &&
363 timeInPeriod < rampPeriod) ||
364 (cycleState == PEAK_LOAD && timeInPeriod < peakPeriod) ||
365 (cycleState == TROUGH_LOAD && timeInPeriod < troughPeriod)) {
366 return; // No state change
367 }
368 switch (cycleState) {
369 case RAMPING_UP:
370 if (peakPeriod > 0) {
371 cycleState = PEAK_LOAD;
372 } else {
373 cycleState = RAMPING_DOWN;
374 }
375 lastMean = (double) minDelay;
376 periodStart = currentTime;
377 break;
378
379 case RAMPING_DOWN:
380 if (troughPeriod > 0) {
381 cycleState = TROUGH_LOAD;
382 } else {
383 cycleState = RAMPING_UP;
384 }
385 lastMean = (double) maxDelay;
386 periodStart = currentTime;
387 break;
388
389 case PEAK_LOAD:
390 if (rampPeriod > 0) {
391 cycleState = RAMPING_DOWN;
392 lastMean = (double) minDelay;
393 } else {
394 cycleState = TROUGH_LOAD;
395 lastMean = (double) maxDelay;
396 }
397 periodStart = currentTime;
398 break;
399
400 case TROUGH_LOAD:
401 if (rampPeriod > 0) {
402 cycleState = RAMPING_UP;
403 lastMean = (double) maxDelay;
404 } else {
405 cycleState = PEAK_LOAD;
406 lastMean = (double) minDelay;
407 }
408 periodStart = currentTime;
409 break;
410
411 default:
412 throw new IllegalStateException(
413 "Illegal cycle state: " + cycleState);
414 }
415 }
416
417 protected double computeCyclicDelay(
418 long currentTime, double min, double max) {
419
420 // Constant load states
421 if (cycleState == PEAK_LOAD) {
422 return min;
423 }
424 if (cycleState == TROUGH_LOAD) {
425 return max;
426 }
427
428 // No ramp - stay at min or max load during ramp
429 if (rampType.equals("none")) { // min or max, no ramp
430 if (cycleState == RAMPING_UP) {
431 return max;
432 } else {
433 return min;
434 }
435 }
436
437 // Linear ramp type and ramping up or down
438 double diff = max - min;
439 if (rampType.equals("linear")) {
440 double prop =
441 (double)(currentTime - periodStart) / (double) rampPeriod;
442 if (cycleState == RAMPING_UP) {
443 return max - diff * prop;
444 } else {
445 return min + diff * prop;
446 }
447 } else { // random jumps down, then back up
448 // Where we last were as proportion of way down to minDelay
449 double lastProp =
450 (max - lastMean) / diff;
451 // Where we would be if this were a linear ramp
452 double linearProp =
453 (double)(currentTime - periodStart) / (double) rampPeriod;
454 // Need to govern size of jumps, otherwise "convergence"
455 // can be too fast - use linear ramp as governor
456 if ((cycleState == RAMPING_UP && (lastProp > linearProp)) ||
457 (cycleState == RAMPING_DOWN &&
458 ((1 - lastProp) > linearProp)))
459 lastProp = (cycleState == RAMPING_UP) ? linearProp :
460 (1 - linearProp);
461 double prop = 0;
462 if (cycleState == RAMPING_UP) { // Random jump toward 1
463 prop = randomData.nextUniform(lastProp, 1);
464 } else { // Random jump toward 0
465 prop = randomData.nextUniform(0, lastProp);
466 }
467 // Make sure sequence is monotone
468 if (cycleState == RAMPING_UP) {
469 return Math.min(lastMean, max - diff * prop);
470 } else {
471 return Math.max(lastMean, min + diff * prop);
472 }
473 }
474 }
475 }