1/*
2  Copyright (C) 2000-2005 SKYRIX Software AG
3
4  This file is part of SOPE.
5
6  SOPE is free software; you can redistribute it and/or modify it under
7  the terms of the GNU Lesser General Public License as published by the
8  Free Software Foundation; either version 2, or (at your option) any
9  later version.
10
11  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12  WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  License for more details.
15
16  You should have received a copy of the GNU Lesser General Public
17  License along with SOPE; see the file COPYING.  If not, write to the
18  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19  02111-1307, USA.
20*/
21
22#include <NGObjWeb/WOStatisticsStore.h>
23#include <NGObjWeb/WORequest.h>
24#include <NGObjWeb/WOResponse.h>
25#include <NGObjWeb/WOContext.h>
26#include <NGObjWeb/WOComponent.h>
27#include "common.h"
28
29@interface _WOPageStats : NSObject
30{
31@public
32  NSString       *pageName;
33  unsigned       totalResponseCount;
34  unsigned       totalResponseSize;
35  unsigned       zippedResponsesCount;
36  unsigned       totalZippedSize;
37  unsigned       largestResponseSize;
38  NSInteger      smallestResponseSize;
39  NSTimeInterval minimumDuration;
40  NSTimeInterval maximumDuration;
41  NSTimeInterval totalDuration;
42}
43@end
44
45@interface WORequest(UsedPrivates)
46- (NSCalendarDate *)startDate;
47- (id)startStatistics;
48@end
49
50@implementation WOStatisticsStore
51
52static char *monthAbbr[13] = {
53  "Dec",
54  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
55  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
56};
57static NSTimeZone *gmt = nil;
58static Class NSNumberClass    = Nil;
59static Class NSStringClass    = Nil;
60static BOOL  runMultithreaded = NO;
61
62+ (void)initialize {
63  NSNumberClass = [NSNumber class];
64  NSStringClass = [NSString class];
65
66  if (gmt == nil)
67    gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
68
69  runMultithreaded = [[NSUserDefaults standardUserDefaults]
70		                      boolForKey:@"WORunMultithreaded"];
71}
72
73- (id)init {
74  if ((self = [super init])) {
75    self->startTime = [[NSDate date] copy];
76    self->smallestResponseSize = -1;
77    self->totalDuration = 0.0;
78
79    if (runMultithreaded)
80      self->lock = [[NSRecursiveLock alloc] init];
81  }
82  return self;
83}
84
85- (void)dealloc {
86  [self->pageStatistics release];
87  [self->startTime      release];
88  [self->lock           release];
89  [super dealloc];
90}
91
92/* query */
93
94static id mkuint(unsigned int i) {
95  return [NSNumberClass numberWithUnsignedInt:i];
96}
97static id mkdbl(double d) {
98#if 1 // TODO: why is that?
99  char buf[64];
100  sprintf(buf, "%.3f", d);
101  return [NSStringClass stringWithCString:buf];
102#else
103  return [NSNumberClass numberWithDouble:d];
104#endif
105}
106
107- (NSDictionary *)statisticsForPageNamed:(NSString *)_pageName {
108  // TODO: fix inefficient use of NSString
109  NSMutableDictionary *stats;
110  _WOPageStats        *pageStats;
111
112  if ((pageStats = [self->pageStatistics objectForKey:_pageName]) == nil)
113    return nil;
114
115  stats = [NSMutableDictionary dictionaryWithCapacity:16];
116
117  [stats setObject:mkuint(pageStats->totalResponseSize)
118         forKey:@"totalResponseSize"];
119  [stats setObject:mkuint(pageStats->totalResponseCount)
120         forKey:@"totalResponseCount"];
121  [stats setObject:mkdbl(pageStats->totalDuration)
122         forKey:@"totalDuration"];
123
124  if (pageStats->smallestResponseSize >= 0) {
125    [stats setObject:mkuint(pageStats->largestResponseSize)
126           forKey:@"largestResponseSize"];
127    [stats setObject:mkuint(pageStats->smallestResponseSize)
128           forKey:@"smallestResponseSize"];
129    [stats setObject:mkdbl(pageStats->minimumDuration)
130           forKey:@"minimumDuration"];
131    [stats setObject:mkdbl(pageStats->maximumDuration)
132           forKey:@"maximumDuration"];
133  }
134
135  if (pageStats->totalResponseCount > 0) {
136    [stats setObject:
137             mkuint(pageStats->totalResponseSize/pageStats->totalResponseCount)
138           forKey:@"averageResponseSize"];
139    [stats setObject:
140             mkdbl(pageStats->totalDuration / pageStats->totalResponseCount)
141           forKey:@"averageDuration"];
142  }
143
144  [stats setObject:mkuint(pageStats->zippedResponsesCount)
145         forKey:@"numberOfZippedResponses"];
146  [stats setObject:mkuint(pageStats->totalZippedSize)
147         forKey:@"totalZippedSize"];
148
149  /* calc frequencies */
150  {
151    double d;
152
153    d = ((double)self->totalResponseCount) / 100.0; // one percent
154    d = ((double)pageStats->totalResponseCount) / d; // percents of total
155    [stats setObject:[NSStringClass stringWithFormat:@"%.3f%%", d]
156           forKey:@"responseFrequency"];
157
158    d = ((double)self->totalDuration) / 100.0; // one percent
159    d = ((double)pageStats->totalDuration) / d; // percents of total
160    [stats setObject:[NSStringClass stringWithFormat:@"%.3f%%", d]
161           forKey:@"relativeTimeConsumption"];
162
163    d = ((double)self->pageResponseCount) / 100.0; // one percent
164    d = ((double)pageStats->totalResponseCount) / d; // percents of total
165    [stats setObject:[NSStringClass stringWithFormat:@"%.3f%%", d]
166           forKey:@"pageFrequency"];
167
168
169    d = ((double)self->totalResponseSize) / 100.0; // one percent
170    d = ((double)pageStats->totalResponseSize) / d; // percents of total
171    [stats setObject:[NSStringClass stringWithFormat:@"%.3f%%", d]
172           forKey:@"pageDeliveryVolume"];
173  }
174
175  return stats;
176}
177
178- (NSDictionary *)statistics {
179  NSMutableDictionary *stats;
180  NSDate   *now;
181  double   uptime;
182
183  stats  = [NSMutableDictionary dictionaryWithCapacity:64];
184  now    = [NSDate date];
185  uptime = [now timeIntervalSinceDate:self->startTime];
186
187  [stats setObject:mkuint(self->totalResponseSize)
188         forKey:@"totalResponseSize"];
189  [stats setObject:mkuint(self->totalResponseCount)
190         forKey:@"totalResponseCount"];
191  [stats setObject:mkdbl(self->totalDuration)
192         forKey:@"totalDuration"];
193  [stats setObject:mkuint(self->pageResponseCount)
194         forKey:@"pageResponseCount"];
195  if (self->smallestResponseSize >= 0) {
196    [stats setObject:mkuint(self->largestResponseSize)
197           forKey:@"largestResponseSize"];
198    [stats setObject:mkuint(self->smallestResponseSize)
199           forKey:@"smallestResponseSize"];
200    [stats setObject:mkdbl(self->minimumDuration)
201           forKey:@"minimumDuration"];
202    [stats setObject:mkdbl(self->maximumDuration)
203           forKey:@"maximumDuration"];
204  }
205
206  if ((self->totalDuration > 0) && (uptime > 0)) {
207    [stats setObject:
208             [NSStringClass stringWithFormat:@"%.3f%%",
209                         self->totalDuration / (uptime / 100.0)]
210           forKey:@"instanceLoad"];
211  }
212
213  if (self->totalResponseCount > 0) {
214    [stats setObject:mkuint(self->totalResponseSize / self->totalResponseCount)
215           forKey:@"averageResponseSize"];
216    [stats setObject:mkdbl(self->totalDuration / self->totalResponseCount)
217           forKey:@"averageDuration"];
218  }
219
220  [stats setObject:self->startTime forKey:@"instanceStartDate"];
221  [stats setObject:now             forKey:@"statisticsDate"];
222  [stats setObject:[NSStringClass stringWithFormat:@"%.3f", uptime]
223         forKey:@"instanceUptime"];
224  [stats setObject:[NSStringClass stringWithFormat:@"%.3f", uptime / 3600.0]
225         forKey:@"instanceUptimeInHours"];
226
227  [stats setObject:mkuint(self->zippedResponsesCount)
228         forKey:@"numberOfZippedResponses"];
229  [stats setObject:mkuint(self->totalZippedSize)
230         forKey:@"totalZippedSize"];
231
232  /* page statistics */
233  {
234    NSEnumerator        *pageNames;
235    NSString            *pageName;
236    NSMutableDictionary *pageStats;
237
238    pageStats = [NSMutableDictionary dictionaryWithCapacity:16];
239    pageNames = [self->pageStatistics keyEnumerator];
240    while ((pageName = [pageNames nextObject])) {
241      NSDictionary *s;
242
243      s = [self statisticsForPageNamed:pageName];
244      if (s == nil)
245        continue;
246
247      [pageStats setObject:s forKey:pageName];
248    }
249
250    if (pageStats)
251      [stats setObject:pageStats forKey:@"pageStatistics"];
252  }
253
254  return stats;
255}
256
257/* recording */
258
259- (void)recordStatisticsForResponse:(WOResponse *)_response
260  inContext:(WOContext *)_context
261{
262  WOComponent    *page;
263  unsigned       size;
264  NSNumber       *zippedSize;
265  NSDate         *requestStartDate;
266  NSDate         *now;
267  NSTimeInterval duration;
268
269  zippedSize = [[_response userInfo] objectForKey:@"WOResponseZippedLength"];
270  if (zippedSize) {
271    size = [[[_response userInfo] objectForKey:@"WOResponseUnzippedLength"]
272	                          unsignedIntValue];
273  }
274  else
275    size = [[_response content] length];
276
277  requestStartDate = [[_context request] startDate];
278  now = [NSDate date];
279
280  duration = (requestStartDate)
281    ? [now timeIntervalSinceDate:requestStartDate]
282    : 0.0;
283
284  self->totalResponseCount++;
285  self->totalResponseSize += size;
286  self->totalDuration     += duration;
287
288  if (self->smallestResponseSize == -1) {
289    /* first request */
290    self->largestResponseSize  = size;
291    self->smallestResponseSize = size;
292    self->maximumDuration      = duration;
293    self->minimumDuration      = duration;
294  }
295  else {
296    if (size > self->largestResponseSize)  self->largestResponseSize = size;
297    if (size < self->smallestResponseSize) self->smallestResponseSize = size;
298    if (duration > self->maximumDuration)  self->maximumDuration = duration;
299    if (duration < self->minimumDuration)  self->minimumDuration = duration;
300  }
301
302  if (zippedSize) {
303    self->zippedResponsesCount++;
304    self->totalZippedSize += [zippedSize unsignedIntValue];
305  }
306
307  if ((page = [_context page])) {
308    _WOPageStats *pageStats;
309
310    self->pageResponseCount++;
311
312    if (self->pageStatistics == nil)
313      self->pageStatistics = [[NSMutableDictionary alloc] initWithCapacity:64];
314
315    if ((pageStats = [self->pageStatistics objectForKey:[page name]]) == nil) {
316      pageStats = [[[_WOPageStats alloc] init] autorelease];
317      pageStats->pageName = [[page name] copy];
318
319      pageStats->largestResponseSize  = size;
320      pageStats->smallestResponseSize = size;
321      pageStats->maximumDuration      = duration;
322      pageStats->minimumDuration      = duration;
323
324      [self->pageStatistics setObject:pageStats forKey:pageStats->pageName];
325    }
326    else {
327      if (size > pageStats->largestResponseSize)
328        pageStats->largestResponseSize = size;
329      if (size < pageStats->smallestResponseSize)
330        pageStats->smallestResponseSize = size;
331      if (duration > pageStats->maximumDuration)
332        pageStats->maximumDuration = duration;
333      if (duration < pageStats->minimumDuration)
334        pageStats->minimumDuration = duration;
335    }
336
337    pageStats->totalResponseCount++;
338    pageStats->totalResponseSize += size;
339    pageStats->totalDuration     += duration;
340
341    if (zippedSize) {
342      pageStats->zippedResponsesCount++;
343      pageStats->totalZippedSize += [zippedSize unsignedIntValue];
344    }
345  }
346}
347
348- (NSString *)descriptionForResponse:(WOResponse *)_response
349  inContext:(WOContext *)_context
350{
351  NSString    *result;
352  WOComponent *page;
353
354  if ((page = [_context page]) == nil) {
355    return [NSStringClass stringWithFormat:
356			    @"<no page generated for context %@>",
357			    _context];
358  }
359
360  result =
361    [page respondsToSelector:@selector(descriptionForResponse:inContext:)]
362    ? [page descriptionForResponse:_response inContext:_context]
363    : [page name];
364  return result;
365}
366
367/* formatting */
368
369- (NSString *)formatDescription:(NSString *)_description
370  forResponse:(WOResponse *)_response
371  inContext:(WOContext *)_context
372{
373  NSMutableString *result;
374  WORequest       *request;
375  NSString        *remoteHost = @"-";
376  NSCalendarDate  *now;
377  NSDate          *startDate;
378  NSString        *tmp;
379  char            buf[64];
380
381  request = [_context request];
382  result  = [NSMutableString stringWithCapacity:256];
383
384  /* remote host and date */
385
386  if ((remoteHost = [request headerForKey:@"x-webobjects-remote-host"]))
387    ;
388  else if ((remoteHost = [request headerForKey:@"x-webobjects-remote-addr"]))
389    ;
390  else
391    remoteHost = @"-";
392
393  now = [NSCalendarDate calendarDate];
394  [now setTimeZone:gmt];
395
396  [result appendString:remoteHost];
397  sprintf(buf,
398#if GS_64BIT_OLD
399	  " - - [%02i/%s/%04i:%02i:%02i:%02i GMT] ",
400#else
401	  " - - [%02li/%s/%04li:%02li:%02li:%02li GMT] ",
402#endif
403	  [now dayOfMonth], monthAbbr[[now monthOfYear]],
404	  [now yearOfCommonEra],
405	  [now hourOfDay], [now minuteOfHour], [now secondOfMinute]);
406  tmp = [[NSStringClass alloc] initWithCString:buf];
407  [result appendString:tmp];
408  [tmp release];
409
410  /* request */
411
412  [result appendString:@"\""];
413  [result appendString:[request method]];
414  [result appendString:@" "];
415  [result appendString:[request uri]];
416  [result appendString:@" "];
417  [result appendString:[request httpVersion]];
418  [result appendString:@"\" "];
419
420  /* response */
421
422  [result appendFormat:@"%i %i",
423            [_response status], (int)[[_response content] length]];
424
425  if ((startDate = [request startDate]) != nil) {
426    NSTimeInterval duration;
427
428    duration = [now timeIntervalSinceDate:startDate];
429    sprintf(buf, " %.3f", duration);
430    tmp = [[NSStringClass alloc] initWithCString:buf];
431    [result appendString:tmp];
432    [tmp release];
433  }
434
435  return result;
436}
437
438/* NSLocking */
439
440- (void)lock {
441  [self->lock lock];
442}
443- (void)unlock {
444  [self->lock unlock];
445}
446
447@end /* WOStatisticsStore */
448
449@implementation _WOPageStats
450
451- (id)init {
452  self->totalDuration   = 0.0;
453  self->minimumDuration = 0.0;
454  self->maximumDuration = 0.0;
455  return self;
456}
457
458- (void)dealloc {
459  RELEASE(self->pageName);
460  [super dealloc];
461}
462
463@end /* _WOPageStats */
464