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    }