--- inn-1.7.2.orig/innd/his.c
+++ inn-1.7.2/innd/his.c
@@ -1,4 +1,4 @@
-/*  $Revision: 1.23 $
+/*  $Revision: 1.2+histcache$
 **
 **  History file routines.
 */
@@ -8,7 +8,21 @@
 #include "clibrary.h"
 #include "innd.h"
 #include "dbz.h"
+#if	defined(TIMER)
+#include "timer.h"
+#endif	/* defined(TIMER) */
 
+/*
+ * history lookup cache
+ */
+#define HISCACHESIZE  (64 * 1024L)    /* num elements in lookup cache array */
+typedef struct {
+    long              MessageIDHash;
+    BOOL              Found;
+} _HIScache;
+STATIC _HIScache      HIScache[HISCACHESIZE];
+unsigned long         HIScachehits;
+unsigned long         HIScachelookups;
 
 STATIC char	HIShistpath[] = _PATH_HISTORY;
 STATIC FILE	*HISwritefp;
@@ -16,6 +30,8 @@
 STATIC int	HISdirty;
 STATIC int	HISincore = INND_DBZINCORE;
 
+#include "timer.h"
+
 
 /*
 **  Set up the history files.
@@ -46,6 +62,9 @@
 	    exit(1);
 	}
     }
+    /* initialize history lookup cache data */
+    memset((void *)&HIScache, 0, sizeof(HIScache));
+    HIScachehits = HIScachelookups = 0;
 }
 
 
@@ -55,6 +74,9 @@
 void
 HISsync()
 {
+#if	defined(TIMER)
+    TMRstart(TMR_HISSYNC);
+#endif	/* defined(TIMER) */
     if (HISdirty) {
 	if (dbzsync()) {
 	    syslog(L_FATAL, "%s cant dbzsync %m", LogName);
@@ -62,6 +84,9 @@
 	}
 	HISdirty = 0;
     }
+#if	defined(TIMER)
+    TMRstop(TMR_HISSYNC);
+#endif	/* defined(TIMER) */
 }
 
 
@@ -84,6 +109,10 @@
 	    syslog(L_ERROR, "%s cant close history %m", LogName);
 	HISreadfd = -1;
     }
+
+    if (HIScachelookups > 0)
+        syslog(L_NOTICE, "SERVER history cache final: %lu lookups, %lu hits",
+       	       HIScachelookups, HIScachehits);
 }
 
 
@@ -138,8 +167,14 @@
     register int	i;
 
     /* Get the seek value into the history file. */
+#if	defined(TIMER)
+    TMRstart(TMR_HISGREP);
+#endif	/* defined(TIMER) */
     HISsetkey(MessageID, &key);
     val = dbzfetch(key);
+#if	defined(TIMER)
+    TMRstop(TMR_HISGREP);
+#endif	/* defined(TIMER) */
     if (val.dptr == NULL || val.dsize != sizeof offset)
 	return NULL;
 
@@ -198,9 +233,36 @@
 {
     datum	key;
     datum	val;
+    long	hash_pos, hash_val;
 
+
+    /* hash msg id */
+    hash_val = dbzhash(MessageID, strlen(MessageID));
+    hash_pos = hash_val % HISCACHESIZE;
+
+    /* check history lookup cache for hashed msg id */
+    HIScachelookups++;
+    if (HIScache[hash_pos].MessageIDHash == hash_val) {
+        HIScachehits++;
+        return HIScache[hash_pos].Found;
+    }
+
+    /* check dbz history for msg id */
+#if	defined(TIMER)
+    TMRstart(TMR_HISHAVE);
+#endif	/* defined(TIMER) */
     HISsetkey(MessageID, &key);
     val = dbzfetch(key);
+
+    /* store hashed msg id, lookup result in history lookup cache */
+    HIScache[hash_pos].MessageIDHash = hash_val;
+    HIScache[hash_pos].Found = (char)(val.dptr != NULL);
+
+#if	defined(TIMER)
+    TMRstop(TMR_HISHAVE);
+#endif	/* defined(TIMER) */
+
+    /* return result of lookup */
     return val.dptr != NULL;
 }
 
@@ -237,11 +299,14 @@
     char		*paths;
 {
     static char		NOPATHS[] = "";
-    long		offset;
+    long		offset, hash_val, hash_pos;
     datum		key;
     datum		val;
     int			i;
 
+#if	defined(TIMER)
+    TMRstart(TMR_HISWRITE);
+#endif	/* defined(TIMER) */
     HISsetkey(Data->MessageID, &key);
     if (paths != NULL && paths[0] != '\0')
 	HISslashify(paths);
@@ -268,6 +333,9 @@
 	i = errno;
 	syslog(L_ERROR, "%s cant write history %m", LogName);
 	IOError("history", i);
+#if	defined(TIMER)
+        TMRstop(TMR_HISWRITE);
+#endif	/* defined(TIMER) */
 	return FALSE;
     }
 
@@ -278,8 +346,19 @@
 	i = errno;
 	syslog(L_ERROR, "%s cant dbzstore %m", LogName);
 	IOError("history database", i);
+#if	defined(TIMER)
+	TMRstop(TMR_HISWRITE);
+#endif	/* defined(TIMER) */
 	return FALSE;
     }
+#if	defined(TIMER)
+    TMRstop(TMR_HISWRITE);
+#endif	/* defined(TIMER) */
+    /* store hashed message id value in history lookup cache */
+    hash_val = dbzhash(Data->MessageID, strlen(Data->MessageID));
+    hash_pos = hash_val % HISCACHESIZE;
+    HIScache[hash_pos].MessageIDHash = hash_val;
+    HIScache[hash_pos].Found = TRUE;
 
     if (++HISdirty >= ICD_SYNC_COUNT)
 	HISsync();
--- inn-1.7.2.orig/innd/nc.c
+++ inn-1.7.2/innd/nc.c
@@ -1,4 +1,4 @@
-/*  $Revision: 1.51 $
+/*  $Revision: 1.51+precommit-cache$
 **
 **  Routines for the NNTP channel.  Other channels get the descriptors which
 **  we turn into NNTP channels, and over which we speak NNTP.
@@ -107,6 +108,45 @@
 
 
 /*
+ * precommit cache
+ */
+#define NCPRECOMCACHESIZE 4096L
+#define NCPRECOMCACHETIMEOUT 30
+
+/* uncomment line below to:
+ *	- zap cache entry after art acceptance or rejection
+ */
+/* #define NCPRECOM_PEDANTIC */
+
+/* uncomment line below to:
+ *	- have CHECK return DEFER instead of REFUSE
+ *	  for cache hits
+ */
+/* #define NCPRECOM_DEFER */
+
+typedef struct {
+	long MessageIDHash;
+	time_t Timestamp;
+} _NCprecomcache;
+
+STATIC _NCprecomcache NCprecomcache[NCPRECOMCACHESIZE];
+unsigned long NCprecomhits;
+unsigned long NCprecomlookups;
+STATIC long NCprecompos, NCprecomval;
+STATIC NCprecominit = 0;
+
+
+STATIC void
+NCprecomclear ()
+{
+  memset((void *)&NCprecomcache, 0, sizeof(NCprecomcache));
+  NCprecomhits = NCprecomlookups = 0;
+}
+
+
+
+  
+/*
 **  Clear the work-in-progress entry and wake up anyone who might
 **  have been waiting for us.
 */
@@ -264,6 +304,11 @@
 	}
 	cp->State = CSgetcmd;
 	NCwritereply(cp, response);
+#ifdef NCPRECOM_PEDANTIC
+        NCprecomval = dbzhash(wp->MessageID, strlen(wp->MessageID));
+        NCprecompos = NCprecomval % NCPRECOMCACHESIZE;
+        NCprecomcache[NCprecompos].MessageIDHash = 0L;
+#endif
 	break;
 
     case OMthrottled:
@@ -343,7 +388,7 @@
     }
 
     /* Write the terminator. */
-    WCHANappend(cp, NCdotterm, STRLEN(NCdotterm));
+    NCwritereply(cp, NCdot); /* Was WCHANappend.  fixed 1997/10/31 yone@pi.ntts.co.jp */
 }
 #endif	/* 0 */
 
@@ -382,7 +427,7 @@
     }
 
     /* Write the terminator. */
-    NCwritereply(cp, NCdot);
+    NCwritereply(cp, NCdot); /* Was WCHANappend.  fixed 1997/10/31 yone@pi.ntts.co.jp */
 }
 
 
@@ -595,7 +640,20 @@
     if (NCbadid(cp, p))
 	return;
 
-    if (HIShavearticle(p)) {
+    /* compute msg id hash for pre-commit cache check */
+    NCprecomval = dbzhash(p, strlen(p));
+    NCprecompos = NCprecomval % NCPRECOMCACHESIZE;
+    NCprecomlookups++;
+
+    /* check pre-commit cache for msg id, with timeout */
+    if ((NCprecomcache[NCprecompos].MessageIDHash == NCprecomval) &&
+        ((NCprecomcache[NCprecompos].Timestamp + NCPRECOMCACHETIMEOUT) > Now.time)) {
+	NCprecomhits++;
+	cp->Refused++;
+	NCwritereply(cp, NNTP_HAVEIT);
+    }
+
+    else if (HIShavearticle(p)) {
 	cp->Refused++;
 	NCwritereply(cp, NNTP_HAVEIT);
     }
@@ -612,6 +670,9 @@
 #endif	/* defined(NNTP_RESENDIT_LATER) */
     }
     else {
+        /* pre-commit msg id so that noone else offers us this for 30 seconds */
+        NCprecomcache[NCprecompos].MessageIDHash = NCprecomval;
+        NCprecomcache[NCprecompos].Timestamp = Now.time;
 	cp->State = CSgetarticle;
 	NCwritereply(cp, NNTP_SENDIT);
     }
@@ -1409,6 +1474,14 @@
     register CHANNEL	*cp;
     int			i;
 
+    /* one-time initialization of pre-commit cache */
+    if (!NCprecominit) {
+      NCprecomclear();
+      NCprecominit = 1;
+      syslog(L_NOTICE, "Pre-commit cache initialized: %ld entries, %ld bytes",
+      		NCPRECOMCACHESIZE, (long) sizeof(NCprecomcache));
+    }
+
     /* Create the channel. */
     cp = CHANcreate(fd, CTnntp, MustAuthorize ? CSgetauth : CSgetcmd,
 	    NCreader, NCwritedone);
@@ -1484,6 +1557,12 @@
     int			msglen;
     WIP			*who;
 
+#if defined(DO_PERL)
+    char                *perlrc;
+#endif /* DO_PERL */
+
+
+
     if (AmSlave) {
 	NCwritereply(cp, NCbadcommand);
 	return;
@@ -1508,14 +1587,51 @@
 	return;
     }
 
-    if (HIShavearticle(p)) {
+#if defined(DO_PERL)
+    /*  invoke a perl message filter on the message id */
+    else if ((perlrc = (char *)HandleMessageID(p)) != NULL) {
+	cp->Refused++;
+	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_ERR_GOTID_VAL, p);
+	NCwritereply(cp, cp->Sendid.Data);
+    }
+#endif
+
+    /* compute msg id hash for pre-commit cache check */
+    NCprecomval = dbzhash(p, msglen - 5);
+    NCprecompos = NCprecomval % NCPRECOMCACHESIZE;
+    NCprecomlookups++;
+
+    /* check pre-commit cache for msg id, with timeout */
+    if ((NCprecomcache[NCprecompos].MessageIDHash == NCprecomval) &&
+        ((NCprecomcache[NCprecompos].Timestamp + NCPRECOMCACHETIMEOUT) > Now.time)) {
+        NCprecomhits++;
+
+#ifdef NCPRECOM_DEFER
+	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_RESENDID_VAL, p);
+#else
+	cp->Refused++;
+	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_ERR_GOTID_VAL, p);
+#endif
+	NCwritereply(cp, cp->Sendid.Data);
+    }
+    
+    /* check history for msg id existence */
+    else if (HIShavearticle(p)) {
 	cp->Refused++;
 	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_ERR_GOTID_VAL, p);
 	NCwritereply(cp, cp->Sendid.Data);
+
+    /* check to see if article's already in progress on some other channel */
     } else if (NCinprogress(cp, p, &who)) {
 	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_RESENDID_VAL, p);
 	NCwritereply(cp, cp->Sendid.Data);
+
+    /* otherwise, we don't have the message, tell remote to send it to us */
     } else {
+	/* pre-commit msg id so that noone else offers us this for 30 seconds */
+	NCprecomcache[NCprecompos].MessageIDHash = NCprecomval;
+	NCprecomcache[NCprecompos].Timestamp = Now.time;
+
 	(void)sprintf(cp->Sendid.Data, "%d %s", NNTP_OK_SENDID_VAL, p);
 	NCwritereply(cp, cp->Sendid.Data);
     }
@@ -1540,10 +1656,21 @@
 	continue;
     for ( ; ISWHITE(*p); p++)
 	continue;
+
+    msglen = strlen(p) + 5; /* 3 digits + space + id + null */
+
     if (!ARTidok(p)) {
 	syslog(L_NOTICE, "%s bad_messageid %s", CHANname(cp), MaxLength(p, p));
     }
-    msglen = strlen(p) + 5; /* 3 digits + space + id + null */
+
+    /* pre-commit msg id so that noone else offers us this for 30 seconds */
+    else {
+      NCprecomval = dbzhash(p, msglen - 5);
+      NCprecompos = NCprecomval % NCPRECOMCACHESIZE;
+      NCprecomcache[NCprecompos].MessageIDHash = NCprecomval;
+      NCprecomcache[NCprecompos].Timestamp = Now.time;
+    }
+
     if (cp->Sendid.Size < msglen) {
 	if (cp->Sendid.Size > 0) DISPOSE(cp->Sendid.Data);
 	if (msglen > MAXHEADERSIZE) cp->Sendid.Size = msglen;
--- inn-1.7.2.orig/innd/cc.c
+++ inn-1.7.2/innd/cc.c
@@ -737,6 +744,10 @@
     return "1 Not active";
 }
 
+extern unsigned long           HIScachehits;
+extern unsigned long           HIScachelookups;
+extern unsigned long           NCprecomhits;
+extern unsigned long           NCprecomlookups;
 
 /*
 **  Return our operating mode.
@@ -746,6 +757,9 @@
 CCmode(av)
     char		*av[];
 {
+#if defined(DO_PERL)
+    dSP;
+#endif
     register char	*p;
     register int	i;
     int			h;
@@ -842,9 +856,37 @@
         p += strlen(strcpy(p, "enabled"));
     else
         p += strlen(strcpy(p, "disabled"));
+
+    /* perl filter status */
+
+    if (perl_get_cv("filter_stats", FALSE) != NULL) {
+        *p++ = '\n';
+        p += strlen(strcpy(p, "Perl filter stats: "));
+ 
+	ENTER ;
+	SAVETMPS;
+    
+	perl_call_argv("filter_stats", G_EVAL|G_NOARGS, NULL);
+
+	SPAGAIN;
+
+	p += strlen(strcpy(p, POPp)); 
+   
+	PUTBACK;
+	FREETMPS;
+	LEAVE; 
+    }    
+
 #endif /* defined(DO_PERL) */
 
-     i = strlen(buff);
+    *p++ = '\n';
+    sprintf(p, "History cache: %lu lookups, %lu hits", HIScachelookups, HIScachehits);
+    p += strlen(p);
+    *p++ = '\n';
+    sprintf(p, "Precommit cache: %lu lookups, %lu hits", NCprecomlookups, NCprecomhits);
+    p += strlen(p);
+
+    i = strlen(buff);
     if (CCreply.Size <= i) {
 	CCreply.Size = i;
 	RENEW(CCreply.Data, char, CCreply.Size + 1);
--- inn-1.7.2.orig/innd/chan.c
+++ inn-1.7.2/innd/chan.c
@@ -31,6 +31,8 @@
 STATIC CHANNEL	*CHANrc;
 #endif /* PRIORITISE_REMCONN */
 
+#include "timer.h"
+
 
 /*
 **  Set a buffer's contents, ignoring anything that might have
@@ -781,6 +793,10 @@
     char		*p;
     time_t		LastUpdate;
 
+#if	defined(TIMER)
+    TMRinit();
+#endif	/* defined(TIMER) */
+
     LastUpdate = GetTimeInfo(&Now) < 0 ? 0 : Now.time;
     for ( ; ; ) {
 	/* See if any processes died. */
@@ -790,8 +806,17 @@
 	MyRead = RCHANmask;
 	MyWrite = WCHANmask;
 	MyTime = TimeOut;
+#if	defined(TIMER)
+	i = TMRmainloophook();
+	if (i)
+		MyTime.tv_sec = i;
+	TMRstart(TMR_IDLE);
+#endif	/* defined(TIMER) */
 	count = select(CHANlastfd + 1, &MyRead, &MyWrite, (FDSET *)NULL,
 		&MyTime);
+#if	defined(TIMER)
+	TMRstop(TMR_IDLE);
+#endif	/* defined(TIMER) */
 	if (GotTerminate) {
 	    (void)write(2, EXITING, STRLEN(EXITING));
 	    CleanupAndExit(0, (char *)NULL);
--- inn-1.7.2.orig/innd/art.c
+++ inn-1.7.2/innd/art.c
@@ -10,6 +10,9 @@
 #include "dbz.h"
 #include "art.h"
 #include <sys/uio.h>
+#if	defined(TIMER)
+#include "timer.h"
+#endif	/* defined(TIMER) */
 
 typedef struct iovec	IOVEC;
 
@@ -1725,6 +1768,7 @@
     static BUFFER	Files;
     static BUFFER	Header;
     static char		buff[SPOOLNAMEBUFF];
+    static char		path[MAXHEADERSIZE];
     register char	*p;
     register int	i;
     register int	j;
@@ -1757,6 +1803,18 @@
     Data.MessageID = ihave;
     error = ARTclean(article, &Data);
 
+    /* This makes me want to scream in agony and rip my eyeballs
+       out.  We clobber our Path: header all to hell later on, so
+       we will preserve the data in a static buffer.  I suppose
+       we could have gone groveling around in the article buffer,
+       but that seemed even worse, and more complex code.  JG970605 */
+    strcpy(path, "?");
+    if (p = HDR(_path)) {
+	sprintf(path, "%s", p);
+	Data.Path = path;
+	Data.PathLength = strlen(path);
+    }
+
     /* Not much we can do here except toss it and report the error. */
     if (error != NULL) {
         (void)sprintf(buff, "%d %s", NNTP_REJECTIT_VAL,
@@ -2174,6 +2283,9 @@
 	ngp->PostCount = 0;
 
 	if (Data.Name[0] == '\0') {
+#if	defined(TIMER)
+	    TMRstart(TMR_ARTWRITE);
+#endif /* defined(TIMER) */
 	    /* Write the article the first time. */
 	    (void)sprintf(Data.Name, "%s/%lu", ngp->Dir, ngp->Filenum);
 	    if (ARTwrite(Data.Name, article, &Data) < 0
@@ -2192,12 +2304,21 @@
 		if (distributions)
 		    DISPOSE(distributions);
 		ARTreject(buff, article);
+#if	defined(TIMER)
+		TMRstop(TMR_ARTWRITE);
+#endif	/* defined (TIMER) */
 		return buff;
 	    }
+#if	defined(TIMER)
+	    TMRstop(TMR_ARTWRITE);
+#endif	/* defined(TIMER) */
 	    p += strlen(strcpy(p, Data.Name));
 	    Data.NameLength = strlen(Data.Name);
 	}
 	else {
+#if	defined(TIMER)
+	    TMRstart(TMR_ARTLINK);
+#endif	/* defined(TIMER) */
 	    /* Link to the main article. */
 	    (void)sprintf(linkname, "%s/%lu", ngp->Dir, ngp->Filenum);
 	    if (DoLinks && link(Data.Name, linkname) < 0
@@ -2223,6 +2344,9 @@
 		}
 #endif	/* defined(DONT_HAVE_SYMLINK) */
 	    }
+#if	defined(TIMER)
+	    TMRstop(TMR_ARTLINK);
+#endif	/* defined(TIMER) */
 	    *p++ = ' ';
 	    p += strlen(strcpy(p, linkname));
 	}
@@ -2279,11 +2403,25 @@
      * has been processed.  We could pause ourselves here, but it doesn't
      * seem to be worth it. */
     if (Accepted) {
-	if (ControlHeader >= 0)
+	if (ControlHeader >= 0) {
+#if	defined(TIMER)
+    	    TMRstart(TMR_ARTCTRL);
+#endif	/* defined(TIMER) */
 	    ARTcontrol(&Data, HDR(ControlHeader));
+#if	defined(TIMER)
+    	    TMRstop(TMR_ARTCTRL);
+#endif	/* defined(TIMER) */
+	}
 	p = HDR(_supersedes);
-	if (*p && ARTidok(p))
+	if (*p && ARTidok(p)) {
+#if	defined(TIMER)
+    	    TMRstart(TMR_ARTCNCL);
+#endif	/* defined(TIMER) */
 	    ARTcancel(&Data, p, FALSE);
+#if	defined(TIMER)
+    	    TMRstop(TMR_ARTCNCL);
+#endif	/* defined(TIMER) */
+	}
     }
 
     /* If we need the overview data, write it. */
@@ -2292,8 +2430,15 @@
 
     /* And finally, send to everyone who should get it */
     for (sp = Sites, i = nSites; --i >= 0; sp++)
-	if (sp->Sendit)
+	if (sp->Sendit) {
+#if	defined(TIMER)
+    	    TMRstart(TMR_SITESEND);
+#endif	/* defined(TIMER) */
 	    SITEsend(sp, &Data);
+#if	defined(TIMER)
+    	    TMRstop(TMR_SITESEND);
+#endif	/* defined(TIMER) */
+	}
 
     return NNTP_TOOKIT;
 }
--- inn-1.7.2.orig/innd/Makefile
+++ inn-1.7.2/innd/Makefile
@@ -44,14 +44,18 @@
 LINTLIB	= ../llib-linn.ln
 ## =()<TCL_LIB	= @<TCL_LIB>@>()=
 TCL_LIB	= 
+## =()<IDENT_LIB	= @<IDENT_LIB>@>()=
+IDENT_LIB	= -lident
+
+CFLAGS+= -DTIMER
 
 SOURCES	= \
 	art.c cc.c chan.c his.c icd.c innd.c lc.c nc.c newsfeeds.c ng.c \
-	proc.c rc.c site.c tcl.c perl.c
+	proc.c rc.c site.c tcl.c perl.c timer.c
 
 OBJECTS	= \
 	art.o cc.o chan.o his.o icd.o innd.o lc.o nc.o newsfeeds.o ng.o \
-	proc.o rc.o site.o tcl.o perl.o
+	proc.o rc.o site.o tcl.o perl.o timer.o
 
 ALL	= innd inndstart
 
@@ -68,7 +72,7 @@
 	$(CTAGS) $(SOURCES) ../lib/*.c innd.h ../include/*.h
 
 innd:		$(P) $(OBJECTS) $(LIBNEWS)
-	$(CC) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBNEWS) $(TCL_LIB) $(PERLLIB) $(LIBS)
+	$(CC) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBNEWS) $(TCL_LIB) $(PERLLIB) $(IDENT_LIB) $(LIBS)
 
 inndstart:	$(P) inndstart.o $(LIBNEWS)
 	$(CC) $(LDFLAGS) -o $@ inndstart.o $(LIBNEWS) $(LIBS)
--- inn-1.7.2.orig/innd/timer.c
+++ inn-1.7.2/innd/timer.c
@@ -0,0 +1,149 @@
+#include	<stdio.h>
+#include	<string.h>
+#include	<sys/time.h>
+
+#include	"logging.h"
+#include	"timer.h"
+
+unsigned	start[MAXTIMERS];
+unsigned	cumulative[MAXTIMERS];
+unsigned	count[MAXTIMERS];
+
+unsigned	last_summary;
+
+int		maxtimer = 0;
+
+/*
+ * This package is a quick hack to allow me to draw some conclusions
+ * about INN and "where the time goes" without having to run profiling
+ * and continually stop/restart the server.
+ *
+ * Functions that should have their time monitored need to call
+ * TMRstart(n) at the beginning of the segment of code and TMRstop(n)
+ * at the end.  The time spent will be accumulated and added to the
+ * total for the counter 'n'.  'n' is probably one of the constants in 
+ * timer.h.  You probably want to add a description to TMRsummary
+ * explaining what your 'n' represents.
+ *
+ * There are no sanity checks to see if you have failed to start a timer,
+ * etc., etc.  It is assumed that the user is moderately intelligent and
+ * can properly deploy this code.
+ *
+ * Recursion is not allowed on a given timer.  Setting multiple timers
+ * is fine (i.e., you may have a timer for the total time to write an
+ * article, how long the disk write takes, how long the history update
+ * takes, blah blah.. which are components of the total art write time)
+ *
+ * This package is written with the assumption that TMRmainloophook()
+ * will be called with none of the timers started.  The statistics are
+ * generated here.  It may not be fatal to have a timer running, but
+ * it will not be properly accounted for (at least within that time slice).
+ */
+
+/*
+ * This function is designed to report the number of milliseconds since
+ * the first invocation.  I wanted better resolution than time(), and
+ * something easier to work with than gettimeofday()'s struct timeval's.
+ */
+
+unsigned	gettime()
+{
+	static			init = 0;
+	static struct timeval	start_tv;
+	struct timeval		tv;
+
+	if (! init) {
+		gettimeofday(&start_tv, NULL);
+		init++;
+	}
+	gettimeofday(&tv, NULL);
+	return((tv.tv_sec - start_tv.tv_sec) * 1000 + (tv.tv_usec - start_tv.tv_usec) / 1000);
+}
+
+
+
+
+
+void		TMRinit()
+{
+	int i;
+
+	last_summary = gettime();	/* First invocation */
+
+	for (i = 0; i < MAXTIMERS; i++) {
+		count[i] = start[i] = cumulative[i] = 0;
+	}
+}
+
+
+
+
+
+void		dosummary(secs)
+unsigned	secs;
+{
+	char buffer[8192];
+	char buf[256];
+	char *str;
+	int i;
+
+	sprintf(buffer, "ME time %d ", secs);
+	for (i = 0; i < maxtimer; i++) {
+		str = "???";
+		switch (i) {
+			case TMR_IDLE:		str = "idle";  break;
+			case TMR_ARTWRITE:	str = "artwrite";  break;
+			case TMR_ARTLINK:	str = "artlink";  break;
+			case TMR_HISWRITE:	str = "hiswrite";  break;
+			case TMR_HISSYNC:	str = "hissync";  break;
+			case TMR_SITESEND:	str = "sitesend";  break;
+			case TMR_ARTCTRL:	str = "artctrl";  break;
+			case TMR_ARTCNCL:	str = "artcncl";  break;
+			case TMR_HISHAVE:	str = "hishave";  break;
+			case TMR_HISGREP:	str = "hisgrep";  break;
+		}
+		sprintf(buf, "%s %d(%d) ", str, cumulative[i], count[i]);
+		cumulative[i] = count[i] = 0;
+		strcat(buffer, buf);
+	}
+	syslog(L_NOTICE, "%s", buffer);
+}
+
+
+
+
+
+int		TMRmainloophook()
+{
+	unsigned now = gettime();
+
+	if (now - last_summary >= SUMMARY_INTERVAL) {
+		dosummary(now - last_summary);
+		last_summary = now;
+		return 0;
+	}
+	return SUMMARY_INTERVAL - (now - last_summary)/1000;
+}
+
+
+
+
+
+void		TMRstart(n)
+int n;
+{
+	if (n >= maxtimer) {
+		maxtimer = n + 1;
+	}
+	start[n] = gettime();
+}
+
+
+
+
+void		TMRstop(n)
+int n;
+{
+	cumulative[n] += gettime() - start[n];
+	count[n]++;
+}
--- inn-1.7.2.orig/innd/timer.h
+++ inn-1.7.2/innd/timer.h
@@ -0,0 +1,19 @@
+#define		SUMMARY_INTERVAL	300000
+
+#define		MAXTIMERS		32
+
+#define		TMR_IDLE		0
+#define		TMR_ARTWRITE		1
+#define		TMR_ARTLINK		2
+#define		TMR_HISWRITE		3
+#define		TMR_HISSYNC		4
+#define		TMR_SITESEND		5
+#define		TMR_ARTCTRL		6
+#define		TMR_ARTCNCL		7
+#define		TMR_HISHAVE		8
+#define		TMR_HISGREP		9
+
+extern void TMRinit();
+extern int TMRmainloophook();
+extern void TMRstart(int);
+extern void TMRstop(int);
