All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.hyperfoil.cli.commands.Wrk Maven / Gradle / Ivy

/*
 * Copyright 2018 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * Licensed 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 io.hyperfoil.cli.commands;

import io.hyperfoil.api.config.Benchmark;
import io.hyperfoil.api.config.Protocol;
import io.hyperfoil.api.http.HttpMethod;
import io.hyperfoil.api.statistics.LongValue;
import io.hyperfoil.api.statistics.StatisticsSnapshot;
import io.hyperfoil.cli.context.HyperfoilCommandInvocation;
import io.hyperfoil.api.config.BenchmarkBuilder;
import io.hyperfoil.api.config.PhaseBuilder;
import io.hyperfoil.core.builders.StepCatalog;
import io.hyperfoil.core.handlers.ByteBufSizeRecorder;
import io.hyperfoil.core.impl.LocalBenchmarkData;
import io.hyperfoil.core.impl.LocalSimulationRunner;
import io.hyperfoil.core.impl.statistics.StatisticsCollector;
import io.hyperfoil.core.util.Util;

import org.HdrHistogram.HistogramIterationValue;
import org.aesh.AeshRuntimeRunner;
import org.aesh.command.Command;
import org.aesh.command.CommandDefinition;
import org.aesh.command.CommandResult;
import org.aesh.command.invocation.CommandInvocation;
import org.aesh.command.option.Argument;
import org.aesh.command.option.Option;
import org.aesh.command.option.OptionList;
import org.aesh.utils.ANSI;
import org.aesh.utils.Config;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import static io.vertx.core.logging.LoggerFactory.LOGGER_DELEGATE_FACTORY_CLASS_NAME;


public class Wrk {

   //ignore logging when running in the console below severe
   static {
      Handler[] handlers = Logger.getLogger("").getHandlers();
      for (int index = 0; index < handlers.length; index++) {
         handlers[index].setLevel(Level.SEVERE);
      }
   }


   public static void main(String[] args) {

      //set logger impl
      System.setProperty(LOGGER_DELEGATE_FACTORY_CLASS_NAME, "io.vertx.core.logging.Log4j2LogDelegateFactory");

      try {
         AeshRuntimeRunner.builder().command(WrkCommand.class).args(args).execute();
      } catch (Exception e) {
         System.out.println("Failed to execute command:" + e.getMessage());
         e.printStackTrace();
         //todo: should provide help info here, will be added in newer version of æsh
         //System.out.println(runtime.commandInfo("wrk"));
      }
   }

   @CommandDefinition(name = "wrk", description = "Runs a workload simluation against one endpoint using the same vm")
   public class WrkCommand implements Command {
      @Option(shortName = 'c', description = "Total number of HTTP connections to keep open", required = true)
      int connections;

      @Option(shortName = 'd', description = "Duration of the test, e.g. 2s, 2m, 2h", required = true)
      String duration;

      @Option(shortName = 't', description = "Total number of threads to use.")
      int threads;

      @Option(shortName = 'R', description = "Work rate (throughput)", required = true)
      int rate;

      @Option(shortName = 's', description = "!!!NOT SUPPORTED: LuaJIT script")
      String script;

      @Option(shortName = 'h', hasValue = false, overrideRequired = true)
      boolean help;

      @OptionList(shortName = 'H', name = "header", description = "HTTP header to add to request, e.g. \"User-Agent: wrk\"")
      List headers;

      @Option(description = "Print detailed latency statistics", hasValue = false)
      boolean latency;

      @Option(description = "Record a timeout if a response is not received within this amount of time.", defaultValue = "60s")
      String timeout;

      @Argument(description = "URL that should be accessed", required = true)
      String url;

      String path;
      String[][] parsedHeaders;

      private boolean executedInCli = false;

      @Override
      public CommandResult execute(CommandInvocation commandInvocation) {
         if (help) {
            commandInvocation.println(commandInvocation.getHelpInfo("wrk"));
            return CommandResult.SUCCESS;
         }
         if (script != null) {
            commandInvocation.println("Scripting is not supported at this moment.");
         }
         if (!url.startsWith("http://") && !url.startsWith("https://")) {
            url = "http://" + url;
         }
         URI uri;
         try {
            uri = new URI(url);
         } catch (URISyntaxException e) {
            commandInvocation.println("Failed to parse URL: " + e.getMessage());
            return CommandResult.FAILURE;
         }
         path = uri.getPath();
         if (uri.getQuery() != null) {
            path = path + "?" + uri.getQuery();
         }
         if (uri.getFragment() != null) {
            path = path + "#" + uri.getFragment();
         }
         if (headers != null) {
            parsedHeaders = new String[headers.size()][];
            for (int i = 0; i < headers.size(); i++) {
               String h = headers.get(i);
               int colonIndex = h.indexOf(':');
               if (colonIndex < 0) {
                  commandInvocation.println(String.format("Cannot parse header '%s', ignoring.", h));
                  continue;
               }
               String header = h.substring(0, colonIndex).trim();
               String value = h.substring(colonIndex + 1).trim();
               parsedHeaders[i] = new String[]{ header, value };
            }
         } else {
            parsedHeaders = null;
         }
         //check if we're running in the cli
         if (commandInvocation instanceof HyperfoilCommandInvocation)
            executedInCli = true;

         Protocol protocol = Protocol.fromScheme(uri.getScheme());
         BenchmarkBuilder builder = new BenchmarkBuilder(null, new LocalBenchmarkData())
               .name("wrk " + new SimpleDateFormat("YY/MM/dd HH:mm:ss").format(new Date()))
               .http()
               .protocol(protocol).host(uri.getHost()).port(protocol.portOrDefault(uri.getPort()))
               .sharedConnections(connections)
               .endHttp()
               .threads(this.threads);

         addPhase(builder, "calibration", "1s");
         addPhase(builder, "test", duration).startAfter("calibration").maxDuration(duration);
         Benchmark benchmark = builder.build();

         // TODO: allow running the benchmark from remote instance
         LocalSimulationRunner runner = new LocalSimulationRunner(benchmark);
         commandInvocation.println("Running for " + duration + " test @ " + url);
         commandInvocation.println(threads + " threads and " + connections + " connections");

         if (executedInCli) {
            ((HyperfoilCommandInvocation) commandInvocation).context().setBenchmark(benchmark);
            startRunnerInCliMode(runner, benchmark, (HyperfoilCommandInvocation) commandInvocation);
         } else {
            runner.run();
            StatisticsCollector collector = new StatisticsCollector(benchmark);
            runner.visitStatistics(collector);
            StatisticsSnapshot total = new StatisticsSnapshot();
            collector.visitStatistics((phase, stepId, metric, stats, countDown) -> {
               if ("test".equals(phase.name())) {
                  stats.addInto(total);
               }
            }, null);
            printStats(total, commandInvocation);
         }

         return CommandResult.SUCCESS;
      }

      private void startRunnerInCliMode(LocalSimulationRunner runner, Benchmark benchmark,
                                        HyperfoilCommandInvocation invocation) {

         CountDownLatch latch = new CountDownLatch(1);
         Thread thread = new Thread(() -> {
            runner.run();
            latch.countDown();
         });
         thread.start();

         long startTime = System.currentTimeMillis();
         StatisticsCollector collector = new StatisticsCollector(benchmark);
         StatisticsSnapshot total = new StatisticsSnapshot();
         while (latch.getCount() > 0) {

            long duration = System.currentTimeMillis() - startTime;
            if (duration % 800 == 0) {
               invocation.getShell().write(ANSI.CURSOR_START);
               invocation.getShell().write(ANSI.ERASE_WHOLE_LINE);
               runner.visitStatistics(collector);
               collector.visitStatistics((phase, stepId, metric, stats, countDown) -> {
                  if ("test".equals(phase.name())) {
                     double durationSeconds = (stats.histogram.getEndTimeStamp() - stats.histogram.getStartTimeStamp()) / 1000d;
                     invocation.print("Requests/sec: " + String.format("%.02f", stats.histogram.getTotalCount() / durationSeconds));
                     stats.addInto(total);
                  }
               }, null);

               try {
                  Thread.sleep(10);
               } catch (InterruptedException e) {
                  //if we're interrupted, lets try to interrupt the benchmark...
                  invocation.println("Interrupt received, trying to abort run...");
                  thread.interrupt();
                  latch.countDown();
               }
            }
         }
         invocation.context().setRunning(false);
         invocation.println(Config.getLineSeparator() + "benchmark finished");
         runner.visitStatistics(collector);
         collector.visitStatistics((phase, stepId, metric, stats, countDown) -> {
            if ("test".equals(phase.name())) {
               stats.addInto(total);
            }
         }, null);
         printStats(total, invocation);
      }

      private PhaseBuilder addPhase(BenchmarkBuilder benchmarkBuilder, String phase, String duration) {
         return benchmarkBuilder.addPhase(phase).constantPerSec(rate)
               .duration(duration)
               .maxSessionsEstimate(rate * 15)
               .scenario()
               .initialSequence("request")
               .step(StepCatalog.class).httpRequest(HttpMethod.GET)
               .path(path)
               .headerAppender((session, request) -> {
                  if (parsedHeaders != null) {
                     for (String[] header : parsedHeaders) {
                        request.putHeader(header[0], header[1]);
                     }
                  }
               })
               .timeout(timeout)
               .handler()
               .rawBytesHandler(new ByteBufSizeRecorder("bytes"))
               .endHandler()
               .endStep()
               .step(StepCatalog.class).awaitAllResponses()
               .endSequence()
               .endScenario();
      }

      private void printStats(StatisticsSnapshot stats, CommandInvocation invocation) {
         long dataRead = ((LongValue) stats.custom.get("bytes")).value();
         double durationSeconds = (stats.histogram.getEndTimeStamp() - stats.histogram.getStartTimeStamp()) / 1000d;
         invocation.println("                  Avg     Stdev       Max");
         invocation.println("Latency:    " + Util.prettyPrintNanos((long) stats.histogram.getMean()) + " "
               + Util.prettyPrintNanos((long) stats.histogram.getStdDeviation()) + " " + Util.prettyPrintNanos(stats.histogram.getMaxValue()));
         if (latency) {
            invocation.println("Latency Distribution");
            for (double percentile : new double[]{ 0.5, 0.75, 0.9, 0.99, 0.999, 0.9999, 0.99999, 1.0 }) {
               invocation.println(String.format("%7.3f", 100 * percentile) + " " + Util.prettyPrintNanos(stats.histogram.getValueAtPercentile(100 * percentile)));
            }
            invocation.println("----------------------------------------------------------");
            invocation.println("Detailed Percentile Spectrum");
            invocation.println("    Value  Percentile  TotalCount  1/(1-Percentile)");
            for (HistogramIterationValue value : stats.histogram.percentiles(5)) {
               invocation.println(Util.prettyPrintNanos(value.getValueIteratedTo()) + " " + String.format("%9.5f%%  %10d  %15.2f",
                     value.getPercentile(), value.getTotalCountToThisValue(), 100 / (100 - value.getPercentile())));
            }
            invocation.println("----------------------------------------------------------");
         }
         invocation.println(stats.histogram.getTotalCount() + " requests in " + durationSeconds + "s, " + Util.prettyPrintData(dataRead) + " read");
         invocation.println("Requests/sec: " + String.format("%.02f", stats.histogram.getTotalCount() / durationSeconds));
         if (stats.errors() > 0) {
            invocation.println("Socket errors: connect " + stats.connectFailureCount + ", reset " + stats.resetCount + ", timeout " + stats.timeouts);
            invocation.println("Non-2xx or 3xx responses: " + stats.status_4xx + stats.status_5xx + stats.status_other);
         }
         invocation.println("Transfer/sec: " + Util.prettyPrintData(dataRead / durationSeconds));
      }

   }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy