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 }