001    package hirondelle.web4j.webmaster;
002    
003    import java.util.*;
004    import java.util.logging.*;
005    import hirondelle.web4j.util.Consts;
006    import hirondelle.web4j.model.ModelUtil;
007    import hirondelle.web4j.util.Util;
008    
009    /**
010     Statistics on server response time.
011     
012     <P>This class uses the metaphor of a 'photographic exposure' of, say, 10 minutes, whereby response times
013     for a specific time interval are grouped together in a single bin, and average and maximum response times for 
014     that interval are derived for that group.
015     
016     <P>A particular <tt>PerformanceSnapshot</tt> is used only if its 'exposure time' 
017     has not yet ended. A typical 'exposure' lasts a few minutes. 
018     (See {@link hirondelle.web4j.webmaster.PerformanceMonitor} and the <tt>web.xml</tt> 
019     of the example application for more information.) 
020     By inspecting the return value of {@link #getEndTime}, <em>the caller 
021     determines if a <tt>PerformanceSnapshot</tt> object can still be used</em>, or if a new 
022     <tt>PerformanceSnapshot</tt> object must be created for the 'next exposure'.
023    
024     <P>This class is immutable. In particular, {@link #addResponseTime} returns a new object, 
025     instead of changing the state of an existing one.
026    */
027    public final class PerformanceSnapshot {
028      
029      /**
030       @param aExposureTime number of minutes to gather statistics ; see <tt>web.xml</tt>
031       for more information.
032      */
033      public PerformanceSnapshot(Integer aExposureTime){
034        fMaxUrl = Consts.EMPTY_STRING;
035        fAvgResponseTime = 0L;
036        fMaxResponseTime = 0L;
037        fNumRequests = 0;
038        fExposureTime = aExposureTime.intValue();
039        fEndTime = calcEndTime();
040      }
041    
042      /**
043       Return a <tt>PerformanceSnapshot</tt> having no activity. 
044       Such objects are used to explicitly 'fill in the gaps' during periods of no activity.
045       
046       <P>The returned object has the same exposure time as <tt>aCurrentSnapshot</tt>.
047       Its end time is taken as <tt>aCurrentSnapshot.getEndTime()</tt>, plus the exposure time.
048       All other items are <tt>0</tt> or empty. 
049      */
050      public static PerformanceSnapshot forGapInActivity(PerformanceSnapshot aCurrentSnapshot){
051        return new PerformanceSnapshot(
052          aCurrentSnapshot.getEndTime().getTime() + aCurrentSnapshot.getExposureTime()*60*1000, 
053          Consts.EMPTY_STRING, 
054          0L, 
055          0L, 
056          0,
057          aCurrentSnapshot.getExposureTime()
058        );
059      }
060    
061      /**
062       Return a new <tt>PerformanceSnapshot</tt> whose state reflects an additional  
063       data point.
064      
065       @param aResponseTime response time of a particular server request.
066       @param aURL URL of the underlying request.
067      */
068      public PerformanceSnapshot addResponseTime(long aResponseTime, String aURL){
069        long maxResponseTime = 
070          aResponseTime > fMaxResponseTime ? aResponseTime : fMaxResponseTime
071        ;
072        String maxUrl = aResponseTime > fMaxResponseTime ? aURL : fMaxUrl;
073        int numRequests = fNumRequests + 1;
074        return new PerformanceSnapshot(
075          fEndTime, 
076          maxUrl, 
077          getNewAvgResponseTime(aResponseTime), 
078          maxResponseTime, 
079          numRequests,
080          fExposureTime
081        );
082      }
083    
084      /**
085       Return the time that this snapshot will 'end'. After this time, 
086       a new <tt>PerformanceSnapshot</tt> must be created by the caller (using the  
087       constructor).
088      */
089      public Date getEndTime(){
090        return new Date(fEndTime);
091      }
092      
093      /**
094       Return the number of server requests this snapshot has recorded. 
095      
096       <P>If a page contains two images, for example, the server will likely count 
097       3 requests, not 1 (one page and two images). 
098      */
099      public Integer getNumRequests(){
100        return new Integer(fNumRequests);
101      }
102      
103      /** Return the average response time recorded during this snapshot.  */
104      public Long getAvgResponseTime(){
105        return new Long(fAvgResponseTime);
106      }
107      
108      /** Return the maximum response time recorded during this snapshot.  */
109      public Long getMaxResponseTime(){
110        return new Long(fMaxResponseTime);
111      }
112      
113      /** Return the exposure time in minutes, as passed to the constructor.  */
114      public Integer getExposureTime(){
115        return new Integer(fExposureTime);
116      }
117      
118      /** Return the URL of the request responsible for {@link #getMaxResponseTime}.  */
119      public String getURLWithMaxResponseTime(){
120        return fMaxUrl;
121      }
122      
123      /** Intended for debugging only.  */
124      @Override public String toString(){
125        return ModelUtil.toStringFor(this);
126      }
127      
128      @Override public boolean equals(Object aThat){
129        if ( this == aThat ) return true;
130        if ( !(aThat instanceof PerformanceSnapshot) ) return false;
131        PerformanceSnapshot that = (PerformanceSnapshot)aThat;
132        return ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
133      }
134      
135      @Override public int hashCode(){
136        return ModelUtil.hashCodeFor(getSignificantFields());
137      }
138      
139      // PRIVATE //
140      private final int fNumRequests;
141      private final long fEndTime;
142      private final long fAvgResponseTime;
143      private final long fMaxResponseTime;
144      private final int fExposureTime;
145      private final String fMaxUrl;
146      
147      private static final Logger fLogger = Util.getLogger(PerformanceSnapshot.class);  
148      
149      private PerformanceSnapshot(
150        long aEndTime, 
151        String aMaxUrl, 
152        long aAvgResponseTime, 
153        long aMaxResponseTime, 
154        int aNumRequests,
155        int aExposureTime
156      ){
157        fEndTime = aEndTime;
158        fMaxUrl = aMaxUrl;
159        fAvgResponseTime = aAvgResponseTime;
160        fMaxResponseTime = aMaxResponseTime;
161        fNumRequests = aNumRequests;
162        fExposureTime = aExposureTime;
163      }
164      
165      /**
166       Return a new average response time, rounded to the nearest millisecond.
167      */
168      private long getNewAvgResponseTime(long aNewResponseTime){
169        //use integer division to do the rounding
170        //here, all previous requests are treated as having the same average response time
171        long numerator = (fAvgResponseTime * fNumRequests) + aNewResponseTime;
172        long denominator = fNumRequests + 1;
173        return numerator/denominator;
174      }
175      
176      /**
177       Return the time this snapshot's exposure will end.
178      
179       <P>The end time is the next 'round' minute of the hour in agreement with the 
180       configured exposure time. For example, if the exposure time is 20 minutes, and 
181       the current time is 32 minutes past the hour, the end time will be 40 minutes past 
182       the hour. (If the current time is 40 minutes past the hour, the end time will be 
183       60 minutes past the hour.)
184      */
185      private long calcEndTime(){
186        //this item is used as a 'workspace', and its state is changed to find the 
187        //desired end point :
188        final Calendar result = new GregorianCalendar();
189        //leniency will increment hour, day, and so on, if necessary :
190        result.setLenient(true);
191        result.setTimeInMillis(System.currentTimeMillis());
192        //these items are not relevant to the result, since we need to return an even minute
193        result.set(Calendar.MILLISECOND, 0);
194        result.set(Calendar.SECOND, 0);
195        fLogger.finest("Initial calendar, minus seconds : " + result.toString());
196        //increase the minutes until the desired end point is reached
197        //note this item in non-final
198        int minute = result.get(Calendar.MINUTE); 
199        fLogger.finest("Initial minute: " + minute);
200        
201        //Note that the minute is always incremented at least once
202        //This avoids error when the new Snapshot is created at a 'whole' minute (a 
203        //common occurrence).
204        do {
205          ++minute;
206        }
207        while (minute % fExposureTime != 0);
208        fLogger.finest("Final minute : " + minute);
209        
210        result.set(Calendar.MINUTE, minute);
211        return result.getTimeInMillis();
212      }
213      
214      private Object[] getSignificantFields(){
215        return new Object[]{
216          fNumRequests, fEndTime, fAvgResponseTime, fMaxResponseTime, fExposureTime, fMaxUrl 
217       };
218      }
219    }