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 }