org.wildfly.swarm.proc.Monitor.java Source code

Java tutorial

Introduction

Here is the source code for org.wildfly.swarm.proc.Monitor.java

Source

/*
 * *
 *  * Copyright 2015-2016 Red Hat, Inc, and 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 org.wildfly.swarm.proc;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import com.github.zafarkhaja.semver.Version;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.hyperic.sigar.ProcMem;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.ptql.ProcessFinder;

import static org.wildfly.swarm.proc.Units.bytesToMegabytes;

/**
 * @author Heiko Braun
 * @since 28/04/16
 */
public class Monitor {

    public Monitor(CommandLine cmd) {
        skipTests = cmd.hasOption("skip");

        baseDir = new File(cmd.getOptionValue("b"));
        workDir = new File(cmd.getOptionValue("w"));
        archiveDir = cmd.hasOption("a") ? Optional.of(new File(cmd.getOptionValue("a"))) : Optional.empty();

        outputFile = cmd.hasOption("o") ? Optional.of(new File(cmd.getOptionValue("o"))) : Optional.empty();
        comparisonOutputFile = cmd.hasOption("c") ? Optional.of(new File(cmd.getOptionValue("c")))
                : Optional.empty();

        System.out.println("Base dir: " + baseDir.getAbsolutePath());

        if (archiveDir.isPresent())
            System.out.println("Archive dir: " + archiveDir.get().getAbsolutePath());

        if (archiveDir.isPresent() && !archiveDir.get().exists())
            throw new RuntimeException("Archive does not exist: " + archiveDir.get().getAbsolutePath());

        collector = (outputFile.isPresent() && !skipTests) ? new CSVCollector(outputFile.get())
                : new SystemOutCollector();

        this.NUM_ITERATIONS = cmd.hasOption("n") ? Integer.valueOf(cmd.getOptionValue("n")) : 10;
    }

    public static void main(String[] args) throws Exception {

        Options options = new Options();

        options.addOption(Option.builder("b").longOpt("base").required(true)
                .desc("the WildFly Swarm examples directory").hasArg().build());

        options.addOption(Option.builder("a").longOpt("archive").required(false)
                .desc("the directory with previous performance results").hasArg().build());

        options.addOption(Option.builder("o").longOpt("output").required(false)
                .desc("the .csv file to store the current test results").hasArg().build());

        options.addOption(Option.builder("skip").longOpt("skip-tests").required(false)
                .desc("skip test execution phase").build());

        options.addOption(Option.builder("n").longOpt("number-iterations").required(false).hasArg()
                .desc("number of iterations per test").build());

        options.addOption(Option.builder("w").longOpt("workdir").required(true).hasArg()
                .desc("where to store testing artifacts").build());

        options.addOption(Option.builder("c").longOpt("comparison-csv").required(false).hasArg()
                .desc("the .csv file to store the comparison").build());

        CommandLineParser parser = new DefaultParser();
        CommandLine cmd = null;
        try {
            cmd = parser.parse(options, args);
        } catch (ParseException e) {
            usage(options);
        }

        for (Option opt : options.getOptions()) {
            if (opt.isRequired() && !cmd.hasOption(opt.getOpt())) {
                usage(options);
            }
        }

        // perform tests
        new Monitor(cmd).run();
    }

    private static void usage(Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("Monitor", "WildFly Swarm Performance Monitor", options, "", true);
        System.exit(-1);
    }

    private void run() throws Exception {
        long total0 = System.currentTimeMillis();

        if (!skipTests) {
            // test criteria
            Properties props = new Properties();
            props.load(Monitor.class.getClassLoader().getResourceAsStream("swarm-apps.properties"));

            // first phase: main test execution loop
            for (Object o : props.keySet()) {
                String swarmFile = (String) o;
                String httpCheck = (String) props.get(o);

                File file = new File(this.baseDir, swarmFile);
                String id = file.getAbsolutePath();

                if (!file.exists())
                    throw new RuntimeException("File does not exist: " + file.getAbsolutePath());

                collector.onBegin(id);
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    runTest(i, file, httpCheck, collector);
                }
                collector.onFinish(id);
            }
            collector.close();

            System.out.println("Test Execution Time: " + (System.currentTimeMillis() - total0) + "ms");

        } else {
            System.out.println("Test execution has been skipped.");
        }

        // second phase: compare with previous, archived results
        if (outputFile.isPresent() && archiveDir.isPresent()) {
            Optional<ArchivedResult> prev = getPreviousResults(outputFile.get().toPath(), this.archiveDir.get());
            if (prev.isPresent()) {
                // maybe we should check here that outputFile is a valid CSV (in case we skipped the tests and are running
                // against an already existing file), but if it isn't, things will fail down the line anyway
                checkDeviation(this.outputFile.get(), prev.get());
            } else {
                System.out.println("Performance comparison skipped.");
            }
        }
    }

    private void checkDeviation(File testResult, ArchivedResult archivedResult) throws Exception {
        System.out.println("Comparing against " + archivedResult.getVersion());

        List<CSVRecord> current = loadCSV(testResult).getRecords();
        List<CSVRecord> previous = loadCSV(archivedResult.getFile()).getRecords();

        if (comparisonOutputFile.isPresent()) {
            String previousName = archivedResult.getVersion().toString();
            String currentName = testResult.getName().replaceFirst(".csv$", "");
            new CsvOutputComparator(comparisonOutputFile.get(), previousName, currentName).compare(previous,
                    current);
        }

        new FailFastComparator(10.00).compare(previous, current);
    }

    private CSVParser loadCSV(File file) throws Exception {
        Reader input = Files.newBufferedReader(file.toPath());
        return CSVFormat.DEFAULT.withHeader().parse(input);
    }

    private static boolean isSameFile(Path path1, Path path2) {
        try {
            return Files.isSameFile(path1, path2);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private Optional<ArchivedResult> getPreviousResults(Path currentOutput, File dir) throws IOException {
        try (Stream<Path> stream = Files.walk(dir.toPath(), 1)) {
            return stream.filter(path -> Files.isRegularFile(path))
                    .filter(path -> path.getFileName().toString().endsWith(".csv"))
                    .filter(path -> !isSameFile(currentOutput, path)).map(path -> {
                        String fileName = path.getFileName().toString();
                        Version version = Version.valueOf(fileName.substring(0, fileName.lastIndexOf(".")));
                        return new ArchivedResult(version, path.toFile());
                    }).sorted(Comparator.comparing(ArchivedResult::getVersion).reversed()).findFirst();
        }
    }

    /**
     * Main test execution. Spawns an external process
     * @param iteration
     * @param file
     * @param httpCheck
     * @param collector
     */
    private void runTest(int iteration, File file, String httpCheck, final Collector collector) {

        System.out.println("Testing " + file.getAbsolutePath() + ", iteration " + iteration);
        String id = file.getAbsolutePath();

        String uid = UUID.randomUUID().toString();
        Process process = null;
        int attempts = 0;

        try {
            Path workDir = Files.createDirectories(
                    this.workDir.toPath().resolve(Paths.get(file.getName(), "iteration-" + iteration)));
            Path tmp = Files.createDirectory(workDir.resolve("tmp"));

            ProcessBuilder pb = new ProcessBuilder("java", "-Duid=" + uid,
                    "-Djava.io.tmpdir=" + tmp.toAbsolutePath().toString(), "-jar", file.getAbsolutePath())
                            .redirectOutput(workDir.resolve("stdout.txt").toFile())
                            .redirectError(workDir.resolve("stderr.txt").toFile());

            final long s0 = System.currentTimeMillis();
            process = pb.start();

            final CloseableHttpClient httpClient = HttpClients.createDefault();

            while (true) {
                if (attempts >= NUM_CONNECTION_ATTEMPTS) {
                    System.out.println("Max attempts reached, escaping sequence");
                    break;
                }

                CloseableHttpResponse response = null;
                try {
                    HttpGet request = new HttpGet(httpCheck);
                    response = httpClient.execute(request);
                    int statusCode = response.getStatusLine().getStatusCode();

                    if (statusCode == 200) {
                        collector.onMeasurement(id, Measure.STARTUP_TIME,
                                (double) (System.currentTimeMillis() - s0));
                        warmup(httpClient, httpCheck);
                        measureMemory(id, uid, collector);
                        measureJarSize(id, file, collector);
                        measureTmpDirSize(id, tmp, collector);
                        break;
                    } else if (statusCode == 404) {
                        // this can happen during server boot, when the HTTP endpoint is already exposed
                        // but the application is not yet deployed
                    } else {
                        System.err.println("Failed to execute HTTP check: " + statusCode);
                        break;
                    }
                } catch (HttpHostConnectException e) {
                    // server not running yet
                } finally {
                    if (response != null) {
                        response.close();
                    }
                }

                attempts++;
                Thread.sleep(MS_BETWEEN_ATTEMPTS);
            }

            httpClient.close();

            final long s1 = System.currentTimeMillis();
            process.destroy();
            boolean finished = process.waitFor(2, TimeUnit.SECONDS);
            if (finished) {
                collector.onMeasurement(id, Measure.SHUTDOWN_TIME, (double) (System.currentTimeMillis() - s1));
            }
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (process != null && process.isAlive()) {
                process.destroyForcibly();
                try {
                    process.waitFor(2, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    private void warmup(CloseableHttpClient httpClient, String httpCheck) throws IOException {
        for (int i = 0; i < 100; i++) {
            HttpGet request = new HttpGet(httpCheck);
            try (CloseableHttpResponse response = httpClient.execute(request)) {
                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != 200) {
                    System.err.println("Failed to execute warmup: " + statusCode);
                    return;
                }
            }
        }
    }

    private void measureMemory(String id, String uid, Collector collector) throws Exception {
        // see https://support.hyperic.com/display/SIGAR/PTQL
        Sigar sigar = new Sigar();
        final ProcessFinder processFinder = new ProcessFinder(sigar);
        long pid = processFinder.findSingleProcess("State.Name.eq=java,Args.1.ct=" + uid);

        Jcmd.gc(pid);

        ProcMem procMem = sigar.getProcMem(pid);
        long rss = procMem.getResident();
        collector.onMeasurement(id, Measure.RSS_AFTER_INVOCATION, bytesToMegabytes(rss));

        long javaHeap = Jstat.usedHeap(pid);
        collector.onMeasurement(id, Measure.JAVA_HEAP_AFTER_INVOCATION, bytesToMegabytes(javaHeap));
    }

    private void measureJarSize(String id, File jar, Collector collector) throws IOException {
        long jarSize = jar.length();
        collector.onMeasurement(id, Measure.JAR_SIZE, bytesToMegabytes(jarSize));
    }

    private void measureTmpDirSize(String id, Path tmpDir, Collector collector) throws IOException {
        try (Stream<Path> stream = Files.walk(tmpDir)) {
            long tmpDirSize = stream.mapToLong(path -> path.toFile().length()).sum();

            collector.onMeasurement(id, Measure.TMP_DIR_SIZE, bytesToMegabytes(tmpDirSize));
        }
    }

    private static final int NUM_CONNECTION_ATTEMPTS = 1000;

    private static final int MS_BETWEEN_ATTEMPTS = 20;

    private int NUM_ITERATIONS = 10;

    private final File baseDir;

    private final File workDir;

    private final Optional<File> archiveDir;

    private final Optional<File> outputFile;

    private final Optional<File> comparisonOutputFile;

    private final Collector collector;

    private boolean skipTests;

    static class ArchivedResult {
        private Version version;
        private File file;

        public ArchivedResult(Version version, File file) {
            this.version = version;
            this.file = file;
        }

        public Version getVersion() {
            return version;
        }

        public File getFile() {
            return file;
        }
    }
}