/* * 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 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 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 gobblin.metrics.MetricContext} tree. * @param forkAtDepth If multiple threads, each thread has its own {@link 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 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 gobblin.metrics.MetricContext} tree. * @param forkAtDepth If multiple threads, each thread has its own {@link 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; } }