com.webtide.jetty.load.generator.jenkins.LoadGeneratorBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.webtide.jetty.load.generator.jenkins.LoadGeneratorBuilder.java

Source

//
//  ========================================================================
//  Copyright (c) 1995-2016 Webtide LLC, Olivier Lamy
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================

package com.webtide.jetty.load.generator.jenkins;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.HealthReport;
import hudson.model.JDK;
import hudson.model.Node;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.ArgumentListBuilder;
import hudson.util.ReflectionUtils;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.tasks.SimpleBuildStep;
import org.HdrHistogram.Histogram;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.mortbay.jetty.load.generator.LoadGenerator;
import org.mortbay.jetty.load.generator.Resource;
import org.mortbay.jetty.load.generator.listeners.CollectorInformations;
import org.mortbay.jetty.load.generator.listeners.report.DetailledTimeReportListener;
import org.mortbay.jetty.load.generator.listeners.report.GlobalSummaryListener;
import org.mortbay.jetty.load.generator.listeners.report.SummaryReport;
import org.mortbay.jetty.load.generator.listeners.responsetime.ResponseNumberPerPath;
import org.mortbay.jetty.load.generator.listeners.responsetime.ResponsePerStatus;
import org.mortbay.jetty.load.generator.listeners.responsetime.TimePerPathListener;
import org.mortbay.jetty.load.generator.starter.LoadGeneratorStarterArgs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 */
public class LoadGeneratorBuilder extends Builder implements SimpleBuildStep {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoadGeneratorBuilder.class);

    private final String resourceGroovy;

    private final String host;

    private final String port;

    private final String users;

    private final String resourceFromFile;

    private final String runningTime;

    private final TimeUnit runningTimeUnit;

    private final String runIteration;

    private final String resourceRate;

    private List<Resource.NodeListener> nodeListeners = new ArrayList<>();

    private Resource loadResource;

    private LoadGeneratorStarterArgs.Transport transport;

    private boolean secureProtocol;

    private String jdkName;

    private String jvmExtraArgs;

    private String generatorNumber = "1";

    private String warmupNumber = "0";

    private String alpnVersion;

    private int threadsNumber = 1;

    // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
    @DataBoundConstructor
    public LoadGeneratorBuilder(String resourceGroovy, String host, String port, String users,
            String resourceFromFile, String runningTime, TimeUnit runningTimeUnit, String runIteration,
            String resourceRate, LoadGeneratorStarterArgs.Transport transport, boolean secureProtocol,
            int threadsNumber) {
        this.resourceGroovy = Util.fixEmptyAndTrim(resourceGroovy);
        this.host = host;
        this.port = port;
        this.users = users;
        this.resourceFromFile = resourceFromFile;
        this.runningTime = runningTime;
        this.runningTimeUnit = runningTimeUnit == null ? TimeUnit.SECONDS : runningTimeUnit;
        this.runIteration = runIteration;
        this.resourceRate = StringUtils.isEmpty(resourceRate) ? "1" : resourceRate;
        this.transport = transport;
        this.secureProtocol = secureProtocol;
        this.threadsNumber = threadsNumber;
    }

    public LoadGeneratorBuilder(Resource resource, String host, String port, String users, //
            String resourceFromFile, String runningTime, TimeUnit runningTimeUnit, String runIteration, //
            String resourceRate, LoadGeneratorStarterArgs.Transport transport, boolean secureProtocol, //
            String jvmExtraArgs, String generatorNumber, int threadsNumber) {

        this(null, host, port, users, resourceFromFile, runningTime, runningTimeUnit, runIteration, resourceRate,
                transport, secureProtocol, threadsNumber);
        this.loadResource = resource;
        this.jvmExtraArgs = jvmExtraArgs;
        this.generatorNumber = generatorNumber;
    }

    public String getResourceGroovy() {
        return resourceGroovy;
    }

    public String getHost() {
        return host;
    }

    public String getPort() {
        return port;
    }

    public String getUsers() {
        return users;
    }

    public String getResourceFromFile() {
        return resourceFromFile;
    }

    public String getRunningTime() {
        return runningTime;
    }

    public TimeUnit getRunningTimeUnit() {
        return runningTimeUnit;
    }

    public String getRunIteration() {
        return runIteration;
    }

    public void addNodeListener(Resource.NodeListener nodeListener) {
        this.nodeListeners.add(nodeListener);
    }

    public String getResourceRate() {
        return resourceRate;
    }

    public Resource getLoadResource() {
        return loadResource;
    }

    public void setLoadResource(Resource loadResource) {
        this.loadResource = loadResource;
    }

    public LoadGeneratorStarterArgs.Transport getTransport() {
        return transport;
    }

    public boolean isSecureProtocol() {
        return secureProtocol;
    }

    public String getJdkName() {
        return jdkName;
    }

    @DataBoundSetter
    public void setJdkName(String jdkName) {
        this.jdkName = jdkName;
    }

    public String getJvmExtraArgs() {
        return jvmExtraArgs;
    }

    @DataBoundSetter
    public void setJvmExtraArgs(String jvmExtraArgs) {
        this.jvmExtraArgs = jvmExtraArgs;
    }

    public String getGeneratorNumber() {
        return generatorNumber;
    }

    @DataBoundSetter
    public void setGeneratorNumber(String generatorNumber) {
        this.generatorNumber = generatorNumber;
    }

    public String getWarmupNumber() {
        return warmupNumber;
    }

    @DataBoundSetter
    public void setWarmupNumber(String warmupNumber) {
        this.warmupNumber = warmupNumber;
    }

    public String getAlpnVersion() {
        return alpnVersion;
    }

    @DataBoundSetter
    public void setAlpnVersion(String alpnVersion) {
        this.alpnVersion = alpnVersion;
    }

    public int getThreadsNumber() {
        return threadsNumber;
    }

    @DataBoundSetter
    public void setThreadsNumber(int threadsNumber) {
        this.threadsNumber = threadsNumber;
    }

    @Override
    public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
        try {
            doRun(listener, build.getWorkspace(), build.getRootBuild(), launcher);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        return true;
    }

    @Override
    public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath filePath, @Nonnull Launcher launcher,
            @Nonnull TaskListener taskListener) throws InterruptedException, IOException {
        LOGGER.debug("simpleBuildStep perform");

        try {
            doRun(taskListener, filePath, run, launcher);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    public void doRun(TaskListener taskListener, FilePath workspace, Run<?, ?> run, Launcher launcher)
            throws Exception {

        Resource resource = this.loadResource == null ? loadResource(workspace) : this.loadResource;

        if (resource == null) {
            taskListener.getLogger().println("resource profile must be set, Build ABORTED");
            LOGGER.error("resource profile must be set, Build ABORTED");
            run.setResult(Result.ABORTED);
            return;
        }

        runProcess(taskListener, workspace, run, launcher, resource);

    }

    /**
     * Expand tokens with token macro and build variables
     */
    protected static String expandTokens(TaskListener listener, String str, Run<?, ?> run) throws Exception {
        if (str == null) {
            return null;
        }
        try {
            Class<?> clazz = Class.forName("org.jenkinsci.plugins.tokenmacro.TokenMacro");
            Method expandMethod = ReflectionUtils.findMethod(clazz, "expand", new Class[] { AbstractBuild.class, //
                    TaskListener.class, //
                    String.class });
            return (String) expandMethod.invoke(null, run, listener, str);
            //opts = TokenMacro.expand(this, listener, opts);
        } catch (Exception tokenException) {
            //Token plugin not present. Ignore, this is OK.
            LOGGER.trace("Ignore problem in expanding tokens", tokenException);
        } catch (LinkageError linkageError) {
            // Token plugin not present. Ignore, this is OK.
            LOGGER.trace("Ignore problem in expanding tokens", linkageError);
        }

        str = StrSubstitutor.replace(str, run.getEnvironment(listener));

        return str;
    }

    protected void runProcess(TaskListener taskListener, FilePath workspace, Run<?, ?> run, Launcher launcher,
            Resource resource) throws Exception {

        // -------------------------
        // listeners to get data files
        // -------------------------
        List<Resource.NodeListener> nodeListeners = new ArrayList<>();

        Path resultFilePath = Paths.get(launcher.getChannel() //
                .call(new LoadGeneratorProcessFactory.RemoteTmpFileCreate()));

        ValuesFileWriter valuesFileWriter = new ValuesFileWriter(resultFilePath);
        nodeListeners.add(valuesFileWriter);

        List<LoadGenerator.Listener> loadGeneratorListeners = new ArrayList<>();
        loadGeneratorListeners.add(valuesFileWriter);

        Path statsResultFilePath = Paths.get(launcher.getChannel() //
                .call(new LoadGeneratorProcessFactory.RemoteTmpFileCreate()));

        ArgumentListBuilder args = getArgsProcess(resource, launcher.getComputer(), taskListener, //
                run, statsResultFilePath.toString());

        String monitorUrl = getMonitorUrl(taskListener, run);

        String alpnBootVersion = getAlpnVersion();
        // well a quick marker to say we do not need alpn
        if (getTransport() == LoadGeneratorStarterArgs.Transport.HTTP //
                || getTransport() == LoadGeneratorStarterArgs.Transport.HTTPS) {
            alpnBootVersion = "N/A";
        }

        LOGGER.info("load generator args:" + args.toString());

        new LoadGeneratorProcessRunner().runProcess(taskListener, workspace, launcher, //
                this.jdkName, getCurrentNode(launcher.getComputer()), //
                nodeListeners, loadGeneratorListeners, //
                args.toList(), getJvmExtraArgs(), //
                alpnBootVersion, //
                AlpnBootVersions.getInstance().getJdkVersionAlpnBootVersion());

        String stats = workspace.child(statsResultFilePath.toString()).readToString();

        TimePerPathListener timePerPathListener = new TimePerPathListener(false);
        GlobalSummaryListener globalSummaryListener = new GlobalSummaryListener();
        // this one will use some memory for a long load test!!
        // FIXME find a way to flush that somewhere!!
        DetailledTimeReportListener detailledTimeReportListener = new DetailledTimeReportListener();

        // -----------------------------
        // handle response time reports
        // -----------------------------

        ResponsePerStatus responsePerStatus = new ResponsePerStatus();

        ResponseNumberPerPath responseNumberPerPath = new ResponseNumberPerPath();

        nodeListeners.clear();
        if (this.nodeListeners != null) {
            nodeListeners.addAll(this.nodeListeners);
        }
        nodeListeners.add(responseNumberPerPath);
        nodeListeners.add(timePerPathListener);
        nodeListeners.add(globalSummaryListener);
        nodeListeners.add(detailledTimeReportListener);
        nodeListeners.add(responsePerStatus);

        LOGGER.info("LoadGenerator parsing response result files");

        //-------------------------------------------------
        // time values
        //-------------------------------------------------
        parseTimeValues(workspace, resultFilePath, nodeListeners);

        //-------------------------------------------------
        // Monitor values
        //-------------------------------------------------
        String monitorJson = getMonitorValues(monitorUrl, taskListener);

        taskListener.getLogger().print("monitorJson: " + monitorJson);

        Map<String, Object> monitoringResultMap = null;

        try {
            monitoringResultMap = new ObjectMapper() //
                    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) //
                    .readValue(monitorJson, Map.class);
        } catch (Exception e) {
            LOGGER.warn("skip error parsing json monitoring result");
        }
        // manage results

        SummaryReport summaryReport = new SummaryReport(run.getId());

        timePerPathListener.getResponseTimePerPath().entrySet().stream().forEach(entry -> {
            String path = entry.getKey();
            Histogram histogram = entry.getValue();

            AtomicInteger number = responseNumberPerPath.getResponseNumberPerPath().get(path);
            LOGGER.debug("responseTimePerPath: {} - mean: {}ms - number: {}", //
                    path, //
                    TimeUnit.NANOSECONDS.toMillis(Math.round(histogram.getMean())), //
                    number.get());
            summaryReport.addResponseTimeInformations(path, new CollectorInformations(histogram, //
                    TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS));
        });

        timePerPathListener.getLatencyTimePerPath().entrySet().stream().forEach(entry -> {
            String path = entry.getKey();
            Histogram histogram = entry.getValue();

            AtomicInteger number = responseNumberPerPath.getResponseNumberPerPath().get(path);
            LOGGER.debug("responseTimePerPath: {} - mean: {}ms - number: {}", //
                    path, //
                    TimeUnit.NANOSECONDS.toMillis(Math.round(histogram.getMean())), //
                    number.get());
            summaryReport.addLatencyTimeInformations(path, new CollectorInformations(histogram, //
                    TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS));
        });

        // FIXME calculate score from previous build
        HealthReport healthReport = new HealthReport(30, "text");

        Map<String, List<ResponseTimeInfo>> allResponseInfoTimePerPath = new HashMap<>();

        detailledTimeReportListener.getDetailledLatencyTimeValuesReport().getEntries().stream().forEach(entry -> {
            List<ResponseTimeInfo> responseTimeInfos = allResponseInfoTimePerPath.get(entry.getPath());
            if (responseTimeInfos == null) {
                responseTimeInfos = new ArrayList<>();
                allResponseInfoTimePerPath.put(entry.getPath(), responseTimeInfos);
            }
            responseTimeInfos.add(new ResponseTimeInfo(entry.getTimeStamp(), //
                    TimeUnit.NANOSECONDS.toMillis(entry.getTime()), //
                    entry.getHttpStatus()));
        });

        run.addAction(new LoadGeneratorBuildAction(healthReport, //
                summaryReport, //
                new CollectorInformations(globalSummaryListener.getResponseTimeHistogram().getIntervalHistogram(),
                        //
                        TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS), //
                new CollectorInformations(globalSummaryListener.getLatencyTimeHistogram().getIntervalHistogram(),
                        //
                        TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS), //
                allResponseInfoTimePerPath, run, monitoringResultMap, stats));

        // cleanup

        getCurrentNode(launcher.getComputer()) //
                .getChannel() //
                .call(new LoadGeneratorProcessFactory.DeleteTmpFile(resultFilePath.toString()));

        LOGGER.info("LoadGenerator end");
    }

    protected void parseTimeValues(FilePath workspace, Path responseTimeResultFilePath,
            List<Resource.NodeListener> nodeListeners) throws Exception {
        Path responseTimeResultFile = Files.createTempFile("loadgenerator_result_responsetime", ".csv");

        workspace.child(responseTimeResultFilePath.toString())
                .copyTo(Files.newOutputStream(responseTimeResultFile));

        CSVParser csvParser = new CSVParser(Files.newBufferedReader(responseTimeResultFile),
                CSVFormat.newFormat('|'));

        csvParser.forEach(strings -> {
            Values values = new Values() //
                    .eventTimestamp(Long.parseLong(strings.get(0))) //
                    .method(strings.get(1)) //
                    .path(strings.get(2)) //
                    .status(Integer.parseInt(strings.get(3))) //
                    .size(Long.parseLong(strings.get(4))) //
                    .responseTime(Long.parseLong(strings.get(5))) //
                    .latencyTime(Long.parseLong(strings.get(6)));

            for (Resource.NodeListener listener : nodeListeners) {
                listener.onResourceNode(values.getInfo());
            }
        });

        Files.deleteIfExists(responseTimeResultFile);
    }

    protected ArgumentListBuilder getArgsProcess(Resource resource, Computer computer, TaskListener taskListener,
            Run<?, ?> run, String statsResultFilePath) throws Exception {

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        StringWriter stringWriter = new StringWriter();
        objectMapper.writeValue(stringWriter, resource);
        stringWriter.close();

        final String tmpFilePath = getCurrentNode(computer) //
                .getChannel().call(new CopyResource(stringWriter.toString()));

        ArgumentListBuilder cmdLine = new ArgumentListBuilder();

        cmdLine.add("--resource-json-path").add(tmpFilePath) //
                .add("--host").add(expandTokens(taskListener, host, run)) //
                .add("--port").add(expandTokens(taskListener, port, run)) //
                .add("--transport").add(StringUtils.lowerCase(this.getTransport().toString())) //
                .add("--users").add(expandTokens(taskListener, users, run)) //
                .add("--resource-rate").add(expandTokens(taskListener, resourceRate, run)) //
                .add("--stats-file").add(statsResultFilePath) //
                .add("--threads").add(threadsNumber < 1 ? 1 : threadsNumber) //
                .add("--scheme").add(isSecureProtocol() ? "https" : "http");

        int iterationRuns = NumberUtils.toInt(expandTokens(taskListener, runIteration, run), 0);

        if (iterationRuns > 0) {
            cmdLine.add("--iterations").add(Integer.toString(iterationRuns));
        } else {
            cmdLine.add("--running-time").add(expandTokens(taskListener, runningTime, run));
            cmdLine.add("--running-time-unit");
            switch (this.runningTimeUnit) {
            case HOURS:
                cmdLine.add("h");
                break;
            case MINUTES:
                cmdLine.add("m");
                break;
            case SECONDS:
                cmdLine.add("s");
                break;
            case MILLISECONDS:
                cmdLine.add("ms");
                break;
            default:
                throw new IllegalArgumentException(runningTimeUnit + " is not recognized");
            }
        }

        int warmupNumber = StringUtils.isNotEmpty(getWarmupNumber()) ? //
                Integer.parseInt(expandTokens(taskListener, this.getWarmupNumber(), run)) : -1;

        if (warmupNumber > 0) {
            cmdLine.add("--warmup-iterations").add(warmupNumber);
        }

        // FIXME deleting tmp file
        // getCurrentNode().getChannel().call( new DeleteTmpFile( tmpFilePath ) );
        LOGGER.debug("finish");
        LOGGER.info("load generator starter args:" + cmdLine.toString());
        return cmdLine;

    }

    protected String getMonitorUrl(TaskListener taskListener, Run run) throws Exception {
        String url = (this.isSecureProtocol() ? "https" : "http") //
                + "://" + expandTokens(taskListener, this.host, run);

        if (StringUtils.isNotEmpty(this.port)) {
            url = url //
                    + ":" + expandTokens(taskListener, this.port, run) //
                    + "/monitor";
        }
        return url;
    }

    protected String getMonitorValues(String monitorUrl, TaskListener taskListener) throws Exception {
        HttpClient httpClient = new HttpClient();
        try {
            httpClient.start();
            ContentResponse contentResponse = httpClient.newRequest(monitorUrl + "?stats=true").send();
            return contentResponse == null ? "" : contentResponse.getContentAsString();
        } catch (Exception e) {
            taskListener.getLogger().println("error calling stats monitorUrl:" + monitorUrl + "," + e.getMessage());
            e.printStackTrace();
            return "";
        } finally {
            httpClient.stop();
        }
    }

    static class CopyResource extends MasterToSlaveCallable<String, IOException> implements Serializable {
        private String resourceAsJson;

        public CopyResource(String resourceAsJson) {
            this.resourceAsJson = resourceAsJson;
        }

        @Override
        public String call() throws IOException {
            Path tmpPath = Files.createTempFile("profile", ".tmp");
            Files.write(tmpPath, resourceAsJson.getBytes());
            return tmpPath.toString();
        }
    }

    protected Resource loadResource(FilePath workspace) throws Exception {

        Resource resource = null;

        String groovy = StringUtils.trim(this.getResourceGroovy());

        String profileFromPath = getResourceFromFile();

        if (StringUtils.isBlank(groovy) && StringUtils.isNotBlank(profileFromPath)) {
            FilePath profileGroovyFilePath = workspace.child(profileFromPath);
            groovy = IOUtils.toString(profileGroovyFilePath.read());
        }

        if (StringUtils.isNotBlank(groovy)) {
            CompilerConfiguration compilerConfiguration = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
            compilerConfiguration.setDebug(true);
            compilerConfiguration.setVerbose(true);

            compilerConfiguration.addCompilationCustomizers(
                    new ImportCustomizer().addStarImports("org.eclipse.jetty.load.generator"));

            GroovyShell interpreter = new GroovyShell(Resource.class.getClassLoader(), //
                    new Binding(), //
                    compilerConfiguration);

            resource = (Resource) interpreter.evaluate(groovy);
        }

        return resource;
    }

    protected Node getCurrentNode(Computer computer) {
        Node node = null;
        // well avoid NPE when running workflow testing
        //return Executor.currentExecutor().getOwner().getNode();
        if (Computer.currentComputer() != null) {
            node = Computer.currentComputer().getNode();
        }
        if (node == null) {
            node = computer.getNode();
        }
        return node;
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    @Symbol("loadgenerator")
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {

        private static final List<TimeUnit> TIME_UNITS = Arrays.asList(TimeUnit.DAYS, //
                TimeUnit.HOURS, //
                TimeUnit.MINUTES, //
                TimeUnit.SECONDS, //
                TimeUnit.MILLISECONDS);

        private static final List<LoadGeneratorStarterArgs.Transport> TRANSPORTS = Arrays.asList(
                LoadGeneratorStarterArgs.Transport.HTTP, //
                LoadGeneratorStarterArgs.Transport.HTTPS, //
                LoadGeneratorStarterArgs.Transport.H2, //
                LoadGeneratorStarterArgs.Transport.H2C);

        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            return "HTTP LoadGenerator";
        }

        public List<TimeUnit> getTimeUnits() {
            return TIME_UNITS;
        }

        public List<JDK> getJdks() {
            return Jenkins.getInstance().getJDKs();
        }

        public List<LoadGeneratorStarterArgs.Transport> getTransports() {
            return TRANSPORTS;
        }

        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            // indicates that this builder can be used with all kinds of project types 
            return true;
        }

        /*
        @Override
        public boolean configure( StaplerRequest req, JSONObject formData )
        throws FormException
        {
        save();
        return super.configure( req, formData );
        }*/

    }

}