/*****************************************************************************/
/*
                                    WB.c

WASD Bench :^) - an analogue to Apache Bench (AB, based on version 1.3d).
Why have it?  Apache Bench only compiles and runs on VMS 7.n and later.  This
version should compile and run for all supported WASD configurations.  It also
has the significant performance advantage (looks like ~25%) of using the
underlying $QIO services and not the socket API, and is AST event driven rather
than using the likes of select().  It is not a full implementation of AB (for
instance, it currently does not do POSTs).  The CLI attempts to allow the same
syntax as used by AB (within the constraint that not all options are supported)
so that it is relatively easy to switch between the two (perhaps for comparison
purposes) if desired.

Needless-to-say; putting this utility together was heavily influenced (and none
of the Math is mine) by Apache Bench :^)  Credits for that:

  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  This program is based on ZeusBench V1.0 written by Adam Twiss
  Copyright (c) 1996 by Zeus Technology Ltd.

  Copyright (c) 2000 The Apache Software Foundation.  All rights reserved.
  Including Mike Belshe, Michael Campanella, Dean Gaudet, Ralf S.Engelschall,
  Kurt Sussman, David N. Welton, and probably a host of others.
  See the prologue of AB.C for more detail.
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


A NOTE ON 'NODELACK'
--------------------
The /NODELACK (+n) qualifier applies the TCPIP$C_TCP_NODELACK (TCP_NODELACK)
TCP protocol option.  As the Compaq TCP/IP Services Programming Manual observes

"Under most circumstances, TCP sends data when presented.  When outstanding
data has not been acknowleged, TCP gathers small amounts of data into a single
packet and sends it when an acknowlegement is received.  This functionality can
cause significant delays for some clients ..."

and indeed does seem to introduce an unnecessary 200mS delay when using
keep-alive with WASD (and Apache) Bench(es).  Applying the /NODELACK (+n)
qualifier disables this acknowlgement delay - usually with spectacular
improvements in throughput, particularly where smaller responses are involved. 
The system-wide TCP/IP configuration change producing equivalent behaviour is

  $ TCPIP SET PROTOCOL TCP /NODELAY_ACK

(thanks to JFP for this information).


EXERCISE
--------
This functionality is not found in Apache Bench.  It is basically to supercede
similar functionality provided by the retired WWWRKOUT.  The "exercise" option
allows WASD Bench to be used to stress-test a server.  This behaviour includes
mixing HEAD (~5%) with GET requests, and breaking requests during both request
and response transfers (~5%).  These are designed to shake up the server with
indeterminate request types and client error behaviours.  The best way to
utilize this stress-testing is wrap WASD Bench with a DCL procedure providing a
variety of different requests types, quantities and concurrencies.

  $!(example "wrapper" procedure)
  $ IF P1 .EQS. "" THEN P1 = F$GETSYI("NODENAME")
  $ WB = "$HT_EXE:WB"
  $ SPAWN/NOWAIT WB +e +s +n -n 100 -c  5    http://'p1'/ht_root/exercise/0k.txt
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  5 -k http://'p1'/ht_root/exercise/64k.txt
  $ SPAWN/NOWAIT WB +e +s    -n  50 -c  2    http://'p1'/cgi-bin/conan
  $!(delay spawning anymore until this one concludes)
  $              WB +e +s    -n 100 -c  5    http://'p1'/ht_root/*.*
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  5 -k http://'p1'/ht_root/exercise/64k.txt
  $ SPAWN/NOWAIT WB +e +s +n -n 100 -c  1    +c ISO-8859-1 -
                    http://'p1'/ht_root/exercise/16k.txt
  $ SPAWN/NOWAIT WB +e +s    -n  10 -c  1    http://'p1'/cgi-bin/doesnt-exist
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  2    http://'p1'/cgi-bin/conan/search
  $!(delay spawning anymore until this one concludes)
  $              WB +e +s    -n  50 -c  2    http://'p1'/ht_root/src/httpd/*.*
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  5 -k +l en,de,fr -
                    http://'p1'/ht_root/doc/
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  5 -k http://'p1'/ht_root/exercise/64k.txt
  $ SPAWN/NOWAIT WB +e +s -k -n  50 -c  5 -k http://'p1'/cgi-bin/cgi_symbols
  $              WB +e +s    -n 100 -c  5    +c ISO-8859-1 -
                    http://'p1'/ht_root/*.*
  $!(etc.)


QUALIFIERS/SWITCHES
-------------------
/CONCURRENCY=<integer>     number of simultaneous connections
-C <integer>
/CHARSET=<string>          Accept-Charset string
+c <string>                (yup, it's a plus)
/DBUG                      enabled all (Debug) statements
+d                         (yup, it's a plus)
/DELAYS                    enable Nagle and acknowlegement delay (default off)
-D                         (yup, it's a minus and uppercase)
/EXERCISE                  exercise/test the server (see description above)
+e                         (yup, it's a plus)
/HELP                      display usage information
-h
/KEEPALIVE                 use keep-alive
-k
/LANGUAGE=<string>         Accept-Language string
+l <string>                (yup, it's a plus)
/NOCONFIDENCE              do not show confidence warnings
-S                         (yes, that's upper-case)
/NOHOST                    do not include a "Host: request field
+h                         (yup, it's a plus)
/NOPERCENTILES             do not show percentiles table
-d
/NUMBER=<integer>          total number of connections benchmarked
-n <integer>
/NODELACK                  disable delay acknowlegement (also see +D)
+n                         (yup, it's a plus)
/NONAGLE                   disable Nagle Algorithm (also see +D)
+N                         (yup, it's a plus and uppercase)
/QUIETLY                   minimise output
-q
/SILENTLY                  just make requests, no output (use when exercising)
+s                         (yup, it's a plus)
/VERSION                   print version information
-V


BUILD DETAILS
-------------
See BUILD_WB.COM


COPYRIGHT
---------
Copyright (C) 2002-2004 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
11-JUL-2004  MGD  v1.1.0, make Nagle and acknowlegement delays off by default,
                          add /DELAYS (-D) and /NONAGLE (+N) qualifiers
23-DEC-2003  MGD  v1.0.1, minor conditional mods to support IA64
08-APR-2002  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.1.0"
#define SOFTWARENM "WB"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* VMS related header files */
#include <descrip.h>
#include <iodef.h>
#include <libclidef.h>
#include <libdef.h>
#include <libdtdef.h>
#include <lib$routines.h>
#include <ssdef.h>
#include <stsdef.h>
#include <starlet.h>
#include <syidef.h>

/* Internet-related header files */
#include <socket.h>
#include <in.h>
#include <netdb.h>
#include <inet.h>

/* define required values from TCPIP$INETDEF.H (Multinet does not supply one) */
#define INET_PROTYP$C_STREAM 1
#define INETACP$C_TRANS 2
#define INETACP_FUNC$C_GETHOSTBYNAME 1
#define INETACP_FUNC$C_GETHOSTBYADDR 2
#define TCPIP$C_TCP 6
#define TCPIP$C_AF_INET 2
#define TCPIP$C_DSC_ALL 2
#define TCPIP$C_FULL_DUPLEX_CLOSE 8192
#define TCPIP$C_REUSEADDR 4
#define TCPIP$C_SOCK_NAME 4
#define TCPIP$C_SOCKOPT 1
#define TCPIP$C_TCPOPT 6
#define TCPIP$C_TCP_NODELAY 1
#define TCPIP$C_TCP_NODELACK 9

#define boolean int
#define true 1
#define false 0
 
/* this definition is not available in older VMS versions */
#ifndef EFN$C_ENF
#  define EFN$C_ENF 128  /* Event No Flag (no stored state) */
#endif

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define MINOF(a,b) ((a)<(b))?(a):(b)
#define MAXOF(a,b) ((a)>(b))?(a):(b)

#define MAX_READ_BUFFER_SIZE 16384
#define VARY_READ_BUFFER_SIZE 24
#define DEFAULT_SERVER_PORT 80
#define DEFAULT_NUMBER 100
#define DEFAULT_CONCURRENCY 10
#define DEFAULT_READ_BUFFER_SIZE 16384

/* lib$cvtf_from_internal_time() complains about about a zero elapsed time! */
#define LIB$_DELTIMREQ 1410052

   char  CopyrightInfo [] =
"Copyright (C) 2002-2004 Mark G.Daniel\n\
This program, comes with ABSOLUTELY NO WARRANTY.\n\
This is free software, and you are welcome to redistribute it\n\
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.\n";

struct AnIOsb
{
   unsigned short  Status;
   unsigned short  Count;
   unsigned long  Unused;
};

struct ConnectionStruct
{
   unsigned short  Channel;
   boolean  HeadMethod,
            KeepAlive,
            ResponseHeader;
   int  RxCount,
        ContentCount,
        ContentLength,
        ConnectionNumber,
        ReadBufferSize,
        ResponseHeaderCount;
   char  ReadBuffer [MAX_READ_BUFFER_SIZE+1];
   struct AnIOsb  IOsb;
   struct StatStruct  *StatPtr;
};

struct StatStruct
{
   int  ConnectionNumber,
        RxCount;
   unsigned long  StartBinTime [2],     /* start of request */
                  ConnectBinTime [2],   /* server connected */
                  EndWriteBinTime [2],  /* end of request write */
                  BeginReadBinTime [2], /* initial response */
                  EndBinTime [2];       /* end of request */
   float  ConnectTime,
          ProcessingTime,
          TotalTime,
          WaitTime;
};

#define MAX_CONNECTION_ERROR_COUNT 20

char  Utility [] = "WB";

unsigned long  CvtTimeFloatSeconds = LIB$K_DELTA_SECONDS_F;

#define DBUG 1
#if DBUG
boolean  Debug,
         DebugStats;
#else
#define Debug 0
#define DebugStats 0
#endif

boolean  DoExercise,
         DoHeadMethod,
         DoKeepAlive,
         DoQuietly,
         DoShowHelp,
         DoShowVersion,
         DoSilently,
         DoVerbose,
         NoConfidence,
         NoDelayAck,
         NoDelayNagle,
         NoNoDelaysAtAll,
         NoHost,
         NoPercentiles;

int  BreakRequestCount,
     BreakResponseCount,
     BreakRequestLast,
     BreakResponseLast,
     CliNumber,
     CliConcurrency,
     ConnectionCount,
     ConnectionErrorCount,
     BrokenPipeCount,
     EfnEnf = EFN$C_ENF,
     FailedRequestCount,
     GetMethodCount,
     HeadMethodCount,
     HeadMethodLast,
     HeartBeatRes = 100,
     HtmlTransfered,
     HttpReadErrorCount,
     KeepAliveRequestCount,
     OutstandingCount,
     ProxyServerPort,
     ReadBufferSize,
     RequestGetLength,
     RequestHeadLength,
     ResponseDocumentLength,
     ServerPort,
     TotalTransfered,
     TotalBytesSent;

int  StatusCodeCount [6];

unsigned long  EndBinTime [2],
               StartBinTime [2];

char  *CliAcceptCharsetPtr,
      *CliAcceptLanguagePtr,
      *CliProxyServerHostPtr,
      *ServerHostPtr,
      *SpecifiedPathPtr;

char  CommandLine [256],
      HttpProxyServer [256],
      RequestGetBuffer [512],
      RequestHeadBuffer [512],
      ResponseServerSoftware [256],
      ServerHost [256];

struct StatStruct  *StatDataPtr;

$DESCRIPTOR (InetDeviceDsc, "UCX$DEVICE");

int  OptionEnabled = 1;

struct hostent  *ServerHostEntryPtr;
struct sockaddr_in  ServerSocketName;

struct {
   unsigned int  Length;
   char  *Address;
} ServerSocketNameItem =
   { sizeof(ServerSocketName), (void*)&ServerSocketName };

struct {
   unsigned short  Length;
   unsigned short  Parameter;
   char  *Address;
} ReuseAddr =
   { sizeof(OptionEnabled), TCPIP$C_REUSEADDR, (void*)&OptionEnabled },
  NoDelaysAtAll [] = {
    { sizeof(OptionEnabled), TCPIP$C_TCP_NODELAY, (void*)&OptionEnabled },
    { sizeof(OptionEnabled), TCPIP$C_TCP_NODELACK, (void*)&OptionEnabled },
  },
  NoDelAck =
   { sizeof(OptionEnabled), TCPIP$C_TCP_NODELACK, (void*)&OptionEnabled },
  NoDelay =
   { sizeof(OptionEnabled), TCPIP$C_TCP_NODELAY, (void*)&OptionEnabled },
  ReuseAddressSocketOption =
   { sizeof(ReuseAddr), TCPIP$C_SOCKOPT, (void*)&ReuseAddr },
  NoDelAckTcpOption =
   { sizeof(NoDelAck), TCPIP$C_TCPOPT, (void*)&NoDelAck },
  NoDelayTcpOption =
   { sizeof(NoDelay), TCPIP$C_TCPOPT, (void*)&NoDelay },
  NoDelaysAtAllTcpOption =
   { sizeof(NoDelaysAtAll), TCPIP$C_TCPOPT, (void*)&NoDelaysAtAll };

struct {
   unsigned short  Protocol;
   unsigned char  Type;
   unsigned char  Family;
} TcpSocket = { TCPIP$C_TCP, INET_PROTYP$C_STREAM, TCPIP$C_AF_INET };

/* function prototypes */
int CompareConnect (struct StatStruct*, struct StatStruct*);
int CompareProcessing (struct StatStruct*, struct StatStruct*);
int CompareResponse (struct StatStruct*, struct StatStruct*);
int CompareTotal (struct StatStruct*, struct StatStruct*);
void ConnectToServer (struct ConnectionStruct*);
void ConnectToServerAst (struct ConnectionStruct*);
void CloseConnection (struct ConnectionStruct*);
void DebugStatsData ();
void GetParameters (int, char**);
void GenerateConnections ();
void ParseURL ();
void ReadResponseAst (struct ConnectionStruct*);
void ReportTotals ();
void ShowHelp ();
int strsame (char*, char*, int);
void WriteRequestAst (struct ConnectionStruct*);
char* ErrorMessage (int);

/*****************************************************************************/
/*
*/

int main
(
int argc,
char *argv[]
)
{
   static char  SyiVersion [8];
   static struct
   {
      short  buf_len;
      short  item;
      char   *buf_addr;
      short  *short_ret_len;
   }
   SyiItem [] =
   {
     { sizeof(SyiVersion), SYI$_VERSION, (char*)&SyiVersion, 0 },
     { 0,0,0,0 }
   };

   int  status,
        VmsVersion;

   /*********/
   /* begin */
   /*********/

   if (getenv("WB$DBUG")) Debug = true;

   GetParameters (argc, argv);

   if (DoShowVersion)
   {
      fprintf (stdout, "%%%s-I-VERSION, %s\n%s",
               Utility, SOFTWAREID, CopyrightInfo);
      exit (SS$_NORMAL);
   }

   if (DoShowHelp) ShowHelp ();

   if (VMSnok (status = sys$getsyi (0, 0, 0, &SyiItem, 0, 0, 0)))
      exit (status);
   VmsVersion = ((SyiVersion[1]-48) * 10) + (SyiVersion[3]-48);
   if (Debug) fprintf (stdout, "VmsVersion: %d\n", VmsVersion);
   if (VmsVersion < 70) EfnEnf = 0;

   ParseURL ();

   StatDataPtr = calloc (CliNumber+1, sizeof(struct StatStruct));

   if (!DoSilently)
   {
      fprintf (stdout,
"This is WASD Bench :^) %s\n\
Copyright (c) 2002 Mark G.Daniel, http://wasd.vsm.com.au/\n\
\n\
Benchmarking %s (be patient)%s",
         SOFTWAREID+3, ServerHostPtr, DoQuietly ? "..." : "\n");
      fflush (stdout);
   }

   GenerateConnections ();

   if (!DoSilently)
   {
      if (DoQuietly)
         fprintf (stdout, "..done\n");
      else
         fprintf (stdout, "Finished %d requests\n", ConnectionCount);
   }

   ReportTotals ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Queue up calls to initiate the specified number of concurrent connections. 
This is done with ASTs disabled so connection is actually initiated until this
function reenables ASTs.
*/
 
void GenerateConnections ()

{
   int  status,
        Count;
   char  AcceptCharsetBuffer [256],
         AcceptLanguageBuffer [256],
         HostBuffer [256],
         KeepAliveBuffer [256];
   struct ConnectionStruct  *cxptr;

   /*********/
   /* begin */
   /*********/

   if (NoHost)
      HostBuffer[0] = '\0';
   else
      sprintf (HostBuffer, "Host: %s\r\n", ServerHostPtr);

   if (CliAcceptCharsetPtr)
      sprintf (AcceptCharsetBuffer, "Accept-Charset: %s\r\n",
               CliAcceptCharsetPtr);
   else
      AcceptCharsetBuffer[0] = '\0';

   if (CliAcceptLanguagePtr)
      sprintf (AcceptLanguageBuffer, "Accept-Language: %s\r\n",
               CliAcceptLanguagePtr);
   else
      AcceptLanguageBuffer[0] = '\0';

   if (DoKeepAlive)
      strcpy (KeepAliveBuffer, "Connection: Keep-Alive\r\n");
   else
      KeepAliveBuffer[0] = '\0';

   RequestGetLength = sprintf (RequestGetBuffer,
"GET %s%s HTTP/1.0\r\n\
User-Agent: WASD Bench %s\r\n\
Accept: */*\r\n\
%s\
%s\
%s\
%s\
\r\n",
      HttpProxyServer, SpecifiedPathPtr, SOFTWAREID+3,
      AcceptCharsetBuffer,
      AcceptLanguageBuffer,
      HostBuffer,
      KeepAliveBuffer);
   if (Debug) fprintf (stdout, "|%s|\n", RequestGetBuffer);

   RequestHeadLength = sprintf (RequestHeadBuffer,
"HEAD %s%s HTTP/1.0\r\n\
User-Agent: WASD Bench %s\r\n\
Accept: */*\r\n\
%s\
%s\
%s\
%s\
\r\n",
      HttpProxyServer, SpecifiedPathPtr, SOFTWAREID+3,
      AcceptCharsetBuffer,
      AcceptLanguageBuffer,
      HostBuffer,
      KeepAliveBuffer);
   if (Debug) fprintf (stdout, "|%s|\n", RequestHeadBuffer);

   sys$gettim (&StartBinTime);

   /* disable user-mode ASTs until all initial connections are queued */
   sys$setast (0);
   for (Count = 0; Count < CliConcurrency; Count++)
   {
      cxptr = calloc (1, sizeof(struct ConnectionStruct));
      if (!cxptr) exit (vaxc$errno);
      status = sys$dclast (&ConnectToServer, cxptr, 0);
      if (VMSnok (status)) exit (status);
   }
   sys$setast (1);

   sys$hiber ();
}

/*****************************************************************************/
/*
Loop through the specified number of connections (or attempts thereof).  
Maintain, as best it can be, the specified number of simultaneous connections.  
If a connection attempt fails just quit there for that particular loop, leave 
a bit of time (as reads occur) to allow the server to recover.
*/
 
void ConnectToServer (struct ConnectionStruct *cxptr)

{
   boolean  KeepAlive;
   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ConnectToServer() cxptr: %d\n", cxptr);

   if (ConnectionCount >= CliNumber)
   {
      if (cxptr->Channel)
      {
         /* keep-alive will leave this channel connected */
         status = sys$dassgn (cxptr->Channel);
         if (Debug) fprintf (stdout, "sys$dassgn() %%X%08.08X\n", status);
      }
      if (!OutstandingCount) sys$wake (0, 0);
      return;
   }

   ConnectionCount++;
   OutstandingCount++;

   KeepAlive = cxptr->KeepAlive;

   cxptr->ConnectionNumber = ConnectionCount;
   cxptr->StatPtr = &StatDataPtr[cxptr->ConnectionNumber-1];
   cxptr->StatPtr->ConnectionNumber = cxptr->ConnectionNumber;
   cxptr->ContentLength = -1;
   cxptr->ContentCount =
      cxptr->ResponseHeaderCount =
      cxptr->RxCount = 0;
   cxptr->KeepAlive = DoKeepAlive;
   cxptr->HeadMethod = false;
   cxptr->ResponseHeader = true;

   if (DoExercise)
   {
      /* vary the size of the read buffer */
      cxptr->ReadBufferSize = ReadBufferSize;
      if (ConnectionCount % VARY_READ_BUFFER_SIZE)
         cxptr->ReadBufferSize /= ConnectionCount % VARY_READ_BUFFER_SIZE;
      if (cxptr->ReadBufferSize < 512) cxptr->ReadBufferSize = 512;
   }
   else
      cxptr->ReadBufferSize = ReadBufferSize;

   if (KeepAlive)
   {
      /* already connected! */
      sys$gettim (&cxptr->StatPtr->StartBinTime);
      memset (&cxptr->StatPtr->ConnectBinTime, 0, 8);
      memset (&cxptr->StatPtr->EndWriteBinTime, 0, 8);
      memset (&cxptr->StatPtr->BeginReadBinTime, 0, 8);
      memset (&cxptr->StatPtr->EndBinTime, 0, 8);
      /* fudge the initial connection status */
      cxptr->IOsb.Count = 0;
      cxptr->IOsb.Status = SS$_NORMAL;
      ConnectToServerAst (cxptr);
      return;
   }

   /***************************/
   /* initiate TCP connection */
   /***************************/

   /* assign a channel to the internet template device */
   if (VMSnok (status = sys$assign (&InetDeviceDsc, &cxptr->Channel, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-CONNECT, assign() #%d\n",
               Utility, ConnectionCount);
      exit (status);
   }

   /* make the channel a TCP, connection-oriented socket */
   status = sys$qiow (0, cxptr->Channel, IO$_SETMODE, &cxptr->IOsb, 0, 0,
                      &TcpSocket, 0, 0, 0, &ReuseAddressSocketOption, 0);
   if (Debug)
      fprintf (stdout, "sys$qiow() %%X%08.08X IOsb %%X%08.08X\n",
               status, cxptr->IOsb.Status);
   if (VMSok (status) && VMSnok (cxptr->IOsb.Status))
      status = cxptr->IOsb.Status;
   if (VMSnok (status))
   {
      fprintf (stdout, "%%%s-E-SOCKOPT, sys$qiow() #%d\n",
               Utility, ConnectionCount);
      exit (status);
   }

   if (!NoNoDelaysAtAll)
   {
      /* turn off Nagle algorithm and acknowlegement delay (the default) */
      status = sys$qiow (0, cxptr->Channel, IO$_SETMODE, &cxptr->IOsb, 0, 0,
                         0, 0, 0, 0, &NoDelaysAtAllTcpOption, 0);
      if (Debug)
         fprintf (stdout, "sys$qiow() %%X%08.08X IOsb %%X%08.08X\n",
                  status, cxptr->IOsb.Status);
      if (VMSok (status) && VMSnok (cxptr->IOsb.Status))
         status = cxptr->IOsb.Status;
      if (VMSnok (status))
      {
         fprintf (stdout, "%%%s-E-TCPOPT, sys$qiow() #%d\n",
                  Utility, ConnectionCount);
         exit (status);
      }
   }

   if (NoDelayNagle)
   {
      /* turn off Nagle algorithm */
      status = sys$qiow (0, cxptr->Channel, IO$_SETMODE, &cxptr->IOsb, 0, 0,
                         0, 0, 0, 0, &NoDelayTcpOption, 0);
      if (Debug)
         fprintf (stdout, "sys$qiow() %%X%08.08X IOsb %%X%08.08X\n",
                  status, cxptr->IOsb.Status);
      if (VMSok (status) && VMSnok (cxptr->IOsb.Status))
         status = cxptr->IOsb.Status;
      if (VMSnok (status))
      {
         fprintf (stdout, "%%%s-E-TCPOPT, sys$qiow() #%d\n",
                  Utility, ConnectionCount);
         exit (status);
      }
   }

   if (NoDelayAck)
   {
      /* turn off acknowlegement delay */
      status = sys$qiow (0, cxptr->Channel, IO$_SETMODE, &cxptr->IOsb, 0, 0,
                         0, 0, 0, 0, &NoDelAckTcpOption, 0);
      if (Debug)
         fprintf (stdout, "sys$qiow() %%X%08.08X IOsb %%X%08.08X\n",
                  status, cxptr->IOsb.Status);
      if (VMSok (status) && VMSnok (cxptr->IOsb.Status))
         status = cxptr->IOsb.Status;
      if (VMSnok (status))
      {
         fprintf (stdout, "%%%s-E-TCPOPT, sys$qiow() #%d\n",
                  Utility, ConnectionCount);
         exit (status);
      }
   }

   sys$gettim (&cxptr->StatPtr->StartBinTime);
   memset (&cxptr->StatPtr->ConnectBinTime, 0, 8);
   memset (&cxptr->StatPtr->EndWriteBinTime, 0, 8);
   memset (&cxptr->StatPtr->BeginReadBinTime, 0, 8);
   memset (&cxptr->StatPtr->EndBinTime, 0, 8);

   status = sys$qio (EfnEnf, cxptr->Channel, IO$_ACCESS, &cxptr->IOsb,
                     &ConnectToServerAst, cxptr,
                     0, 0, &ServerSocketNameItem, 0, 0, 0);
   if (VMSnok (status))
   {
      fprintf (stdout, "%%%s-E-CONNECT, sys$qio() #%d\n",
               Utility, cxptr->ConnectionNumber);
      exit (status);
   }
}

/*****************************************************************************/
/*
Connection to server has completed (either successfully or unsuccessfully).
*/
 
void ConnectToServerAst (struct ConnectionStruct *cxptr)

{
   int  status;
   unsigned long  BinTime [2];
   unsigned short  NumTime [7];

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "ConnectToServerAst() IOsb %%X%08.08X\n",
               cxptr->IOsb.Status);

   sys$gettim (&cxptr->StatPtr->ConnectBinTime);

   status = cxptr->IOsb.Status;

   if (VMSnok (status))
   {
      /*****************/
      /* connect error */
      /*****************/

      fprintf (stdout, "%%%s-W-CONNECT, sys$qio() #%d %s\n",
               Utility, cxptr->ConnectionNumber, ErrorMessage(status));
      FailedRequestCount++;
      if (ConnectionErrorCount++ > MAX_CONNECTION_ERROR_COUNT)
      {
         ReportTotals ();
         exit (SS$_NORMAL);
      }

      /* close current then re-initiate a new connection */
      cxptr->KeepAlive = false;
      CloseConnection (cxptr);
      ConnectToServer (cxptr);
      return;
   }

   /***************/
   /* exercising? */
   /***************/

   if (DoExercise)
   {
      sys$gettim (&BinTime);
      sys$numtim (&NumTime, &BinTime);
      if (NumTime[6] != BreakRequestLast && !(NumTime[6] % 26))
      {
         BreakRequestLast = NumTime[6];
         BreakRequestCount++;
         /* ensure the network connection is dropped */
         cxptr->KeepAlive = false;
         CloseConnection (cxptr);
         ConnectToServer (cxptr);
         return;
      }
   }

   /****************/
   /* send request */
   /****************/

   if (DoExercise)
   {
      sys$gettim (&BinTime);
      sys$numtim (&NumTime, &BinTime);
      if (NumTime[6] != HeadMethodLast && !(NumTime[6] % 6))
      {
         HeadMethodLast = NumTime[6];
         cxptr->HeadMethod = true;
      }
   }
   else
      cxptr->HeadMethod = DoHeadMethod;

   if (cxptr->HeadMethod)
   {
      HeadMethodCount++;
      cxptr->HeadMethod = true;
      TotalBytesSent += RequestHeadLength;
      status = sys$qio (EfnEnf, cxptr->Channel,
                        IO$_WRITEVBLK, &cxptr->IOsb, WriteRequestAst, cxptr,
                        RequestHeadBuffer, RequestHeadLength, 0, 0, 0, 0);
   }
   else
   {
      GetMethodCount++;
      TotalBytesSent += RequestGetLength;
      status = sys$qio (EfnEnf, cxptr->Channel,
                        IO$_WRITEVBLK, &cxptr->IOsb, WriteRequestAst, cxptr,
                        RequestGetBuffer, RequestGetLength, 0, 0, 0, 0);
   }

   if (VMSnok (status))
   {
      fprintf (stdout, "%%%s-E-WRITEVBLK, sys$qio() #%d\n",
               Utility, cxptr->ConnectionNumber);
      exit (status);
   }
}

/*****************************************************************************/
/*
Request write has completed (either successfully or unsuccessfully).
*/
 
void WriteRequestAst (struct ConnectionStruct *cxptr)

{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "WriteRequestAst() IOsb: %%X%08.08X\n",
               cxptr->IOsb.Status);

   sys$gettim (&cxptr->StatPtr->EndWriteBinTime);

   status = cxptr->IOsb.Status;

   if (VMSnok (status))
   {
      /***************/
      /* write error */
      /***************/

      if (status == SS$_LINKDISCON) BrokenPipeCount++;

      fprintf (stdout, "%%%s-W-WRITE, sys$qio() #%d %s\n",
               Utility, cxptr->ConnectionNumber, ErrorMessage(status));
      FailedRequestCount++;

      /* close current then re-initiate a new connection */
      cxptr->KeepAlive = false;
      CloseConnection (cxptr);
      ConnectToServer (cxptr);
      return;
   }

   /*****************/
   /* read response */
   /*****************/

   status = sys$qio (EfnEnf, cxptr->Channel, IO$_READVBLK, &cxptr->IOsb,
                     ReadResponseAst, cxptr,
                     cxptr->ReadBuffer, cxptr->ReadBufferSize, 0, 0, 0, 0);
   if (VMSnok (status))
   {
      fprintf (stdout, "%%%s-E-READVBLK, sys$qio() #%d\n",
               Utility, cxptr->ConnectionNumber);
      exit (status);
   }
}

/*****************************************************************************/
/*
A read from the server has completed.  Check it's status.  If complete (or
disconnected) then report.  If not complete then queue to read more.  This code
assumes that at the very least if the header is not written as a whole then it
is written as complete lines.
*/
 
void ReadResponseAst (struct ConnectionStruct *cxptr)

{
   int  status;
   unsigned long  BinTime [2];
   unsigned short  NumTime [7];
   char  *cptr, *lptr, *sptr, *zptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "ReadResponseAst() IOsb: %%X%08.08X Bytes: %d\n",
               cxptr->IOsb.Status, cxptr->IOsb.Count);

   if (!cxptr->RxCount) sys$gettim (&cxptr->StatPtr->BeginReadBinTime);

   status = cxptr->IOsb.Status;

   if (VMSnok (status))
   {
      /*********/
      /* error */
      /*********/

      if (status != SS$_LINKDISCON)
      {
         /**************/
         /* read error */
         /**************/

         fprintf (stdout, "%%%s-W-READ, sys$qio() #%d (%d bytes) %s\n",
                  Utility, cxptr->ConnectionNumber, cxptr->RxCount,
                  ErrorMessage(status));
         HttpReadErrorCount++;
      }

      /* close current then re-initiate a new connection */
      cxptr->KeepAlive = false;
      CloseConnection (cxptr);
      ConnectToServer (cxptr);
      return;
   }

   /********************/
   /* process response */
   /********************/

   if (!cxptr->RxCount)
   {
      /********************/
      /* initial response */
      /********************/

      /* cancel this until we see it in the header */
      cxptr->KeepAlive = false;

      /* skip over "HTTP/n.n" and spaces to get 3 digit status code */
      cxptr->ReadBuffer[cxptr->IOsb.Count] = '\0';
      cptr = cxptr->ReadBuffer;
      while (*cptr && *cptr != ' ' && *cptr != '\r' && *cptr != '\n') cptr++;
      while (*cptr == ' ' && *cptr != '\r' && *cptr != '\n') cptr++;
      /* increment the counter corresponding to the first digit */
      if (isdigit(*cptr) && *cptr >= '1' && *cptr <= '5')
         StatusCodeCount[*cptr-'0']++;
      else
         StatusCodeCount[0]++;
   }

   cxptr->RxCount += cxptr->IOsb.Count;

   if (cxptr->ResponseHeader)
   {
      /*******************/
      /* response header */
      /*******************/

      cxptr->ReadBuffer[cxptr->IOsb.Count] = '\0';
      cptr = cxptr->ReadBuffer;
      while (*cptr)
      {
         lptr = cptr;
         if (toupper(*cptr) == 'C' && strsame (cptr, "Connection:", 11))
         {
            cptr += 11;
            while (isspace(*cptr) && *cptr != '\r' && *cptr != '\n') cptr++;
            if (strsame (cptr, "Keep-Alive", 10)) cxptr->KeepAlive = true;
         }
         else
         if (toupper(*cptr) == 'C' && strsame (cptr, "Content-Length:", 15))
         {
            cptr += 15;
            while (isspace(*cptr) && *cptr != '\r' && *cptr != '\n') cptr++;
            cxptr->ContentLength = atoi(cptr);
            if (!ResponseDocumentLength)
               ResponseDocumentLength = cxptr->ContentLength;
         }
         else
         if (toupper(*cptr) == 'K' && strsame (cptr, "Keep-Alive:", 11))
         {
            cptr += 11;
            cxptr->KeepAlive = true;
         }
         else
         if (!ResponseServerSoftware[0] &&
             toupper(*cptr) == 'S' && strsame (cptr, "Server:", 7))
         {
            cptr += 7;
            while (isspace(*cptr) && *cptr != '\r' && *cptr != '\n') cptr++;
            zptr = (sptr = ResponseServerSoftware) +
                   sizeof(ResponseServerSoftware)-1;
            while (*cptr && *cptr != '\r' && *cptr != '\n' && sptr < zptr)
               *sptr++ = *cptr++;
            *sptr = '\0';
         }
         while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
         if (*cptr == '\r') cptr++;
         if (*cptr == '\n') cptr++;
         cxptr->ResponseHeaderCount += cptr - lptr;
         if (!*cptr) break;
         if (*(unsigned short*)cptr == '\r\n' || *cptr == '\n' )
         {
            if (cxptr->ContentLength < 0) cxptr->KeepAlive = false;
            if (*cptr == '\r') cxptr->ResponseHeaderCount++;
            cxptr->ResponseHeaderCount++;
            cxptr->ResponseHeader = false;
            cxptr->ContentCount = cxptr->RxCount - cxptr->ResponseHeaderCount;
            break;
         }
      }
   }
   else
      cxptr->ContentCount += cxptr->IOsb.Count;

   if (Debug)
      fprintf (stdout, "keepalive:%d count:%d length:%d\n",
               cxptr->KeepAlive, cxptr->ContentCount, cxptr->ContentLength);

   if (cxptr->HeadMethod ||
       (cxptr->ContentLength >= 0 &&
        cxptr->ContentCount >= cxptr->ContentLength))
   {
      CloseConnection (cxptr);
      ConnectToServer (cxptr);
      return;
   }

   /***************/
   /* exercising? */
   /***************/

   if (DoExercise)
   {
      sys$gettim (&BinTime);
      sys$numtim (&NumTime, &BinTime);
      if (NumTime[6] != BreakResponseLast && !(NumTime[6] % 29))
      {
         BreakResponseLast = NumTime[6];
         BreakResponseCount++;
         /* ensure the network connection is dropped */
         cxptr->KeepAlive = false;
         CloseConnection (cxptr);
         ConnectToServer (cxptr);
         return;
      }
   }

   /**********************/
   /* read more response */
   /**********************/

   status = sys$qio (EfnEnf, cxptr->Channel, IO$_READVBLK, &cxptr->IOsb,
                     ReadResponseAst, cxptr,
                     cxptr->ReadBuffer, cxptr->ReadBufferSize, 0, 0, 0, 0);
   if (VMSnok (status))
   {
      fprintf (stdout, "%%%s-E-READVBLK, sys$qio() #%d\n",
               Utility, cxptr->ConnectionNumber);
      exit (status);
   }
}

/****************************************************************************/
/*
Request is concluded.  If keep-alive then basically do nothing.  If not
keep-alive then close the connection by deassigning the channel.
*/

void CloseConnection (struct ConnectionStruct *cxptr)

{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "CloseConnection() %d\n", cxptr->Channel);

   sys$gettim (&cxptr->StatPtr->EndBinTime);

   if (!DoSilently)
      if (!DoQuietly && HeartBeatRes && (!(ConnectionCount % HeartBeatRes)))
         fprintf (stdout, "Completed %d requests\n", ConnectionCount);

   TotalTransfered += cxptr->RxCount;
   cxptr->StatPtr->RxCount += cxptr->RxCount;
   HtmlTransfered += cxptr->RxCount - cxptr->ResponseHeaderCount;

   if (OutstandingCount) OutstandingCount--;

   if (cxptr->KeepAlive)
   {
      KeepAliveRequestCount++;
      return;
   }

   status = sys$dassgn (cxptr->Channel);
   if (Debug) fprintf (stdout, "sys$dassgn() %%X%08.08X\n", status);
   cxptr->Channel = 0;
}

/*****************************************************************************/
/*
Prase the CLI specified URL into it's host-name (and resolved address), port
and path.
*/
 
void ParseURL ()

{
   int  status;
   char  *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   cptr = SpecifiedPathPtr;
   if (strsame (cptr, "http://", 7))
   {
      cptr += 7;
      sptr = ServerHost;
      while (*cptr && *cptr != ':' && *cptr != '/') *sptr++ = *cptr++;
      *sptr = '\0';
      if (*cptr == ':')
      {
         cptr++;
         if (isdigit(*cptr)) ServerPort = atoi(cptr);
         while (*cptr && isdigit(*cptr)) cptr++;
      }
      SpecifiedPathPtr = cptr;
   }
   if (!ServerHost[0])
   {
      /* assume the local host is the server */
      if (gethostname (ServerHost, sizeof(ServerHost)))
         status = SS$_NOSUCHNODE;
      if (VMSnok (status))
      {
         fprintf (stdout, "%%%s-E-SERVER, \"%s\" %s\n",
                  Utility, ServerHost, ErrorMessage(status));
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
      if (Debug) fprintf (stdout, "ServerHost |%s|\n", ServerHost);
   }
   if (!ServerPort) ServerPort = 80;
   if (Debug)
      fprintf (stdout, "|%s|%d|%s|\n",
               ServerHost, ServerPort, SpecifiedPathPtr);
   ServerHostPtr = ServerHost;

   ServerHostEntryPtr = gethostbyname (cptr = ServerHost);
   if (!ServerHostEntryPtr)
   {
      status = vaxc$errno;
      fprintf (stdout, "%%%s-E-SERVER, \"%s\"\n", Utility, cptr);
      exit (status);
   }
   ServerSocketName.sin_family = ServerHostEntryPtr->h_addrtype;
   ServerSocketName.sin_port = htons (ServerPort);
   ServerSocketName.sin_addr = *((struct in_addr *)ServerHostEntryPtr->h_addr);

   strcpy (cptr, ServerHostEntryPtr->h_name);
   if (Debug) fprintf (stdout, "server/proxy |%s|\n", cptr);
   /* looks better if its all in lower case (host name is sometimes upper) */
   for (/* above */; *cptr; cptr++) *cptr = tolower(*cptr);
}
 
/*****************************************************************************/
/*
Report the statistics.  This is based on the format and math used with Apache
Bench.  Float is used extensively with this WASD version, to improve resolution
and to reduce computation issues.
*/
 
void ReportTotals ()

{
   int  cnt, status,
        MilliSeconds,
        Non2xxResponseCount;
   unsigned long  ConnectBinTime [2],
                  ElapsedBinTime [2],
                  ProcessingBinTime [2],
                  WaitBinTime [2],
                  TotalBinTime [2];
   float  ConnectStdDev = 0.0,
          ConnectTimeMin = 99999.0,
          ConnectTimeMax = 0.0,
          ConnectTimeMedian = 0.0,
          ConnectTimeTotal = 0.0,
          ElapsedTime = 0.0,
          KbytesPerSecond = 0.0,
          ProcessingStdDev = 0.0,
          ProcessingTimeMax = 0.0,
          ProcessingTimeMedian = 0.0,
          ProcessingTimeMin = 99999.0,
          ProcessingTimeTotal = 0.0,
          RequestsPerSecond = 0.0,
          TimePerRequestConc = 0.0,
          TimePerRequestMean = 0.0,
          TotalStdDev = 0.0,
          TotalTimeMax = 0.0,
          TotalTimeMedian = 0.0,
          TotalTimeMin = 99999.0,
          TotalTimeTotal = 0.0,
          WaitStdDev = 0.0,
          WaitTimeMax = 0.0,
          WaitTimeMedian = 0.0,
          WaitTimeMin = 99999.0,
          WaitTimeTotal = 0.0;
   struct StatStruct  *sdptr;

   /*********/
   /* begin */
   /*********/

   if (DoSilently) return;

   sys$gettim (&EndBinTime);

   status = lib$sub_times (&EndBinTime, &StartBinTime, &ElapsedBinTime);
   if (VMSnok (status)) exit (status);

   status = lib$cvtf_from_internal_time (&CvtTimeFloatSeconds,
                                         &ElapsedTime,
                                         &ElapsedBinTime);
   if (VMSnok (status)) exit (status);

   if (ElapsedTime > 0.0)
   {
      KbytesPerSecond = ((float)(TotalTransfered + TotalBytesSent) /
                         ElapsedTime / 1024);

      RequestsPerSecond = (float)ConnectionCount / ElapsedTime;
   }
   else
      KbytesPerSecond = RequestsPerSecond = 0.0;

   if (ConnectionCount > 0)
      TimePerRequestConc =
         ((float)CliConcurrency * ElapsedTime / (float)ConnectionCount)
             * 1000.0;
   else
      TimePerRequestConc = 0.0;

   if (ConnectionCount > 0)
      TimePerRequestMean = (ElapsedTime / (float)ConnectionCount) * 1000.0;
   else
      TimePerRequestMean = 0.0;

   Non2xxResponseCount = StatusCodeCount[0] +
                         StatusCodeCount[1] +
                         StatusCodeCount[3] +
                         StatusCodeCount[4] +
                         StatusCodeCount[5];

   MilliSeconds = (int)(ElapsedTime * 1000.0);

   fprintf (stdout,
"Server Software:        %s\n\
Server Hostname:        %s\n\
Server Port:            %d\n\
\n\
Document Path:          %s\n\
Document Length:        %d bytes\n\
\n\
Concurrency Level:      %d\n\
Time taken for tests:   %.3f seconds\n\
Complete requests:      %d\n\
Failed requests:        %d\n\
Broken pipe errors:     %d\n",
   ResponseServerSoftware,
   ServerHost, ServerPort,
   SpecifiedPathPtr,
   ResponseDocumentLength,
   CliConcurrency,
   ElapsedTime,
   ConnectionCount,
   FailedRequestCount,
   BrokenPipeCount);

   if (DoExercise)
   {
      fprintf (stdout,
"GET requests:           %d (%d%%)\n\
HEAD requests:          %d (%d%%)\n\
Request breaks:         %d (%d%%)\n\
Response breaks:        %d (%d%%)\n",
         GetMethodCount, GetMethodCount * 100 / ConnectionCount,
         HeadMethodCount, HeadMethodCount * 100 / ConnectionCount,
         BreakRequestCount, BreakRequestCount * 100 / ConnectionCount,
         BreakResponseCount, BreakResponseCount * 100 / ConnectionCount);
   }

   if (Non2xxResponseCount)
      fprintf (stdout,
"Non-2xx responses:      %d\n", Non2xxResponseCount);

   if (KeepAliveRequestCount)
      fprintf (stdout,
"Keep-Alive requests:    %d\n", KeepAliveRequestCount);

   fprintf (stdout,
"Response count:         1nn:%d 2nn:%d 3nn:%d 4nn:%d 5nn:%d err:%d\n\
Total transferred:      %d bytes\n\
HTML transferred:       %d bytes\n\
Requests per second:    %.2f [#/sec] (mean)\n\
Time per request:       %.2f [mS] (mean)\n\
Time per request:       %.2f [mS] (mean, across all concurrent requests)\n\
Transfer rate:          %.3f [kBytes/S] received\n\
",
   StatusCodeCount[1], StatusCodeCount[2], StatusCodeCount[3],
   StatusCodeCount[4], StatusCodeCount[5], StatusCodeCount[0],
   TotalTransfered,
   HtmlTransfered,
   RequestsPerSecond,
   TimePerRequestConc,
   TimePerRequestMean,
   KbytesPerSecond);

   if (DoExercise) return;

   /********************/
   /* connection times */
   /********************/

   if (!(NoConfidence && NoPercentiles))
   {
      for (cnt = 0; cnt < ConnectionCount; cnt++)
      {
         sdptr = &StatDataPtr[cnt];

         if (!sdptr->EndBinTime[0]) exit (SS$_BUGCHECK);
         if (!sdptr->ConnectBinTime[0])
            memcpy (&sdptr->ConnectBinTime, &sdptr->EndBinTime, 8);
         if (!sdptr->EndWriteBinTime[0])
            memcpy (&sdptr->EndWriteBinTime, &sdptr->EndBinTime, 8);
         if (!sdptr->BeginReadBinTime[0])
            memcpy (&sdptr->BeginReadBinTime, &sdptr->EndBinTime, 8);

         status = lib$sub_times (&sdptr->ConnectBinTime,
                                 &sdptr->StartBinTime,
                                 &ConnectBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$sub_times (&sdptr->BeginReadBinTime,
                                 &sdptr->EndWriteBinTime,
                                 &WaitBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$sub_times (&sdptr->EndBinTime,
                                 &sdptr->ConnectBinTime,
                                 &ProcessingBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$sub_times (&sdptr->EndBinTime,
                                 &sdptr->StartBinTime,
                                 &TotalBinTime);
         if (VMSnok (status)) exit (status);

         status = lib$cvtf_from_internal_time (&CvtTimeFloatSeconds,
                                               &sdptr->ConnectTime,
                                               &ConnectBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$cvtf_from_internal_time (&CvtTimeFloatSeconds,
                                               &sdptr->WaitTime,
                                               &WaitBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$cvtf_from_internal_time (&CvtTimeFloatSeconds,
                                               &sdptr->ProcessingTime,
                                               &ProcessingBinTime);
         if (VMSnok (status)) exit (status);
         status = lib$cvtf_from_internal_time (&CvtTimeFloatSeconds,
                                               &sdptr->TotalTime,
                                               &TotalBinTime);
         if (VMSnok (status)) exit (status);

         sdptr->ConnectTime *= 1000.0;
         sdptr->WaitTime *= 1000.0;
         sdptr->ProcessingTime *= 1000.0;
         sdptr->TotalTime *= 1000.0;

         ConnectTimeMin = MINOF (ConnectTimeMin, sdptr->ConnectTime);
         WaitTimeMin = MINOF (WaitTimeMin, sdptr->WaitTime);
         ProcessingTimeMin = MINOF (ProcessingTimeMin, sdptr->ProcessingTime);
         TotalTimeMin = MINOF (TotalTimeMin, sdptr->TotalTime);

         ConnectTimeMax = MAXOF (ConnectTimeMax, sdptr->ConnectTime);
         WaitTimeMax = MAXOF (WaitTimeMax, sdptr->WaitTime);
         ProcessingTimeMax = MAXOF (ProcessingTimeMax, sdptr->ProcessingTime);
         TotalTimeMax = MAXOF (TotalTimeMax, sdptr->TotalTime);

         ConnectTimeTotal += sdptr->ConnectTime;
         WaitTimeTotal += sdptr->WaitTime;
         ProcessingTimeTotal += sdptr->ProcessingTime;
         TotalTimeTotal += sdptr->TotalTime;
      }

      ConnectTimeTotal /= (float)ConnectionCount;
      WaitTimeTotal /= (float)ConnectionCount;
      TotalTimeTotal /= (float)ConnectionCount;
      ProcessingTimeTotal /= (float)ConnectionCount;

      for (cnt = 0; cnt < ConnectionCount; cnt++)
      {
         float  fs;
         sdptr = &StatDataPtr[cnt];
         fs = sdptr->ConnectTime - ConnectTimeTotal;
         ConnectStdDev += fs * fs;
         fs = sdptr->WaitTime - WaitTimeTotal;
         WaitStdDev += fs * fs;
         fs = sdptr->ProcessingTime - ProcessingTimeTotal;
         ProcessingStdDev += fs * fs;
         fs = sdptr->TotalTime - TotalTimeTotal;
         TotalStdDev += fs * fs;
      }

      if (ConnectionCount)
      {
         ConnectStdDev = sqrt(ConnectStdDev / (ConnectionCount - 1));
         WaitStdDev = sqrt(WaitStdDev / (ConnectionCount - 1));
         ProcessingStdDev = sqrt(ProcessingStdDev / (ConnectionCount - 1));
         TotalStdDev = sqrt(TotalStdDev / (ConnectionCount - 1));
      }
      else
         ConnectStdDev = WaitStdDev = ProcessingStdDev = TotalStdDev = 0.0;

      if (DebugStats) fprintf (stdout, "Connect ...\n");
      qsort (StatDataPtr, ConnectionCount, sizeof(struct StatStruct),
             (int(*)(const void*, const void*))CompareConnect);
      if (DebugStats) DebugStatsData ();
      if ((ConnectionCount > 1) && (ConnectionCount % 2))
         ConnectTimeMedian =
            (StatDataPtr[ConnectionCount / 2].ConnectTime +
             StatDataPtr[ConnectionCount / 2 + 1].ConnectTime) / 2.0;
      else
         ConnectTimeMedian =
            StatDataPtr[ConnectionCount / 2].ConnectTime;
      if (DebugStats) fprintf (stdout, "%7.1f\n", ConnectTimeMedian);

      if (DebugStats) fprintf (stdout, "Response (wait) ...\n");
      qsort (StatDataPtr, ConnectionCount, sizeof(struct StatStruct),
             (int(*)(const void*, const void*))CompareResponse);
      if (DebugStats) DebugStatsData ();
      if ((ConnectionCount > 1) && (ConnectionCount % 2))
         WaitTimeMedian =
            (StatDataPtr[ConnectionCount / 2].WaitTime +
             StatDataPtr[ConnectionCount / 2 + 1].WaitTime) / 2.0;
      else
         WaitTimeMedian =
            StatDataPtr[ConnectionCount / 2].WaitTime;
      if (DebugStats) fprintf (stdout, "%7.1f\n", WaitTimeMedian);

      if (DebugStats) fprintf (stdout, "Processing ...\n");
      qsort (StatDataPtr, ConnectionCount, sizeof(struct StatStruct),
             (int(*)(const void*, const void*))CompareProcessing);
      if (DebugStats) DebugStatsData ();
      if ((ConnectionCount > 1) && (ConnectionCount % 2))
         ProcessingTimeMedian =
            (StatDataPtr[ConnectionCount / 2].ProcessingTime +
             StatDataPtr[ConnectionCount / 2 + 1].ProcessingTime) / 2.0;
      else
         ProcessingTimeMedian =
            StatDataPtr[ConnectionCount / 2].ProcessingTime;
      if (DebugStats) fprintf (stdout, "%7.1f\n", ProcessingTimeMedian);

      if (DebugStats) fprintf (stdout, "Total ...\n");
      qsort (StatDataPtr, ConnectionCount, sizeof(struct StatStruct),
             (int(*)(const void*, const void*))CompareTotal);
      if (DebugStats) DebugStatsData ();
      if ((ConnectionCount > 1) && (ConnectionCount % 2))
         TotalTimeMedian =
            (StatDataPtr[ConnectionCount / 2].TotalTime +
             StatDataPtr[ConnectionCount / 2 + 1].TotalTime) / 2.0;
      else
         TotalTimeMedian =
            StatDataPtr[ConnectionCount / 2].TotalTime;
      if (DebugStats) fprintf (stdout, "%7.1f\n", TotalTimeMedian);

      if (!NoConfidence)
      {
         fprintf (stdout,
"\n\
Connection Times (mS)\n\
                min    mean [+/-sd]   median     max\n\
Connect:    %7.1f %7.1f %7.1f  %7.1f %7.1f\n\
Processing: %7.1f %7.1f %7.1f  %7.1f %7.1f\n\
Waiting:    %7.1f %7.1f %7.1f  %7.1f %7.1f\n\
Total:      %7.1f %7.1f %7.1f  %7.1f %7.1f\n",
         ConnectTimeMin, ConnectTimeTotal, ConnectStdDev,
            ConnectTimeMedian, ConnectTimeMax,
         ProcessingTimeMin, ProcessingTimeTotal, ProcessingStdDev,
            ProcessingTimeMedian, ProcessingTimeMax,
         WaitTimeMin, WaitTimeTotal, WaitStdDev,
            WaitTimeMedian, WaitTimeMax,
         TotalTimeMin, TotalTimeTotal, TotalStdDev,
            TotalTimeMedian, TotalTimeMax);

#define SANE(what,avg,mean,sd) \
         { \
            double db = avg - mean; \
            if (db < 0) db = -db; \
            if (db > 2 * sd ) \
               fprintf (stdout, \
"ERROR: The median and mean for " what " are more than twice the\n\
       standard deviation apart. These results are NOT reliable.\n"); \
            else \
            if (db > sd ) \
               fprintf (stdout, \
"WARNING: The median and mean for " what " are not within a normal\n\
         deviation.  These results are propably not that reliable.\n"); \
         }
         SANE ("the initial connection time",
               ConnectTimeTotal, ConnectTimeMedian, ConnectStdDev);
         SANE ("the processing time",
               ProcessingTimeTotal, ProcessingTimeMedian, ProcessingStdDev);
         SANE ("the waiting time",
               WaitTimeTotal, WaitTimeMedian, WaitStdDev);
         SANE ("the total time",
               TotalTimeTotal, TotalTimeMedian, TotalStdDev);
#undef SANE
      }

      if (!NoPercentiles)
      {
         /* final sort was by total times */
         if (ConnectionCount > 1)
         {
            fprintf (stdout,
"\nPercentage of the requests served within a certain time (mS)\n\
  50%%  %7.1f\n\
  66%%  %7.1f\n\
  75%%  %7.1f\n\
  80%%  %7.1f\n\
  90%%  %7.1f\n\
  95%%  %7.1f\n\
  98%%  %7.1f\n\
  99%%  %7.1f\n\
 100%%  %7.1f\n",
            StatDataPtr[(int)((float)ConnectionCount * 0.50)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.66)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.75)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.80)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.90)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.95)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.98)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount * 0.99)].TotalTime,
            StatDataPtr[(int)((float)ConnectionCount - 1)].TotalTime);
         }
      }
   }
}
 
/*****************************************************************************/
/*
Used by qsort() in ReportTotals().
*/

int CompareConnect
(
struct StatStruct *a,
struct StatStruct *b
)
{
   if ((a->ConnectTime) < (b->ConnectTime)) return (-1);
   if ((a->ConnectTime) > (b->ConnectTime)) return (1);
   return (0);
}

/*****************************************************************************/
/*
Used by qsort() in ReportTotals().
*/

int CompareResponse
(
struct StatStruct *a,
struct StatStruct *b
)
{
   if ((a->WaitTime) < (b->WaitTime)) return (-1);
   if ((a->WaitTime) > (b->WaitTime)) return (1);
   return (0);
}

/*****************************************************************************/
/*
Used by qsort() in ReportTotals().
*/

int CompareProcessing
(
struct StatStruct *a,
struct StatStruct *b
)
{
   if ((a->ProcessingTime) < (b->ProcessingTime)) return (-1);
   if ((a->ProcessingTime) > (b->ProcessingTime)) return (1);
   return (0);
}

/*****************************************************************************/
/*
Used by qsort() in ReportTotals().
*/

int CompareTotal
(
struct StatStruct *a,
struct StatStruct *b
)
{
   if ((a->TotalTime) < (b->TotalTime)) return (-1);
   if ((a->TotalTime) > (b->TotalTime)) return (1);
   return (0);
}

/*****************************************************************************/
/*
*/

void DebugStatsData ()

{
   int  idx;
   struct StatStruct  *sdptr;

   fprintf (stdout, "             Connect    Wait Process   Total\n");
   for (idx = 0; idx < ConnectionCount; idx++)
   {
      sdptr = &StatDataPtr[idx];
      fprintf (stdout, "[%4d] %4d  %7.1f %7.1f %7.1f %7.1f  %d\n",
               idx , sdptr->ConnectionNumber,
               sdptr->ConnectTime, sdptr->WaitTime,
               sdptr->ProcessingTime, sdptr->TotalTime,
               sdptr->RxCount);
   }
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  This function has
been customized for this utility to allow it to be called multiple times during
the one use.
*/

void GetParameters
(
int argc,
char *argv[]
)
{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  acnt, status;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetParameters() %d\n", argc);

   /**************/
   /* initialize */
   /**************/

   ReadBufferSize = DEFAULT_READ_BUFFER_SIZE;
   CliNumber = DEFAULT_NUMBER;
   ServerPort = DEFAULT_SERVER_PORT;
   CliConcurrency = DEFAULT_CONCURRENCY;

   CliProxyServerHostPtr =
      SpecifiedPathPtr = "";
   
   if (argc <= 1) return;

   if (argv[1][0] == '-' || argv[1][0] == '+')
   {
      /*************/
      /* U**x-like */
      /*************/

      acnt = 1;
      while (acnt < argc)
      {
         if (Debug) fprintf (stdout, "%d |%s|\n", acnt, argv[acnt]);

         if (*(unsigned short*)argv[acnt] == '-c')
         {
            if (acnt++ < argc)
               CliConcurrency = atoi(argv[acnt]);
         }
         else
         if (*(unsigned short*)argv[acnt] == '+c')
         {
            if (acnt++ < argc)
               CliAcceptCharsetPtr = argv[acnt];
         }
         else
         if (*(unsigned short*)argv[acnt] == '-d')
            NoPercentiles = true;
         else
         if (*(unsigned short*)argv[acnt] == '-D')
            NoNoDelaysAtAll = true;
         else
         if (*(unsigned short*)argv[acnt] == '-n')
         {
            if (acnt++ < argc)
               CliNumber = atoi(argv[acnt]);
         }
         else
         if (*(unsigned short*)argv[acnt] == '-h')
            DoShowHelp = true;
         else
         if (*(unsigned short*)argv[acnt] == '-k')
            DoKeepAlive = true;
         else
         if (*(unsigned short*)argv[acnt] == '+l')
         {
            if (acnt++ < argc)
               CliAcceptLanguagePtr = argv[acnt];
         }
         else
         if (*(unsigned short*)argv[acnt] == '-q')
            DoQuietly = true;
         else
         if (*(unsigned short*)argv[acnt] == '-S')
            NoConfidence = true;
         else
         if (*(unsigned short*)argv[acnt] == '-V')
            DoShowVersion = true;
         else
         if (*(unsigned short*)argv[acnt] == '+d')
            Debug = DebugStats = true;
         else
         if (*(unsigned short*)argv[acnt] == '+D')
            DebugStats = true;
         else
         if (*(unsigned short*)argv[acnt] == '+e')
            DoExercise = true;
         else
         if (*(unsigned short*)argv[acnt] == '+h')
            NoHost = true;
         else
         if (*(unsigned short*)argv[acnt] == '+n')
            NoDelayAck = true;
         else
         if (*(unsigned short*)argv[acnt] == '+N')
            NoDelayNagle = true;
         else
         if (*(unsigned short*)argv[acnt] == '+s')
            DoSilently = true;
         else
         if (argv[acnt][0] == '-' || argv[acnt][0] == '+')
         {
            fprintf (stdout, "%%%s-E-IVQUAL, unrecognized switch\n \\%s\\\n",
                     Utility, argv[acnt]);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         else
         if (!SpecifiedPathPtr[0])
            SpecifiedPathPtr = argv[acnt];
         else
         {
            fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
                     Utility, argv[acnt]);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         if (acnt >= argc)
         {
            fprintf (stdout, "%%%s-W-VALREQ, missing qualifier or keyword\n",
                     Utility);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         acnt++;
      }

      return;
   }

   /************/
   /* DCL-like */
   /************/

   /* get the entire command line following the verb */
   if (VMSnok (status =
       lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
      exit (status);
   (clptr = CommandLine)[Length] = '\0';
   if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      /**************/
      /* parameters */
      /**************/

      if (strsame (aptr, "/BUFFER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ReadBufferSize = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliAcceptCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CONCURRENCY=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliConcurrency = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = DebugStats = true;
         continue;
      }
      if (strsame (aptr, "/DBUGSTATS", -1))
      {
         DebugStats = true;
         continue;
      }
      if (strsame (aptr, "/DELAYS", 6))
      {
         NoNoDelaysAtAll = true;
         continue;
      }
      if (strsame (aptr, "/EXERCISE", 4))
      {
         DoExercise = true;
         continue;
      }
      if (strsame (aptr, "/HEAD", 4))
      {
         DoHeadMethod = true;
         continue;
      }
      if (strsame (aptr, "/HELP", 4))
      {
         DoShowHelp = true;
         continue;
      }
      if (strsame (aptr, "/KEEPALIVE", 4))
      {
         DoKeepAlive = true;
         continue;
      }
      if (strsame (aptr, "/LANGUAGE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliAcceptLanguagePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/NOCONFIDENCE", 6))
      {
         NoConfidence = true;
         continue;
      }
      if (strsame (aptr, "/NODELACK", 6))
      {
         NoDelayAck = true;
         continue;
      }
      if (strsame (aptr, "/NONAGLE", 6))
      {
         NoDelayNagle = true;
         continue;
      }
      if (strsame (aptr, "/NOHOST", 6))
      {
         NoHost = true;
         continue;
      }
      if (strsame (aptr, "/NOPERCENTILES", 6))
      {
         NoPercentiles = true;
         continue;
      }
      if (strsame (aptr, "/NUMBER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliNumber = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/QUIETLY", 4))
      {
         DoQuietly = true;
         continue;
      }
      if (strsame (aptr, "/SILENTLY", 4))
      {
         DoSilently = true;
         continue;
      }
      if (strsame (aptr, "/VERSION", 4))
      {
         DoShowVersion = true;
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!SpecifiedPathPtr[0])
      {
         SpecifiedPathPtr = aptr;
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/*****************************************************************************/
/*
*/
 
void ShowHelp ()
 
{
   /*********/
   /* begin */
   /*********/

   fprintf (stdout,
"%%%s-I-HELP, usage for the WASD Bench :^) utility (%s)\n\
\n\
WASD Bench - an analogue to Apache Bench (AB).\n\
Why have it?  Well, Apache Bench only compiles and runs on VMS 7.n and later.\n\
This version should compile and run for all supported WASD configurations.\n\
It also has the significant performance advantage (looks like ~25%%) of using\n\
the underlying $QIO services and not the socket API, and is AST event driven\n\
rather than using the likes of select().  It is not a full implementation.\n\
\n\
$ WB [qualifiers ...] URL\n\
$ WB [switches ...] URL\n\
\n\
/CHARSET=<string> /CONCURRENCY=<integer> /DELAYS /EXERCISE /KEEPALIVE\n\
/LANGUAGE=<string> /NUMBER=<integer> /NOCONFIDENCE /NONAGLE /NODELACK\n\
/NOPERCENTILES /QUIETLY /SILENTLY /VERSION\n\
+c <string> -c <integer> -D +e -k\n\
+l <string> -n <integer> -S +N +n\n\
-d -q +s -v\n\
\n\
Usage examples:\n\
  $ WB /CONCURRENT=10 /NUMBER=100 http://the.host.name/the/path\n\
  $ WB -c 10 -n 100 http://the.host.name/the/path\n\
\n",
   Utility, SOFTWAREID);
 
   exit (SS$_NORMAL);
}
 
/*****************************************************************************/
/*
*/

char* ErrorMessage (int VmsStatus)

{
   static char  Message [256];

   int  status;
   short int  Length;
   $DESCRIPTOR (MessageDsc, Message);

   /*********/
   /* begin */
   /*********/

   Message[0] = '\0';
   if (VmsStatus)
   {
      /* text component only */
      sys$getmsg (VmsStatus, &Length, &MessageDsc, 1, 0);
      Message[Length] = '\0';
   }
   if (Message[0])
      return (Message);
   else
      return ("(internal error)");
}
 
/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 
 
boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}
 
/****************************************************************************/
