void printVector(vector<StatsData *> &vals, 
                     HashMap<uint32_t, uint32_t> serverip_to_shortid,
                     const string &header) {
        cout << format("\n%s:\n") % header;
        cout << "server operation   #ops  #dups mean ";
        if (enable_first_latency_stat) {
            cout << "; firstlat mean 50% 90% ";
        }
        if (enable_last_latency_stat) {
            cout << "; lastlat mean 50% 90%";
        }
        cout << "\n";

        sort(vals.begin(),vals.end(),sortByServerOp());
        for (vector<StatsData *>::iterator i = vals.begin();
            i != vals.end();++i) {
            StatsData *j = *i;
            int64_t noperations = j->nops();
            if (j->serverip == 0) {
                cout << " *  ";
            } else {
                cout << format("%03d ") % serverip_to_shortid[j->serverip];
            }
            cout << format("%9s %9lld %6lld %4.2f ")
                    % j->operation % noperations
                    % j->duplicates->countll()
                    % j->duplicates->mean();
            printOneQuant(j->first_latency_ms);
            printOneQuant(j->last_latency_ms);
            cout << "\n";
        }
    }
    // TODO: use rotating hash map
    void handleResponse() {
        // row is a response, so address of server = sourceip
        TidData dummy(transaction_id.val(), sourceip.val(), destip.val());
        TidData *t = pending1->lookup(dummy);

        if (t == NULL) {
            ++missing_request_count;
        } else {
            // we now have both request and response, so we can add latency to statistics
            // TODO: do the calculation in raw units and convert at the end.
            int64_t delay_first_raw = reqtime.valRaw() - t->first_reqtime_raw;
            int64_t delay_last_raw = reqtime.valRaw() - t->last_reqtime_raw;

            double delay_first_ms 
                    = reqtime.rawToDoubleSeconds(delay_first_raw) * 1.0e3;
            double delay_last_ms 
                    = reqtime.rawToDoubleSeconds(delay_last_raw) * 1.0e3;
            StatsData hdummy(sourceip.val(), operation.stringval());
            StatsData *d = stats_table.lookup(hdummy);

            // add to statistics per request type and server
            if (d == NULL) { // create new entry
                hdummy.initStats(enable_first_latency_stat, enable_last_latency_stat);
                d = stats_table.add(hdummy);
            }
            d->add(delay_first_ms, delay_last_ms);
            // remove request from pending hashtable only if
            // we haven't seen a duplicate request.  If we
            // have seen a duplicate request, then there might
            // be a duplicate reply coming
            if (t->duplicate_count > 0) {
                d->duplicates->add(t->duplicate_count);
                if (t->seen_reply) {
                    ++duplicate_reply_count;
                } else {
                    t->seen_reply = true;
                }
            } else {
                pending1->remove(*t);
            }
        }
    }
/*! \brief extract channel information from raw data

  ReadBuffData extracts channel information from the raw data arrays
  and places it into a structure called evt.  A pointer to each
  of the evt objects is placed in the eventlist vector for later time
  sorting.
  \param [in] buf : the buffer to process
  \param [in] bufLen : the length of the buffer
  \param [in] eventList : the event list to add the extracted buffer to
  \return An unused integer
*/
int ReadBuffDataD(word_t *buf, unsigned long *bufLen,
                   vector<ChanEvent*> &eventList) {
    // multiplier for high bits of 48-bit time
    static const double HIGH_MULT = pow(2., 32.);
    word_t modNum;
    unsigned long numEvents = 0;
    word_t *bufStart = buf;
    /* Determine the number of words in the buffer */
    *bufLen = *buf++;
    /* Read the module number */
    modNum = *buf++;
    ChanEvent *lastVirtualChannel = NULL;
    if(*bufLen > 0) {   // check if the buffer has data
        if(*bufLen == 2) {
            // this is an empty channel
            return 0;
        }
        do {
            ChanEvent *currentEvt = new ChanEvent;
            // decoding event data... see pixie16app.c
            // buf points to the start of channel data
            word_t chanNum      = (buf[0] & 0x0000000F);
            word_t slotNum      = (buf[0] & 0x000000F0) >> 4;
            word_t crateNum     = (buf[0] & 0x00000F00) >> 8;
            word_t headerLength = (buf[0] & 0x0001F000) >> 12;
            word_t eventLength  = (buf[0] & 0x1FFE0000) >> 17;
            currentEvt->virtualChannel = ((buf[0] & 0x20000000) != 0);
            currentEvt->saturatedBit   = ((buf[0] & 0x40000000) != 0);
            currentEvt->pileupBit      = ((buf[0] & 0x80000000) != 0);
            // Rev. D header lengths not clearly defined in pixie16app_defs
            //! magic numbers here for now
            // make some sanity checks
            if(headerLength == stats.headerLength) {
                // this is a manual statistics block inserted by the poll program
                stats.DoStatisticsBlock(&buf[1], modNum);
                buf += eventLength;
                numEvents = readbuff::STATS;
                continue;
            }
            if(headerLength != 4  && headerLength != 8 &&
                headerLength != 12 && headerLength != 16) {
                cout << "  Unexpected header length: " << headerLength << endl;
                cout << "    Buffer " << modNum << " of length " << *bufLen << endl;
                cout << "    CHAN:SLOT:CRATE "
                     << chanNum << ":" << slotNum << ":" << crateNum << endl;
                // advance to next event and continue
                // buf += EventLength;
                // continue;
                // skip the rest of this buffer
                return readbuff::ERROR;
                //return numEvents;
            }
            word_t lowTime     = buf[1];
            word_t highTime    = buf[2] & 0x0000FFFF;
            word_t cfdTime     = (buf[2] & 0xFFFF0000) >> 16;
            word_t energy      = buf[3] & 0x0000FFFF;
            word_t traceLength = (buf[3] & 0xFFFF0000) >> 16;
            if(headerLength == 8 || headerLength == 16) {
                // skip the onboard partial sums for now
                //   trailing, leading, gap, baseline
            }
            if(headerLength >= 12) {
                int offset = headerLength - 8;
                for(int i=0; i < currentEvt->numQdcs; i++) {
                        currentEvt->qdcValue[i] = buf[offset + i];
                }
            }
            // one last sanity check
            if(traceLength / 2 + headerLength != eventLength) {
                cout << "  Bad event length (" << eventLength
                    << ") does not correspond with length of header (" << headerLength
                    << ") and length of trace (" << traceLength << ")" << endl;
                buf += eventLength;
                continue;
            }
            // handle multiple crates
            modNum += 100 * crateNum;
            currentEvt->chanNum = chanNum;
            currentEvt->modNum = modNum;
            if(currentEvt->virtualChannel) {
                DetectorLibrary* modChan = DetectorLibrary::get();
                currentEvt->modNum += modChan->GetPhysicalModules();
                if(modChan->at(modNum, chanNum).HasTag("construct_trace")) {
                        lastVirtualChannel = currentEvt;
                }
            }
            currentEvt->energy = energy;
            //KM 2012-10-24 reinstating removal of saturated
            if(currentEvt->saturatedBit)
                currentEvt->energy = 16383;
            currentEvt->trigTime = lowTime;
            currentEvt->cfdTime  = cfdTime;
            currentEvt->eventTimeHi = highTime;
            currentEvt->eventTimeLo = lowTime;
            currentEvt->time = highTime * HIGH_MULT + lowTime;
            buf += headerLength;
            /* Check if trace data follows the channel header */
            if(traceLength > 0) {
                // sbuf points to the beginning of trace data
                halfword_t *sbuf = (halfword_t *)buf;
                currentEvt->trace.reserve(traceLength);
                if(currentEvt->saturatedBit)
                    currentEvt->trace.SetValue("saturation", 1);
                if(lastVirtualChannel != NULL && lastVirtualChannel->trace.empty()) {
                    lastVirtualChannel->trace.assign(traceLength, 0);
                }
                // Read the trace data (2-bytes per sample, i.e. 2 samples per word)
                for(unsigned int k = 0; k < traceLength; k ++) {
                    currentEvt->trace.push_back(sbuf[k]);
                    if(lastVirtualChannel != NULL) {
                        lastVirtualChannel->trace[k] += sbuf[k];
                    }
                }
                buf += traceLength / 2;
            }
            eventList.push_back(currentEvt);
            numEvents++;
        } while(buf < bufStart + *bufLen);
    } else {  // if buffer has data
/*! \brief extract channel information from raw data

  ReadBuffData extracts channel information from the raw data arrays
  and places it into a structure called evt.  A pointer to each
  of the evt objects is placed in the eventlist vector for later time
  sorting.
  \param [in] buf : the buffer to process
  \param [in] bufLen : the length of the buffer
  \param [in] eventList : the event list to add the extracted buffer to
  \return An unused integer
*/
int ReadBuffDataF(word_t *buf, unsigned long *bufLen,
                   vector<ChanEvent*> &eventList) {
    // multiplier for high bits of 48-bit time
    static const double HIGH_MULT = pow(2., 32.);
    word_t modNum;
    unsigned long numEvents = 0;
    word_t *bufStart = buf;
    /* Determine the number of words in the buffer */
    *bufLen = *buf++;
    /* Read the module number */
    modNum = *buf++;
    ChanEvent *lastVirtualChannel = NULL;
    if(*bufLen > 0) {   // check if the buffer has data
        if(*bufLen == 2) {
            // this is an empty channel
            return 0;
        }
        do {
            ChanEvent *currentEvt = new ChanEvent;
            // decoding event data... see pixie16app.c
            // buf points to the start of channel data

	    //Decode the first header word
            word_t chanNum        = (buf[0] & 0x0000000F);
            word_t slotNum        = (buf[0] & 0x000000F0) >> 4;
            word_t crateNum       = (buf[0] & 0x00000F00) >> 8;
	    word_t headerLength   = (buf[0] & 0x0001F000) >> 12;
            word_t eventLength    = (buf[0] & 0x7FFE0000) >> 17;
            currentEvt->pileupBit = (buf[0] & 0x80000000) != 0;

	    // Sanity check
            if(headerLength == stats.headerLength) {
                // this is a manual statistics block inserted poll 
                stats.DoStatisticsBlock(&buf[1], modNum);
                buf += eventLength;
                numEvents = readbuff::STATS;
                continue;
            }

	    //Decode the second header word
            word_t lowTime     = buf[1];

	    //Decode the third header word
            word_t highTime    = buf[2] & 0x0000FFFF;
            word_t cfdTime     = (buf[2] & 0x3FFF0000) >> 16;
	    currentEvt->cfdTrigSource  = ((buf[2] & 0x40000000) != 0);
	    currentEvt->cfdForceTrig   = ((buf[2] & 0x80000000) != 0);

	    //Decode the foruth header word
            word_t energy      = buf[3] & 0x0000FFFF;
            word_t traceLength = (buf[3] & 0x7FFF0000) >> 16;
	    currentEvt->saturatedBit   = ((buf[3] & 0x80000000) != 0);
	    
	    int offset = headerLength - 8;
	    switch(headerLength) {
	    case 4:
	    case 6:
	    case 8:
	    case 10:
		break;
	    case 12:
	    case 14:
	    case 16:
	    case 18:
                for(int i=0; i < currentEvt->numQdcs; i++)
                        currentEvt->qdcValue[i] = buf[offset + i];
		break;
	    default:
		cerr << "  Unexpected header length: " << headerLength << endl;
                cerr << "    Buffer " << modNum << " of length " << *bufLen << endl;
                cerr << "    CHAN:SLOT:CRATE "
                     << chanNum << ":" << slotNum << ":" << crateNum << endl;
                return readbuff::ERROR;
		break;
	    }

            // one last sanity check
            if(traceLength / 2 + headerLength != eventLength) {
                cerr << "  Bad event length (" << eventLength
		     << ") does not correspond with length of header (" 
		     << headerLength << ") and length of trace (" 
		     << traceLength << ")" << endl;
                buf += eventLength;
                continue;
            }

            // handle multiple crates
            modNum += 100 * crateNum;
            currentEvt->chanNum = chanNum;
            currentEvt->modNum = modNum;
            if(currentEvt->virtualChannel) {
                DetectorLibrary* modChan = DetectorLibrary::get();
                currentEvt->modNum += modChan->GetPhysicalModules();
                if(modChan->at(modNum, chanNum).HasTag("construct_trace")) {
                        lastVirtualChannel = currentEvt;
                }
            }
            currentEvt->energy = energy;
            if(currentEvt->saturatedBit)
                currentEvt->energy = 16383;
            currentEvt->trigTime = lowTime;
            currentEvt->cfdTime  = cfdTime;
            currentEvt->eventTimeHi = highTime;
            currentEvt->eventTimeLo = lowTime;
            currentEvt->time = highTime * HIGH_MULT + lowTime;
            buf += headerLength;

            /* Check if trace data follows the channel header */
            if(traceLength > 0) {
                // sbuf points to the beginning of trace data
                halfword_t *sbuf = (halfword_t *)buf;
                currentEvt->trace.reserve(traceLength);
                if(currentEvt->saturatedBit)
                    currentEvt->trace.SetValue("saturation", 1);
                if(lastVirtualChannel != NULL && lastVirtualChannel->trace.empty()) {
                    lastVirtualChannel->trace.assign(traceLength, 0);
                }
                // Read the trace data (2-bytes per sample, i.e. 2 samples per word)
                for(unsigned int k = 0; k < traceLength; k ++) {
                    currentEvt->trace.push_back(sbuf[k]);
                    if(lastVirtualChannel != NULL) {
                        lastVirtualChannel->trace[k] += sbuf[k];
                    }
                }
                buf += traceLength / 2;
            }
            eventList.push_back(currentEvt);
            numEvents++;
        } while(buf < bufStart + *bufLen);
    } else {  // if buffer has data