docs: private void doGraph(final TSDB tsdb, final HttpQuery query)
      throws IOException {
      final String basepath = getGnuplotBasePath(tsdb, query);
      long start_time = DateTime.parseDateTimeString(
        query.getRequiredQueryStringParam("start"),
        query.getQueryStringParam("tz"));
      final boolean nocache = query.hasQueryStringParam("nocache");
      if (start_time == -1) {
        throw BadRequestException.missingParameter("start");
      } else {
        // temp fixup to seconds from ms until the rest of TSDB supports ms
        // Note you can't append this to the DateTime.parseDateTimeString() call as
        // it clobbers -1 results
        start_time /= 1000;
      }
      long end_time = DateTime.parseDateTimeString(
          query.getQueryStringParam("end"),
          query.getQueryStringParam("tz"));
      final long now = System.currentTimeMillis() / 1000;
      if (end_time == -1) {
        end_time = now;
      } else {
        // temp fixup to seconds from ms until the rest of TSDB supports ms
        // Note you can't append this to the DateTime.parseDateTimeString() call as
        // it clobbers -1 results
        end_time /= 1000;
      }
      final int max_age = computeMaxAge(query, start_time, end_time, now);
      if (!nocache && isDiskCacheHit(query, end_time, max_age, basepath)) {
        return;
      }
  
      // Parse TSQuery from HTTP query
      final TSQuery tsquery = QueryRpc.parseQuery(tsdb, query);
      tsquery.validateAndSetQuery();
  
      // Build the queries for the parsed TSQuery
      Query[] tsdbqueries = tsquery.buildQueries(tsdb);
  
      List<String> options = query.getQueryStringParams("o");
      if (options == null) {
        options = new ArrayList<String>(tsdbqueries.length);
        for (int i = 0; i < tsdbqueries.length; i++) {
          options.add("");
        }
      } else if (options.size() != tsdbqueries.length) {
        throw new BadRequestException(options.size() + " `o' parameters, but "
          + tsdbqueries.length + " `m' parameters.");
      } else {
        for (final String option : options) {
          // TODO - far from perfect, should help a little.
          if (option.contains("`") || option.contains("%60") || 
              option.contains("&#96;")) {
            throw new BadRequestException("Option contained a back-tick. "
                + "That's a no-no.");
          }
        }
      }
      for (final Query tsdbquery : tsdbqueries) {
        try {
          tsdbquery.setStartTime(start_time);
        } catch (IllegalArgumentException e) {
          throw new BadRequestException("start time: " + e.getMessage());
        }
        try {
          tsdbquery.setEndTime(end_time);
        } catch (IllegalArgumentException e) {
          throw new BadRequestException("end time: " + e.getMessage());
        }
      }
      final Plot plot = new Plot(start_time, end_time,
            DateTime.timezones.get(query.getQueryStringParam("tz")));
      setPlotDimensions(query, plot);
      setPlotParams(query, plot);
      final int nqueries = tsdbqueries.length;
      @SuppressWarnings("unchecked")
      final HashSet<String>[] aggregated_tags = new HashSet[nqueries];
      int npoints = 0;
      for (int i = 0; i < nqueries; i++) {
        try {  // execute the TSDB query!
          // XXX This is slow and will block Netty.  TODO(tsuna): Don't block.
          // TODO(tsuna): Optimization: run each query in parallel.
          final DataPoints[] series = tsdbqueries[i].run();
          for (final DataPoints datapoints : series) {
            plot.add(datapoints, options.get(i));
            aggregated_tags[i] = new HashSet<String>();
            aggregated_tags[i].addAll(datapoints.getAggregatedTags());
            npoints += datapoints.aggregatedSize();
          }
        } catch (RuntimeException e) {
          logInfo(query, "Query failed (stack trace coming): "
                  + tsdbqueries[i]);
          throw e;
        }
        tsdbqueries[i] = null;  // free()
      }
      tsdbqueries = null;  // free()
  
      if (query.hasQueryStringParam("ascii")) {
        respondAsciiQuery(query, max_age, basepath, plot);
        return;
      }
  
      final RunGnuplot rungnuplot = new RunGnuplot(query, max_age, plot, basepath,
              aggregated_tags, npoints);
  
      class ErrorCB implements Callback<Object, Exception> {
        public Object call(final Exception e) throws Exception {
          LOG.warn("Failed to retrieve global annotations: ", e);
          throw e;
        }
      }
  
      class GlobalCB implements Callback<Object, List<Annotation>> {
        public Object call(final List<Annotation> global_annotations) throws Exception {
          rungnuplot.plot.setGlobals(global_annotations);
          execGnuplot(rungnuplot, query);
  
          return null;
        }
      }
  
      // Fetch global annotations, if needed
      if (!tsquery.getNoAnnotations() && tsquery.getGlobalAnnotations()) {
        Annotation.getGlobalAnnotations(tsdb, start_time, end_time)
                .addCallback(new GlobalCB()).addErrback(new ErrorCB());
      } else {
        execGnuplot(rungnuplot, query);
      }
    }