-- File: EchoTool.mesa - last edit:
-- AOF                  4-Feb-88 13:05:54
-- WIrish               5-Feb-88 12:04:01
-- HGM                  5-Sep-85 18:23:20
-- Copyright (C) 1984, 1985, 1988 by Xerox Corporation. All rights reserved. 

DIRECTORY
  AddressTranslation USING [Error, PrintError, StringToNetworkAddress],
  Buffer USING [],
  Driver USING [Device],
  Format USING [HostNumber, NetworkAddress, NetworkNumber, StringProc],
  FormSW USING [
    AllocateItemDescriptor, BooleanItem, ClientItemsProcType, CommandItem,
    ItemHandle, newLine, NumberItem, ProcType,
    StringItem],
  Heap USING [systemZone],
  Inline USING [BITAND, BITNOT],
  NSBuffer USING [Body, Buffer],
  NSConstants USING [echoerSocket],
  NSTypes USING [maxIDPDataWords],
  Process USING [Detach, Pause, Yield],
  Put USING [Char, CR, Line, Text],
  Runtime USING [GetBcdTime],
  Socket USING [
    ChannelHandle, Create, Delete,
    GetPacket, GetPacketBytes, GetSendBuffer, GetSource, PutPacket,
    ReturnBuffer, SetDestination, SetPacketWords, SetWaitTime, TimeOut],
  String USING [AppendNumber, AppendLongNumber, AppendString],
  SpecialSystem USING [GetProcessorID],
  System USING [
    GetClockPulses, GetGreenwichMeanTime, GreenwichMeanTime, HostNumber,
    NetworkAddress, NetworkNumber, nullSocketNumber,
    Pulses, PulsesToMicroseconds, SocketNumber],
  Time USING [Append, AppendCurrent, Unpack],
  Tool USING [
    Create, UnusedLogName, MakeFormSW, MakeFileSW, MakeSWsProc],
  ToolWindow USING [TransitionProcType],
  Unformat USING [Error, NetworkAddress],
  UserInput USING [UserAbort],
  Window USING [Handle];

EchoTool: PROGRAM
  IMPORTS
    Format, FormSW, Heap, Inline, Process, Put, Runtime,
    SpecialSystem, String, System, Time, Tool, UserInput, Unformat,
    AddressTranslation, Socket
  EXPORTS Buffer =
  BEGIN

  Device: PUBLIC TYPE = Driver.Device;

  z: UNCOUNTED ZONE = Heap.systemZone;

  -- global variable declarations
  log, form: Window.Handle ← NIL;
  thisMachineID, remoteAddress: LONG STRING ← NIL;
  maxLength: CARDINAL ← NSTypes.maxIDPDataWords;
  maxClumpSize: CARDINAL = 50;
  clumpSize: CARDINAL ← 50;
  running, pleaseStop: BOOLEAN ← FALSE;
  fixedLength: BOOLEAN ← FALSE;
  noBang, noLate, noLost: BOOLEAN ← FALSE;
  checkIt: BOOLEAN ← TRUE;
  timeout: CARDINAL ← 3000;  -- ms

  Init: PROCEDURE =
    BEGIN
    herald: LONG STRING = [100];
    String.AppendString[herald, "Echo Tool of "L];
    Time.Append[herald, Time.Unpack[Runtime.GetBcdTime[]]];
    [] ← Tool.Create[
      name: herald, makeSWsProc: MakeThisTool, clientTransition: Transition];
    END;

  MakeThisTool: Tool.MakeSWsProc =
    BEGIN
    logFileName: STRING = [40];
    form ← Tool.MakeFormSW[window: window, formProc: MakeItemArray];
    Tool.UnusedLogName[logFileName, "EchoTool.log$"L];
    log ← Tool.MakeFileSW[window: window, name: logFileName, allowTypeIn: FALSE];
    END;

  MakeItemArray: FormSW.ClientItemsProcType =
    BEGIN
    nItems: CARDINAL = 15;
    i: INTEGER ← -1;
    items ← FormSW.AllocateItemDescriptor[nItems];

    items[i ← i + 1] ← FormSW.CommandItem[tag: "Stop"L, proc: Stop, place: FormSW.newLine];
    items[i ← i + 1] ← FormSW.CommandItem[tag: "PokeOnce"L, proc: PokeOnce];
    items[i ← i + 1] ← FormSW.CommandItem[tag: "Echo"L, proc: Echo];
    items[i ← i + 1] ← FormSW.CommandItem[tag: "Clumps"L, proc: Clumps];
    items[i ← i + 1] ← FormSW.BooleanItem[tag: "FixedLength"L, switch: @fixedLength];
    items[i ← i + 1] ← FormSW.NumberItem[tag: "(Max)Length"L, value: @maxLength, boxWidth: 25];
    items[i ← i + 1] ← FormSW.NumberItem[tag: "clumpSize"L, value: @clumpSize];
    items[i ← i + 1] ← FormSW.CommandItem[tag: "Stats"L, proc: Stats, place: FormSW.newLine];
    items[i ← i + 1] ← FormSW.StringItem[
      tag: "ThisMachineID"L, string: @thisMachineID, readOnly: TRUE, boxWidth: 100];
    items[i ← i + 1] ← FormSW.StringItem[
      tag: "RemoteAddress"L, string: @remoteAddress, inHeap: TRUE, boxWidth: 150];
    items[i ← i + 1] ← FormSW.BooleanItem[
      tag: "CheckIt"L, switch: @checkIt, place: FormSW.newLine];
    items[i ← i + 1] ← FormSW.BooleanItem[tag: "!-Off"L, switch: @noBang];
    items[i ← i + 1] ← FormSW.BooleanItem[tag: "#-Off"L, switch: @noLate];
    items[i ← i + 1] ← FormSW.BooleanItem[tag: "?-Off"L, switch: @noLost];
    items[i ← i + 1] ← FormSW.NumberItem[tag: "Timeout(ms)"L, value: @timeout];
    IF (i + 1) # nItems THEN ERROR;
    RETURN[items, TRUE];
    END;

  Transition: ToolWindow.TransitionProcType =
    BEGIN
    SELECT TRUE FROM
      old = inactive =>
        BEGIN
        AppendMe: PROCEDURE [s: LONG STRING, clientData: LONG POINTER] =
          BEGIN String.AppendString[thisMachineID, s]; END;
        checkIt ← TRUE;
        noBang ← noLate ← noLost ← FALSE;
        thisMachineID ← z.NEW[StringBody[50]];
        Format.HostNumber[AppendMe, LOOPHOLE[SpecialSystem.GetProcessorID[]], productSoftware];
        remoteAddress ← z.NEW[StringBody[50]];
        END;
      new = inactive =>
        BEGIN
        z.FREE[@thisMachineID];
        z.FREE[@remoteAddress];
        END;
      ENDCASE;
    END;

  Stop: FormSW.ProcType =
    BEGIN StopIt[]; END;

  StopIt: PROCEDURE =
    BEGIN pleaseStop ← TRUE; WHILE running DO Process.Pause[1]; ENDLOOP; END;

  Stats: FormSW.ProcType = {};
<<
    BEGIN
    firstDevice: Driver.Device ← Driver.GetDeviceChain[];
    FOR network: Driver.Device ← firstDevice, network.next UNTIL network = NIL DO
      IF UserInput.UserAbort[log] THEN EXIT;
      SELECT network.device FROM
        ethernet, ethernetOne =>
	  BEGIN
          pup: PupRouterDefs.NetworkContext ← Protocol1.GetContext[network, pup];
          ns: RoutingTable.NetworkContext ← Protocol1.GetContext[network, ns];
          stats: LONG POINTER TO EthernetDriverFriends.EtherStatsInfo ← network.stats;
	  Put.Text[log, "Ethernet"L];
	  IF network.device = ethernetOne THEN Put.Text[log, "One"L];
	  Put.Text[log, " Statistics for "L];
	  IF pup # NIL THEN {
	    Put.Text[log, " Pup "L];
	    WriteNumber[pup.pupNetNumber, 8, 0];
	    Put.Text[log, "#"L];
	    WriteNumber[pup.pupHostNumber, 8, 0];
	    Put.Text[log, "#, NS net "L]; };
	  IF ns # NIL THEN WriteNetNumbers[ns.netNumber];
	  Put.Line[log, "."L];
	  IF stats = NIL THEN
	    BEGIN
	    Put.Line[log, "*** NO DRIVER STATS ***"L];
	    LOOP;
	    END;
	  Put.Text[log, "  Rcv: pkts "L];
	  Put.LongDecimal[log, stats.packetsRecv];
	  Put.Text[log, ", words "L];
	  Put.LongDecimal[log, stats.wordsRecv];
	  Put.Text[log, ", bad "L];
	  Put.LongDecimal[log, stats.badRecvStatus];
	  Put.Text[log, ", missed "L];
	  Put.LongDecimal[log, stats.packetsMissed];
	  IF stats.idleInput # 0 THEN
            BEGIN
            Put.Text[log, ", idle "L];
            Put.LongDecimal[log, stats.idleInput];
            END;
	  Put.CR[log ];
	  IF stats.badRecvStatus # 0 OR stats.okButDribble # 0 THEN
            BEGIN
            Put.Text[log, "    crc "L];
            Put.LongDecimal[log, stats.badCrc];
            Put.Text[log, ", bad alignment but ok crc "L];
            Put.LongDecimal[log, stats.badAlignmentButOkCrc];
            Put.Text[log, ", crc and bad alignment "L];
            Put.LongDecimal[log, stats.crcAndBadAlignment];
            Put.CR[log ];
            Put.Text[log, "    ok but dribble "L];
            Put.LongDecimal[log, stats.okButDribble];
            Put.Text[log, ", too long "L];
            Put.LongDecimal[log, stats.packetTooLong];
            Put.Text[log, ", overrun "L];
            Put.LongDecimal[log, stats.overrun];
            Put.CR[log ];
            END;
	  Put.Text[log, "  Xmit: pkts "L];
	  Put.LongDecimal[log, stats.packetsSent];
	  Put.Text[log, ", words "L];
	  Put.LongDecimal[log, stats.wordsSent];
	  Put.Text[log, ", bad "L];
	  Put.LongDecimal[log, stats.badSendStatus];
	  Put.CR[log ];
	  IF stats.stuckOutput # 0 OR stats.badSendStatus # 0
	    OR stats.tooManyCollisions # 0 THEN
            BEGIN
            Put.Text[log, "    underrun "L];
            Put.LongDecimal[log, stats.underrun];
            Put.Text[log, ", stuck "L];
            Put.LongDecimal[log, stats.stuckOutput];
            Put.Text[log, ", too many collisions "L];
            Put.LongDecimal[log, stats.tooManyCollisions];
            Put.CR[log ];
            END;
	  Put.Text[log, "  Lds:"L];
	  FOR i: CARDINAL IN [0..16) DO
            Put.Char[log, ' ]; Put.LongDecimal[log, stats.loadTable[i]]; ENDLOOP;
	  Put.CR[log ];
          END;
	phonenet =>
	  BEGIN
          pup: PupRouterDefs.NetworkContext ← Protocol1.GetContext[network, pup];
          ns: RoutingTable.NetworkContext ← Protocol1.GetContext[network, ns];
          stats: PhoneNetFriends.PhoneNetInfo = PhoneNetFriends.StatsPtrToStats[network.stats];
          Put.Text[log, "PhoneNet Statistics for "L];
          IF pup # NIL THEN {
	    Put.Text[log, "Pup "L];
	    WriteNumber[pup.pupNetNumber, 8, 0];
	    Put.Text[log, "#"L];
	    WriteNumber[pup.pupHostNumber, 8, 0]; };
	  IF ns # NIL THEN {
	    Put.Text[log, "#, NS net "L];
	    WriteNetNumbers[ns.netNumber]; };
	  Put.Text[log, ", Line "L];
	  Put.LongDecimal[log, stats.lineNumber];
	  Put.Text[log, ", "L];
	  Put.LongDecimal[log, stats.speed];
	  Put.Text[log, "KB"L];
	  SELECT stats.remoteHostNumber FROM
	    System.nullHostNumber =>
	      Put.Text[log, ",  Down"L];
	    System.localHostNumber =>
	      Put.Text[log, ",  Looped"L];
	    ENDCASE => {
	      Put.CR[log ];
	      Put.Text[log, "  Up to "L];
	      WriteHostNumbers[stats.remoteHostNumber]; };
	  Put.CR[log ];
	  Put.Text[log, "  Recv: pkts "L];
	  Put.LongDecimal[log, stats.stats[pktsReceived]];
	  Put.Text[log, ", bytes "L];
	  Put.LongDecimal[log, stats.stats[bytesReceived]];
	  Put.Text[log, ", rejected "L];
	  Put.LongDecimal[log, stats.stats[pktsRejected]];
	  Put.Text[log, ", missed "L];
	  Put.LongDecimal[log, stats.stats[rcvErrorNoGet]];
	  Put.Text[log, ", idle "L];
	  Put.LongDecimal[log, stats.stats[tooLongSinceLastReceive]];
	  Put.CR[log ];
	  Put.Text[log, "    Bad crc "L];
	  Put.LongDecimal[log, stats.stats[rcvErrorCRC]];
	  Put.Text[log, ", data lost "L];
	  Put.LongDecimal[log, stats.stats[rcvErrorDataLost]];
	  Put.Text[log, ", device error "L];
	  Put.LongDecimal[log, stats.stats[rcvDeviceError]];
	  Put.Text[log, ", timeout "L];
	  Put.LongDecimal[log, stats.stats[rcvErrorFrameTimeout]];
	  Put.Text[log, ", other error "L];
	  Put.LongDecimal[log, stats.stats[rcvErrorUnknown]];
	  Put.CR[log ];
	  Put.Text[log, "  Send: pkts "L];
	  Put.LongDecimal[log, stats.stats[pktsSent]];
	  Put.Text[log, ", bytes "L];
	  Put.LongDecimal[log, stats.stats[bytesSent]];
	  Put.CR[log ];
	  Put.Text[log, "    NS "L];
	  Put.LongDecimal[log, stats.stats[nsSent]];
	  Put.Text[log, " "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.nsBytesSend]];
	  Put.Text[log, ", Pups "L];
	  Put.LongDecimal[log, stats.stats[pupSent]];
	  Put.Text[log, " "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.pupBytesSend]];
	  Put.Text[log, ", Leaf "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.leafPktsSend]];
	  Put.Text[log, " "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.leafBytesSend]];
	  Put.CR[log ];
	  IF stats.stats[sendErrorBadStatus] # 0 OR stats.stats[queueTooOld] # 0 THEN {
	    Put.Text[log, "    Bad "L];
	    Put.LongDecimal[log, stats.stats[sendErrorBadStatus]];
	    Put.Text[log, ", stuck "L];
	    Put.LongDecimal[log, stats.stats[queueTooOld]];
	    Put.CR[log ]; };
	  Put.Text[log, "    Queue too long "L];
	  Put.LongDecimal[log, stats.stats[congestion]];
	  Put.Text[log, ", NS "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.nsCongestion]];
	  Put.Text[log, ", Pup "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.pupCongestion]];
	  Put.CR[log ];
	  Put.Text[log, "    Conn too greedy "L];
	  Put.LongDecimal[log, stats.stats[connTooGreedy]];
	  Put.Text[log, ", NS "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.nsTooGreedy]];
	  Put.Text[log, ", Pup "L];
	  Put.LongDecimal[log, stats.stats[PhoneNetExtras.pupTooGreedy]];
	  Put.CR[log ];
          IF stats.stats[PhoneNetExtras.nsDupsFiltered] # 0 THEN {
	    Put.Text[log, "    NS duplicates discarded: "L];
	    Put.LongDecimal[log, stats.stats[PhoneNetExtras.nsDupsFiltered]];
	    Put.CR[log ]; };
          IF stats.stats[PhoneNetExtras.pupDupsFiltered] # 0 THEN {
	    Put.Text[log, "    BSP probes/duplicates discarded: "L];
	    Put.LongDecimal[log, stats.stats[PhoneNetExtras.pupDupsFiltered]];
	    Put.CR[log ]; };
          IF stats.stats[PhoneNetExtras.leafDupsFiltered] # 0 THEN {
	    Put.Text[log, "    Leaf duplicates discarded: "L];
	    Put.LongDecimal[log, stats.stats[PhoneNetExtras.leafDupsFiltered]];
	    Put.CR[log ]; };
	  END;
        ENDCASE => NULL;
      ENDLOOP;
    END;
>>

  WriteHostNumbers: PROCEDURE [net: System.HostNumber] =
    BEGIN
    Push: PROCEDURE [s: LONG STRING, clientData: LONG POINTER] =
      BEGIN Put.Text[log, s]; END;
    Format.HostNumber[Push, net, productSoftware];
    Put.Text[log, "="L];
    Format.HostNumber[Push, net, octal];
    END;
      
  WriteNetNumbers: PROCEDURE [net: System.NetworkNumber] =
    BEGIN
    Push: PROCEDURE [s: LONG STRING, clientData: LONG POINTER] =
      BEGIN Put.Text[log, s]; END;
    Format.NetworkNumber[Push, net, productSoftware];
    Put.Text[log, "="L];
    Format.NetworkNumber[Push, net, octal];
    END;
      
  Echo: FormSW.ProcType =
    BEGIN
    fixed: BOOLEAN;
    size: CARDINAL;
    errFlag: BOOLEAN ← FALSE;
    remoteAddr: System.NetworkAddress;
    StopIt[];
    IF fixed AND maxLength < 1 THEN
      BEGIN
      WriteLine["Need at least 2 words."L];
      RETURN;
      END;
    IF maxLength > NSTypes.maxIDPDataWords THEN
      BEGIN
      WriteLine["(max)Length is too big."L];
      RETURN;
      END;
    fixed ← fixedLength;
    size ← maxLength;
    WriteCR[];
    WriteString["Echoing to "L];
    WriteString[remoteAddress];
    WriteString[" = "L];
    remoteAddr ← GetAddress[remoteAddress, NSConstants.echoerSocket !
      Trouble =>
        BEGIN
	WriteLine[reason];
        errFlag ← TRUE;
        CONTINUE;
        END];
    IF errFlag THEN RETURN;
    WriteNetworkAddressVerbose[remoteAddr];
    WriteLine["."L];
    pleaseStop ← FALSE;
    running ← TRUE;
    Process.Detach[FORK Worker[remoteAddr, fixed, size, 1]];
    END;

  DelayRange: TYPE = {
    d1, d1a, d2, d2a, d3, d3a, d4, d4a,
    d5, d5a, d6, d6a, d7, d7a, d8, d8a, d9, d9a,
    d10, d14, d20, d28, d50, d70,
    d100, d140, d200, d280, d500, d700,
    d1000, d1400, d2000, d2800, d5000, d7000,
    d10000, d14000, d20000, d28000, more};
  delayTime: ARRAY DelayRange OF LONG CARDINAL = [
    d1: 1000, d1a: 1500, d2: 2000, d2a: 2500,
    d3: 3000, d3a: 3500, d4: 4000, d4a: 4500,
    d5: 5000, d5a: 5500, d6: 6000, d6a: 6500,
    d7: 7000, d7a: 7500, d8: 8000, d8a: 8500,
    d9: 9000, d9a: 9500,
    d10: 10000, d14: 14000, d20: 20000, d28: 28000, d50: 50000, d70: 70000,
    d100: 100000, d140: 140000, d200: 200000, d280: 280000, d500: 500000, d700: 700000,
    d1000: 1000000, d1400: 1400000, d2000: 2000000, d2800: 2800000, d5000: 5000000,  d7000: 7000000,
    d10000: 10000000, d14000: 14000000, d20000: 20000000, d28000: 28000000, more: LAST[LONG CARDINAL]];
      
  Worker: PROCEDURE [
    remoteAddr: System.NetworkAddress, fixed: BOOLEAN,
    wordsPerPacket, packetsPerClump: CARDINAL] =
    BEGIN
    
    length: CARDINAL = MAX[2, MIN[NSTypes.maxIDPDataWords, wordsPerPacket]];
    delay: ARRAY DelayRange OF LONG CARDINAL ← ALL[0];
    minDelay: LONG CARDINAL ← LAST[LONG CARDINAL];
    maxDelay: LONG CARDINAL ← FIRST[LONG CARDINAL];
    picks: ARRAY [0..16] OF CARDINAL ← ALL[0];
    drops: ARRAY [0..16] OF CARDINAL ← ALL[0];
    sent, good, missed, late, bad, horrible, trash, words: LONG CARDINAL ← 0;
    start, stop: System.Pulses;
    micro: LONG CARDINAL;
    startSec, stopSec: System.GreenwichMeanTime;

    AddToDelayHist: PROCEDURE [micro: LONG CARDINAL] =
      BEGIN
      FOR d: DelayRange IN DelayRange DO  -- Slow but clean
        IF delayTime[d] < micro THEN LOOP;
	delay[d] ← delay[d] + 1;
	EXIT;
	REPEAT FINISHED => ERROR;
	ENDLOOP;
      minDelay ← MIN[minDelay, micro];
      maxDelay ← MAX[maxDelay, micro];
      END;

    PrintDelayHist: PROCEDURE =
      BEGIN
      total: LONG CARDINAL ← 0;
      IF good = 0 THEN RETURN;
      FOR d: DelayRange IN DelayRange DO
        IF delay[d] = 0 THEN LOOP;
        total ← total + delay[d];
        WriteLongNumber[delay[d], 10, 8];
        WriteLongNumber[delay[d]*100/good, 10, 4];
	WriteString["%"L];
        WriteLongNumber[total, 10, 8];
        WriteLongNumber[total*100/good, 10, 4];
	WriteString["% packets with delay less than "L];
	WriteLongDecimal[delayTime[d]];
	WriteLine[" microseconds."L];
	ENDLOOP;
      WriteString["The min delay was "L];
      WriteLongDecimal[minDelay];
      WriteLine[" microseconds."L];
      WriteString["The max delay was "L];
      WriteLongDecimal[maxDelay];
      WriteLine[" microseconds."L];
      END;

    AddToErrorHist: PROCEDURE [
      hist: POINTER TO ARRAY [0..16] OF CARDINAL, bits: WORD] =
      BEGIN
      i: CARDINAL;
      IF bits = 0 THEN RETURN;
      SELECT bits FROM
        1 => i ← 15;
        2 => i ← 14;
        4 => i ← 13;
        10B => i ← 12;
        20B => i ← 11;
        40B => i ← 10;
        100B => i ← 9;
        200B => i ← 8;
        400B => i ← 7;
        1000B => i ← 6;
        2000B => i ← 5;
        4000B => i ← 4;
        10000B => i ← 3;
        20000B => i ← 2;
        40000B => i ← 1;
        100000B => i ← 0;
        ENDCASE => i ← 16;
      hist[i] ← hist[i] + 1;
      END;

    PrintSummary: PROCEDURE [howLong: LONG CARDINAL] =
      BEGIN
      IF howLong # 0 THEN
        BEGIN
        WriteLongNumber[sent/howLong, 10, 8];
        WriteLine[" packets per second."L];
        WriteLongNumber[16*words/howLong, 10, 8];
        WriteLine[" data bits per second."L];
        END;
      END;

    PrintErrorHist: PROCEDURE =
      BEGIN
      ShowPercent[good, sent, "good packets received."L];
      ShowPercent[missed, sent, "packets missed."L];
      ShowPercent[late, sent, "late (or??) packets received."L];
      ShowPercent[bad, sent, "bad packets received."L];
      ShowPercent[horrible, sent, "packets received with more than 10 words wrong."L];
      ShowPercent[trash, sent, "trashy packets received."L];
      IF bad # 0 THEN
        BEGIN
        x: WORD ← 100000B;
        WriteCR[];
        WriteLine["     Bit    Picked Dropped"L];
        FOR i: CARDINAL IN [0..16] DO
          IF picks[i] # 0 OR drops[i] # 0 THEN
            BEGIN
            IF i = 16 THEN WriteString[" Other"L] ELSE O6[x];
            D8[picks[i]];
            D8[drops[i]];
            WriteCR[];
            END;
          x ← x/2;
          ENDLOOP;
        END;
      END;


    soc: Socket.ChannelHandle;
    sendSequenceNumber, recvSequenceNumber: CARDINAL ← 0;
    buffers: ARRAY [0..maxClumpSize) OF NSBuffer.Buffer;
    b: NSBuffer.Buffer;
    body: NSBuffer.Body;
    pktBody: LONG POINTER TO ARRAY [0..0) OF WORD;
    
    soc ← Socket.Create[
      socket: System.nullSocketNumber, send: packetsPerClump, receive: packetsPerClump];
    Socket.SetWaitTime[soc, timeout];
    startSec ← System.GetGreenwichMeanTime[];
    UNTIL pleaseStop OR UserInput.UserAbort[log] DO
      FOR i: CARDINAL IN [0..packetsPerClump) DO
        cycle: CARDINAL;
        IF (((sendSequenceNumber ← sendSequenceNumber + 1) MOD 5) = 0) THEN Process.Yield[];
        IF (sendSequenceNumber MOD NSTypes.maxIDPDataWords) = 0 THEN
          BEGIN
          IF ~noBang THEN WriteLine[""L] ELSE WriteChar['.];
	  END;
        cycle ← IF fixed THEN wordsPerPacket - 1 ELSE sendSequenceNumber MOD length;
	IF cycle = 0 THEN cycle ← 1;
        body ← (b ← Socket.GetSendBuffer[soc]).ns;
        Socket.SetDestination[b, remoteAddr];
        Socket.SetPacketWords[b, cycle + 1];  -- 1 for echoType
	words ← words + cycle + 1;
        body.packetType ← echo;
        body.echoType ← echoRequest;
        pktBody ← @body.echoWords;
        FOR k: CARDINAL IN [0..cycle) DO
          pktBody[k] ← (k*400B + sendSequenceNumber); ENDLOOP;
        buffers[i] ← b;
        ENDLOOP;
      start ← System.GetClockPulses[];
      FOR i: CARDINAL IN [0..packetsPerClump) DO
        Socket.PutPacket[soc, buffers[i]];
        sent ← sent + 1;
        ENDLOOP;
      -- now receive the echo or any back logged echos
      DO
        cycle: CARDINAL;
	diff: INTEGER;
        b ← Socket.GetPacket[
          soc !
          Socket.TimeOut =>
            BEGIN
	    UNTIL sendSequenceNumber = recvSequenceNumber DO
	      missed ← missed + 1;
	      IF ~noLost THEN WriteChar['?];
              recvSequenceNumber ← recvSequenceNumber + 1;
	      ENDLOOP;
	    EXIT;
	    END];
	body ← b.ns;
        pktBody ← @body.echoWords;
        IF body.packetType # echo OR body.echoType # echoResponse THEN
          BEGIN
	  trash ← trash + 1;
          WriteChar['%];
          Socket.ReturnBuffer[b];
          LOOP;
          END;
        recvSequenceNumber ← recvSequenceNumber + 1;
        cycle ← IF fixed THEN length - 1 ELSE recvSequenceNumber MOD length;
	IF cycle = 0 THEN cycle ← 1;
	diff ← pktBody[0] - recvSequenceNumber;
	IF diff < 0 THEN
	  BEGIN 
          late ← late + 1;
          IF ~noLate THEN WriteChar['#];
          Socket.ReturnBuffer[b];
          recvSequenceNumber ← recvSequenceNumber - 1;
          LOOP;
	  END;
	WHILE diff > 0 DO
	  missed ← missed + 1;
	  IF ~noLost THEN WriteChar['?];
          recvSequenceNumber ← recvSequenceNumber + 1;
          cycle ← IF fixed THEN length - 1 ELSE recvSequenceNumber MOD length;
	  IF cycle = 0 THEN cycle ← 1;
	  diff ← pktBody[0] - recvSequenceNumber;
	  ENDLOOP;
        SELECT TRUE FROM
          (Socket.GetPacketBytes[b] # 2*(cycle + 1))
            OR (pktBody[0] # recvSequenceNumber) =>
            BEGIN
            trash ← trash + 1;
            WriteChar['%];
            Socket.ReturnBuffer[b];
            LOOP;
            END;
          ENDCASE =>
            BEGIN  -- the echo we were looking for
            hits: CARDINAL ← 0;
            stop ← System.GetClockPulses[];
            micro ← System.PulsesToMicroseconds[[stop - start]];
	    AddToDelayHist[micro];
            IF checkIt THEN
              FOR k: CARDINAL IN [0..cycle) DO
                IF pktBody[k] # (k*400B + recvSequenceNumber) THEN
                  BEGIN OPEN Inline;
                  expected, found, picked, dropped: WORD;
                  IF hits = 0 THEN
                    BEGIN
                    WriteCR[];
                    WriteCurrentDateAndTime[];
                    WriteString["    Data compare error(s) on packet number "L];
                    WriteLongDecimal[sent];
                    WriteLine["."L];
                    WriteLine["Idx Expected    Found   Picked  Dropped"L];
                    END;
                  expected ← k*400B + recvSequenceNumber;
                  found ← pktBody↑[k];
                  picked ← BITAND[found, BITNOT[expected]];
                  dropped ← BITAND[expected, BITNOT[found]];
                  AddToErrorHist[@picks, picked];
                  AddToErrorHist[@drops, dropped];
                  IF hits < 10 THEN
                    BEGIN
                    O3[k];
                    O9[expected];
                    O9[found];
                    O9[picked];
                    O9[dropped];
                    WriteCR[];
                    END;
                  hits ← hits + 1;
                  END;
                ENDLOOP;
            IF hits = 0 THEN good ← good + 1 ELSE bad ← bad + 1;
            IF hits = 0 AND ~noBang THEN WriteChar['!];
            IF hits > 10 THEN
              BEGIN horrible ← horrible + 1; WriteLine["...."L]; END;
            Socket.ReturnBuffer[b];
            IF recvSequenceNumber = sendSequenceNumber THEN EXIT;
            END;
        ENDLOOP;
      ENDLOOP;
    stopSec ← System.GetGreenwichMeanTime[];
    WriteCR[];
    Socket.Delete[soc];
    WriteLongNumber[sent, 10, 8];
    WriteLine[" packets sent."L];
    PrintSummary[stopSec-startSec];
    PrintErrorHist[];
    PrintDelayHist[];
    running ← FALSE;
    END;


  PokeOnce: FormSW.ProcType =
    BEGIN
    sequenceNumber: CARDINAL ← 0;
    b: NSBuffer.Buffer;
    body : NSBuffer.Body;
    errFlag: BOOLEAN ← FALSE;
    remoteAddr: System.NetworkAddress;
    soc: Socket.ChannelHandle;
    hit: CARDINAL ← 0;
    WriteCR[];
    WriteString["Poking "L];
    WriteString[remoteAddress];
    WriteString[" = "L];
    remoteAddr ← GetAddress[remoteAddress, NSConstants.echoerSocket !
      Trouble =>
        BEGIN
        WriteString["AddressTranslation troubles: "L];
	WriteLine[reason];
        errFlag ← TRUE;
        CONTINUE;
        END];
    IF errFlag THEN RETURN;
    WriteNetworkAddressVerbose[remoteAddr];
    WriteLine["."L];
    soc ← Socket.Create[socket: System.nullSocketNumber, receive: 20];
    Socket.SetWaitTime[soc, timeout];
    body ← (b ← Socket.GetSendBuffer[soc]).ns;
    Socket.SetDestination[b, remoteAddr];
    Socket.SetPacketWords[b, 1];
    body.packetType ← echo;
    body.echoType ← echoRequest;
    Socket.PutPacket[soc, b];
    DO
      b ← Socket.GetPacket[soc ! Socket.TimeOut => EXIT];
      body ← b.ns;
      SELECT TRUE FROM
        (Socket.GetPacketBytes[b] # 2*1) OR (body.echoType # echoResponse) =>
          BEGIN
          WriteChar['#];
          Socket.ReturnBuffer[b];
          LOOP;
          END;
        ENDCASE =>
          BEGIN
	  hit ← hit + 1;
          WriteString["Response number "L];
	  WriteNumber[hit, 10, 2];
          WriteString[" from "L];
          WriteNetworkAddressVerbose[Socket.GetSource[b]];
          WriteLine["."L];
          Socket.ReturnBuffer[b];
          END;
      ENDLOOP;
    WriteCR[];
    Socket.Delete[soc];
    END;

  Clumps: FormSW.ProcType =
    BEGIN
    fixed: BOOLEAN;
    size, clumps: CARDINAL;
    errFlag: BOOLEAN ← FALSE;
    remoteAddr: System.NetworkAddress;
    StopIt[];
    IF fixed AND maxLength < 1 THEN
      BEGIN
      WriteLine["Need at least 2 word."L];
      RETURN;
      END;
    IF maxLength > NSTypes.maxIDPDataWords THEN
      BEGIN
      WriteLine["(max)Length is too big."L];
      RETURN;
      END;
    IF clumpSize > maxClumpSize THEN
      BEGIN
      WriteLine["ClumpSize is too big."L];
      RETURN;
      END;
    IF clumpSize < 1 THEN
      BEGIN
      WriteLine["Need at least 1 packet per clump."L];
      RETURN;
      END;
    fixed ← fixedLength;
    size ← maxLength;
    clumps ← clumpSize;
    WriteCR[];
    WriteString["Echoing Clumps to "L];
    WriteString[remoteAddress];
    WriteString[" = "L];
    remoteAddr ← GetAddress[remoteAddress, NSConstants.echoerSocket !
      Trouble =>
        BEGIN
        WriteString["AddressTranslation troubles: "L];
	WriteLine[reason];
        errFlag ← TRUE;
        CONTINUE;
        END];
    IF errFlag THEN RETURN;
    WriteNetworkAddressVerbose[remoteAddr];
    WriteLine["."L];
    pleaseStop ← FALSE;
    running ← TRUE;
    Process.Detach[FORK Worker[remoteAddr, fixed, size, clumps]];
    END;

  ShowPercent: PROCEDURE [n, sent: LONG CARDINAL, s: LONG STRING] =
    BEGIN
    IF n = 0 THEN RETURN;
    WriteLongNumber[n, 10, 8];
    WriteLongNumber[n*100/sent, 10, 4];
    WriteString["% "L];
    WriteLine[s];
    END;

  WriteChar: PROCEDURE [c: CHARACTER] = BEGIN Put.Char[log, c]; END;

  WriteCR: PROCEDURE = BEGIN Put.CR[log]; END;

  WriteString: PROCEDURE [s: LONG STRING] = BEGIN Put.Text[log, s]; END;

  WriteLine: PROCEDURE [s: LONG STRING] = BEGIN Put.Line[log, s]; END;

  WriteLongNumber: PROCEDURE [n: LONG CARDINAL, radix, width: CARDINAL] = INLINE
    BEGIN
    temp: STRING = [25];
    String.AppendLongNumber[temp, n, radix];
    THROUGH [temp.length..width) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, temp];
    END;
 
  WriteNumber: PROCEDURE [n, radix, width: CARDINAL] = INLINE
    BEGIN
    temp: STRING = [25];
    String.AppendNumber[temp, n, radix];
    THROUGH [temp.length..width) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, temp];
    END;
 
  WriteLongDecimal: PROCEDURE [n: LONG UNSPECIFIED] =
    BEGIN
    temp: STRING = [32];
    String.AppendLongNumber[temp, n, 10];
    Put.Text[log, temp];
    END;
 
  D8: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 10, 8]; END;
 
  O3: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;
 
  O6: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;
 
  O9: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 9]; END;
 
  WriteCurrentDateAndTime: PROCEDURE =
    BEGIN
    temp: STRING = [32];
    Time.AppendCurrent[temp];
    Put.Text[log, temp];
    END;

  WriteNetworkAddressVerbose: PROCEDURE [address: System.NetworkAddress] =
    BEGIN
    temp: STRING = [100];
    Append: PROCEDURE [s: LONG STRING, clientData: LONG POINTER] =
      BEGIN String.AppendString[temp, s]; END;
    Format.NetworkAddress[Append, address, octal];
    String.AppendString[temp, " = "L];
    Format.NetworkAddress[Append, address, productSoftware];
    Put.Text[log, temp];
    END;
  
  Trouble: ERROR [reason: LONG STRING] = CODE;
  GetAddress: PROCEDURE [host: LONG STRING, socket: System.SocketNumber]
    RETURNS [addr: System.NetworkAddress] =
    BEGIN
    localFailed: BOOLEAN ← FALSE;
    IF host = NIL THEN ERROR Trouble["NIL => Address Fault"L];
    addr ← Unformat.NetworkAddress[host, octal !
      Unformat.Error => BEGIN localFailed ← TRUE; CONTINUE; END ];
    IF localFailed THEN
      BEGIN
      addr ← AddressTranslation.StringToNetworkAddress[host !
        AddressTranslation.Error =>
	  BEGIN
	  temp: STRING = [200];
	  proc: Format.StringProc = {String.AppendString[temp, s]};
	  AddressTranslation.PrintError[errorRecord, proc];
	  ERROR Trouble[temp];
	  END].addr;
        addr.socket ← socket;  -- CH returns trash in socket
      END;
    IF addr.socket = System.nullSocketNumber THEN addr.socket ← socket;
    END;

  Init[];  -- this gets string out of global frame
  END...