private static String convertTimestamp(long ms) {
    calendar.setTimeInMillis(ms);

    int hrs = calendar.get(Calendar.HOUR_OF_DAY);
    int mins = calendar.get(Calendar.MINUTE);
    int secs = calendar.get(Calendar.SECOND);

    NumberFormat nf = NumberFormat.getIntegerInstance();
    nf.setMinimumIntegerDigits(2);
    return nf.format(hrs) + ":" + nf.format(mins) + ":" + nf.format(secs);
  }
/** Reads statistics from statarchive files. */
public class PerfStatReader {

  // ------------------------------------------------------------------------------
  // Timestamps
  // ------------------------------------------------------------------------------

  static Calendar calendar = Calendar.getInstance();

  private static String convertTimestamp(long ms) {
    calendar.setTimeInMillis(ms);

    int hrs = calendar.get(Calendar.HOUR_OF_DAY);
    int mins = calendar.get(Calendar.MINUTE);
    int secs = calendar.get(Calendar.SECOND);

    NumberFormat nf = NumberFormat.getIntegerInstance();
    nf.setMinimumIntegerDigits(2);
    return nf.format(hrs) + ":" + nf.format(mins) + ":" + nf.format(secs);
  }

  //////////////////////////////////////////////////////////////////////////////
  ////    INTERNALS                                                         ////
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Fetches the values for all statistics specifications in the configuration, trimmed as specified
   * by the trim specifications. Returns a map keyed by the logical name of the statistics
   * specification, where the value is a (possibly empty) list of {@link PerfStatValue}s for that
   * specification.
   */
  protected static SortedMap processStatConfig(StatConfig statconfig) {

    List tmpArchives = statconfig.getStatisticArchivesAsList();
    if (tmpArchives == null || tmpArchives.size() == 0) {
      return null;
    }
    List archives = new ArrayList();
    archives.addAll(tmpArchives);

    List statspecs = new ArrayList();
    statspecs.addAll(statconfig.getStatSpecInstances().values());

    if (Log.getLogWriter().finestEnabled()) {
      Log.getLogWriter().finest("PerfStatReader: archives ==> " + archives);
      Log.getLogWriter()
          .finest(
              "PerfStatReader: generating from ==> "
                  + statconfig.getStatSpecs().keySet()
                  + "==>"
                  + statconfig.getStatSpecs().values());
    }
    Map rawValues = new HashMap();
    try {
      StatArchiveReader reader =
          new StatArchiveReader(
              (File[]) archives.toArray(new File[archives.size()]),
              (StatSpec[]) statspecs.toArray(new StatSpec[statspecs.size()]),
              true);
      Iterator it = statspecs.iterator();
      while (it.hasNext()) {
        StatSpec spec = (StatSpec) it.next();
        StatArchiveReader.StatValue[] values = reader.matchSpec(spec);
        if (Log.getLogWriter().finestEnabled()) {
          for (int i = 0; i < values.length; i++) {
            StatArchiveReader.StatValue value = values[i];
            long[] times = value.getRawAbsoluteTimeStamps();
            // this is filter=none, untrimmed
            double[] rawvals = value.getRawSnapshots();
            // this is filter=whatever it is for the statspec, untrimmed
            double[] vals = value.getSnapshots();
            Log.getLogWriter().finest(times.length + " timestamps with " + vals.length + " values");
            String s = "";
            for (int j = 0; j < Math.min(times.length, vals.length); j++) {
              s +=
                  "\ntime "
                      + convertTimestamp(times[j])
                      + " "
                      + times[j]
                      + " filtered = "
                      + vals[j]
                      + " raw = "
                      + rawvals[j]
                      + " ";
            }
            Log.getLogWriter().finest("Values: " + s);
          }
          Log.getLogWriter().finest("PerfStatReader: Read spec ==> " + spec);
          for (int p = 0; p < values.length; p++) {
            Log.getLogWriter().finest("PerfStatReader: Initial value[" + p + "]=" + values[p]);
          }
        }
        rawValues.put(spec.getName(), Arrays.asList(values));
      }
    } catch (IOException e) {
      throw new StatConfigException("Unable to read archive", e);
    }
    if (Log.getLogWriter().finerEnabled()) {
      for (Iterator i = rawValues.keySet().iterator(); i.hasNext(); ) {
        String statspecname = (String) i.next();
        List statspecvalues = (List) rawValues.get(statspecname);
        Log.getLogWriter()
            .finer("PerfStatReader: Raw values ==> " + statspecname + "..." + statspecvalues);
      }
    }

    // now do the reading, then filter, trim, and do operations on each result
    SortedMap processedValues = new TreeMap();
    for (Iterator i = rawValues.keySet().iterator(); i.hasNext(); ) {
      String statspecname = (String) i.next();
      List rawValueList = (List) rawValues.get(statspecname);
      List processedValueList = new ArrayList();
      for (Iterator j = rawValueList.iterator(); j.hasNext(); ) {
        StatSpec statspec = statconfig.getStatSpec(statspecname);
        TrimSpec trimspec = statconfig.getTrimSpec(statspec.getTrimSpecName());
        StatArchiveReader.StatValue sv = (StatArchiveReader.StatValue) j.next();
        PerfStatValue psv = getPerfStatValue(statspec, trimspec, sv);

        StatArchiveReader.ResourceType type = sv.getType();
        StatArchiveReader.StatDescriptor desc = type.getStat(statspec.getStatName());
        if (desc == null) {
          String s = "Could not find a StatDescriptor for \"" + statspec.getStatName() + "\"";
          throw new InternalGemFireException(s);
        }

        if (PerfReporter.brief) {
          StringBuffer sb = new StringBuffer();
          sb.append(desc.getDescription());
          sb.append(" (");
          sb.append(desc.getUnits());
          if (statspec.getFilter() == StatArchiveReader.StatValue.FILTER_PERSEC) {
            sb.append("/sec");
          }
          sb.append(")");
          statspec.setDescription(sb.toString());
        }

        processedValueList.add(psv);
        if (Log.getLogWriter().finestEnabled()) {
          Log.getLogWriter().finest("PerfStatReader: PSV: " + psv.toString());
        }
        // @todo lises need to sort these for later comparisons, but on what?
        //             any way to use the logical archive it came from?
      }
      if (Log.getLogWriter().finestEnabled()) {
        Log.getLogWriter()
            .finest("PerfStatReader: Put " + statspecname + " values as " + processedValueList);
      }
      processedValues.put(statspecname, processedValueList);
    }
    if (Log.getLogWriter().finerEnabled()) {
      for (Iterator i = processedValues.keySet().iterator(); i.hasNext(); ) {
        String statspecname = (String) i.next();
        List statspecvalues = (List) processedValues.get(statspecname);
        Log.getLogWriter()
            .finer("PerfStatReader: Processed values ==> " + statspecname + "..." + statspecvalues);
      }
    }

    // now evaluate the expressions
    List exprs = new ArrayList();
    exprs.addAll(statconfig.getExprInstances().values());
    if (Log.getLogWriter().finestEnabled()) {
      Log.getLogWriter().finest("PerfStatReader: exprs ==> " + exprs);
    }
    for (Iterator i = exprs.iterator(); i.hasNext(); ) {
      Expr expr = (Expr) i.next();
      String numerator = expr.getNumerator().getName();
      String denominator = expr.getDenominator().getName();
      List numerators = (List) processedValues.get(numerator);
      List denominators = (List) processedValues.get(denominator);
      if (Log.getLogWriter().finestEnabled()) {
        Log.getLogWriter()
            .finest(
                "Dividing "
                    + numerator
                    + " by "
                    + denominator
                    + " ==> "
                    + numerators
                    + " OVER "
                    + denominators);
      }
      List processedValueList = new ArrayList();
      if (numerators.size() != 0 && denominators.size() != 0) {
        for (int j = 0; j < numerators.size(); j++) {
          PerfStatValue npsv = (PerfStatValue) numerators.get(j);
          PerfStatValue dpsv = (PerfStatValue) denominators.get(j);
          PerfStatValue psv = npsv.dividedBy(dpsv);
          if (psv != null) {
            processedValueList.add(psv);
          }
        }
      } else if (numerators.size() == 0 || denominators.size() == 0) {
        // nothing to do
      } else if (numerators.size() != denominators.size()) {
        String s =
            "Dividing "
                + numerator
                + " by "
                + denominator
                + " found numerators ("
                + numerators.size()
                + ") and denominators ("
                + denominators.size()
                + " have different sizes ==> "
                + numerators
                + " OVER "
                + denominators;
        throw new PerfComparisonException(s);
      }
      processedValues.put(expr.getName(), processedValueList);
    }

    return processedValues;
  }

  /**
   * Translates a StatValue into a PerfStatValue with the specified filter, with the specified
   * operations, and the specified trim. For the latter, if start is -1, uses the beginning of the
   * statistic's existence, and if end is -1, goes to the end of the statistic's existence.
   */
  protected static PerfStatValue getPerfStatValue(
      StatSpec statspec, TrimSpec trimspec, StatArchiveReader.StatValue sv) {
    long start = trimspec.getStart();
    long end = trimspec.getEnd();
    sv = sv.createTrimmed(start, end);
    if (Log.getLogWriter().finestEnabled()) {
      Log.getLogWriter()
          .finest(
              "PerfStatReader: Trimmed from "
                  + trimspec.getStartStr()
                  + " ("
                  + start
                  + ") to "
                  + trimspec.getEndStr()
                  + " ("
                  + end
                  + ")");
    }

    int filter = statspec.getFilter();
    double mean = -1;
    if (filter == StatArchiveReader.StatValue.FILTER_PERSEC && statspec.getMean()) {
      if (start == -1) {
        start = sv.getRawAbsoluteTimeStamps()[0];
      }
      if (end == -1) {
        long[] rats = sv.getRawAbsoluteTimeStamps();
        end = rats[rats.length - 1];
      }
      long elapsedSec = (end - start) / 1000;
      sv.setFilter(StatArchiveReader.StatValue.FILTER_NONE);
      double del = sv.getSnapshotsMaximum() - sv.getSnapshotsMinimum();
      mean = del / elapsedSec;
    }
    sv.setFilter(filter);

    // @todo lises see if psv really needs to hang onto specs
    PerfStatValue psv = new PerfStatValue(statspec, trimspec);
    psv.setIsLargerBetter(sv.getDescriptor().isLargerBetter());
    psv.setSamples(sv.getSnapshotsSize());

    if (statspec.getMin()) psv.setMin(sv.getSnapshotsMinimum());
    if (statspec.getMax()) psv.setMax(sv.getSnapshotsMaximum());
    if (statspec.getMaxMinusMin())
      psv.setMaxMinusMin(sv.getSnapshotsMaximum() - sv.getSnapshotsMinimum());
    if (statspec.getMean()) {
      if (filter == StatArchiveReader.StatValue.FILTER_PERSEC) {
        psv.setMean(mean);
      } else {
        psv.setMean(sv.getSnapshotsAverage());
      }
    }
    if (statspec.getStddev()) psv.setStddev(sv.getSnapshotsStandardDeviation());

    SortedSet archives = new TreeSet();
    StatArchiveReader.ResourceInst[] resources = sv.getResources();
    String productVersion = null;
    for (int i = 0; i < resources.length; i++) {
      String archive = resources[i].getArchive().getFile().getParentFile().getName();
      if (productVersion == null) {
        productVersion = resources[i].getArchive().getArchiveInfo().getProductVersion();
      }
      if (!archives.contains(archive)) {
        archives.add(archive);
      }
    }
    psv.setArchives(archives);
    psv.setProductVersion(productVersion);

    return psv;
  }
}