/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.gobblin.metrics.performance;

import lombok.Builder;
import lombok.Singular;
import lombok.extern.slf4j.Slf4j;

import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.apache.gobblin.util.ExecutorsUtils;


/**
 * Methods to run performance tests on Gobblin Metrics.
 */
@Slf4j
public class PerformanceUtils {

  /**
   * Runs a set of performance tests. The method will take the cardinal product of the values of each input parameter,
   * and run a performance test for each combination of paramters. At the end, it will print out the results.
   *
   * <p>
   *   All parameters (except for queries) are a set of integers, meaning that separate tests will be run for all
   *   the values provided. The number of queries will be identical for all tests.
   * </p>
   *
   * @param threads Number of threads to spawn. Each thread will have an {@link Incrementer} and update metrics.
   * @param depth Depth of the {@link org.apache.gobblin.metrics.MetricContext} tree.
   * @param forkAtDepth If multiple threads, each thread has its own {@link org.apache.gobblin.metrics.MetricContext}. This
   *                    parameter sets the first level in the tree where the per-thread MetricContexts branch off.
   * @param counters Number of counters to generate per thread.
   * @param meters Number of meters to generate per thread.
   * @param histograms Number of histograms to generate per thread.
   * @param timers Number of timers to generate per thread.
   * @param queries Number of increments to do, divided among all threads.
   * @throws Exception
   */
  @Builder(buildMethodName = "run", builderMethodName = "multiTest")
  public static void _multiTest(@Singular("threads") Set<Integer> threads,
      @Singular("depth") Set<Integer> depth,
      @Singular("forkAtDepth") Set<Integer> forkAtDepth,
      @Singular("counters") Set<Integer> counters,
      @Singular("meters") Set<Integer> meters,
      @Singular("histograms") Set<Integer> histograms,
      @Singular("timers") Set<Integer> timers,
      long queries, String name) throws Exception {

    if(threads.isEmpty()) {
      threads = Sets.newHashSet(1);
    }
    if(forkAtDepth.isEmpty()) {
      forkAtDepth = Sets.newHashSet(0);
    }
    if(depth.isEmpty()) {
      depth = Sets.newHashSet(0);
    }
    if(counters.isEmpty()) {
      counters = Sets.newHashSet(0);
    }
    if(meters.isEmpty()) {
      meters = Sets.newHashSet(0);
    }
    if(histograms.isEmpty()) {
      histograms = Sets.newHashSet(0);
    }
    if(timers.isEmpty()) {
      timers = Sets.newHashSet(0);
    }
    if(queries == 0) {
      queries = 50000000l;
    }
    if(Strings.isNullOrEmpty(name)) {
      name = "Test";
    }

    Set<List<Integer>> parameters = Sets.cartesianProduct(threads, depth, forkAtDepth, counters, meters,
        histograms, timers);

    Comparator<List<Integer>> comparator = new Comparator<List<Integer>>() {
      @Override public int compare(List<Integer> o1, List<Integer> o2) {
        Iterator<Integer> it1 = o1.iterator();
        Iterator<Integer> it2 = o2.iterator();

        while(it1.hasNext() && it2.hasNext()) {
          int compare = Integer.compare(it1.next(), it2.next());
          if(compare != 0) {
            return compare;
          }
        }
        if(it1.hasNext()) {
          return 1;
        } else if(it2.hasNext()) {
          return -1;
        } else {
          return 0;
        }
      }
    };

    TreeMap<List<Integer>, Double> results = Maps.newTreeMap(comparator);

    for(List<Integer> p : parameters) {
      Preconditions.checkArgument(p.size() == 7, "Parameter list should be of size 7.");
      results.put(p, singleTest().threads(p.get(0)).depth(p.get(1)).forkAtDepth(p.get(2)).counters(p.get(3)).
          meters(p.get(4)).histograms(p.get(5)).timers(p.get(6)).queries(queries).run());
    }

    System.out.println("===========================");
    System.out.println(name);
    System.out.println("===========================");
    System.out.println("Threads\tDepth\tForkAtDepth\tCounters\tMeters\tHistograms\tTimers\tQPS");
    for(Map.Entry<List<Integer>, Double> result : results.entrySet()) {
      List<Integer> p = result.getKey();
      System.out.println(String
          .format("%d\t%d\t%d\t%d\t%d\t%d\t%d\t%f", p.get(0), p.get(1), p.get(2), p.get(3), p.get(4), p.get(5),
              p.get(6), result.getValue()));
    }
  }

  /**
   * Runs a single performance test. Creates a {@link org.apache.gobblin.metrics.MetricContext} tree, spawns a number of threads,
   * uses and {@link Incrementer} to update the metrics repeatedly, then determines the achieved QPS in number
   * of iterations of {@link Incrementer} per second.
   *
   * @param threads Number of threads to spawn. Each thread will have an {@link Incrementer} and update metrics.
   * @param depth Depth of the {@link org.apache.gobblin.metrics.MetricContext} tree.
   * @param forkAtDepth If multiple threads, each thread has its own {@link org.apache.gobblin.metrics.MetricContext}. This
   *                    parameter sets the first level in the tree where the per-thread MetricContexts branch off.
   * @param counters Number of counters to generate per thread.
   * @param meters Number of meters to generate per thread.
   * @param histograms Number of histograms to generate per thread.
   * @param timers Number of timers to generate per thread.
   * @param queries Number of increments to do, divided among all threads.
   * @return total QPS achieved (e.g. total increments per second in the {@link Incrementer}s)
   * @throws Exception
   */
  @Builder(buildMethodName = "run", builderMethodName = "singleTest")
  public static double _singleTest(int threads, int depth, int forkAtDepth, int counters, int meters, int histograms,
      int timers, long queries) throws Exception {

    System.gc();

    ExecutorService executorService = ExecutorsUtils.loggingDecorator(Executors.newFixedThreadPool(threads));
    if(queries == 0) {
      queries = 50000000l;
    }
    long queriesPerThread = queries / threads;
    long actualQueries = queriesPerThread * threads;

    MetricsUpdater commonUpdater = MetricsUpdater.builder().depth(forkAtDepth).build();

    List<Incrementer> incrementerList = Lists.newArrayList();
    while(incrementerList.size() < threads) {
      final MetricsUpdater metricsUpdater = MetricsUpdater.builder().baseContext(commonUpdater.getContext()).
          depth(depth - forkAtDepth).counters(counters).meters(meters).histograms(histograms).timers(timers).build();
      incrementerList.add(new Incrementer(queriesPerThread, new Runnable() {
        @Override public void run() {
          metricsUpdater.run();
        }
      }));
    }

    List<Future<Long>> incrementerFutures = Lists.newArrayList();
    for(Incrementer incrementer : incrementerList) {
      incrementerFutures.add(executorService.submit(incrementer));
    }

    long totalTime = 0;
    for(Future<Long> future : incrementerFutures) {
      totalTime += future.get();
    }
    double averageTime = (double) totalTime / threads;

    double qps = 1000 * (double)actualQueries / averageTime;
    log.info(String.format("Average qps: %f.", qps));

    executorService.shutdown();

    return qps;
  }

}
