HeapSpace.java

/*
 * Copyright (C) 2019 uwe
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sw4j.sample.memory.heap;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
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;

/**
 *
 * @author Uwe Plonus &lt;u.plonus@gmail.com&gt;
 */
public class HeapSpace implements HeapSpaceMBean {

    private static final Logger logger = Logger.getLogger(HeapSpace.class.getName());

    private static final int DEFAULT_BLOCK_SIZE = 4096;

    private static final int DEFAULT_CREATE_INTERVAL = 200;

    private static final int DEFAULT_SHORT_LIFETIME = 1000;

    private static final int DEFAULT_MEDIUM_LIFETIME = 150000;

    private static final int DEFAULT_LONG_LIFETIME = 600000;

    private static final double DEFAULT_MEDIUM_PROBABILITY = 15.0;

    private static final double DEFAULT_LONG_PROBABILITY = 5.0;

    private static final double MAX_PROBABILITY = 100.0;

    private boolean stopped = false;

    private int blockSize;

    private int createInterval;

    private int shortLifetime;

    private int mediumLifetime;

    private int longLifetime;

    private double mediumProbability;

    private double longProbability;

    public static void main(String... args) throws Exception {
        new HeapSpace().run(args);
    }

    public void run(String... args) throws InterruptedException, ExecutionException {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        try {
            ObjectName name = new ObjectName(HeapSpace.class.getPackage().getName(), "type",
                    HeapSpace.class.getSimpleName());
            mbs.registerMBean(this, name);
        } catch (InstanceAlreadyExistsException | MBeanRegistrationException |
                MalformedObjectNameException | NotCompliantMBeanException exc) {
            logger.log(Level.WARNING, "Problems creating MBean. Running without MBean creation.", exc);
        }
        PossibleConfiguration config = parseCommandLine(args);
        if (config.hasValues()) {
            run(config);
        } else {
            System.out.println(config.getMessage());
        }
    }

    /**
     *
     * @param config must have values.
     * @throws InterruptedException
     * @throws ExecutionException 
     */
    public void run(PossibleConfiguration config) throws InterruptedException, ExecutionException {
        if (!config.hasValues()) {
            throw new IllegalArgumentException("Need a configuration with values");
        }
        logger.info("Started HeapSpace");

        blockSize = config.getBlockSize();
        createInterval = config.getCreate();
        shortLifetime = config.getShortLifetime();
        mediumLifetime = config.getMediumLifetime();
        longLifetime = config.getLongLifetime();
        mediumProbability = config.getMediumProbability();
        longProbability = config.getLongProbability();

        Random rand = new Random();
        final ScheduledExecutorService creationScheduler = Executors.newSingleThreadScheduledExecutor();
        final ExecutorService hashExecutor = Executors.newFixedThreadPool(4);
        final ScheduledExecutorService cleanUpScheduler = Executors.newSingleThreadScheduledExecutor();
        Thread.setDefaultUncaughtExceptionHandler((Thread arg0, Throwable arg1) -> {
            creationScheduler.shutdown();
            hashExecutor.shutdown();
            cleanUpScheduler.shutdown();
            logger.warning("Exiting after unhandled exception in thread");
            arg1.printStackTrace(System.err);
        });
        while (!stopped) {
            ScheduledFuture<Allocator> futureAllocator =
                    creationScheduler.schedule(() -> new Allocator(1 + rand.nextInt(blockSize)),
                            rand.nextInt(getCreateInterval()), TimeUnit.MILLISECONDS);
            final Allocator allocator = futureAllocator.get();
            hashExecutor.submit(() -> {
                allocator.calculateHash();
                logger.info(String.format("Created allocator with hash  %s", allocator.getHash()));
                int cleanUpTime = rand.nextInt(shortLifetime);
                String cleanUpMessage = "Released short lived hash  %s";
                double prob = MAX_PROBABILITY * rand.nextDouble();
                if (prob >= MAX_PROBABILITY - longProbability) {
                    cleanUpTime = mediumLifetime + rand.nextInt(longLifetime - mediumLifetime);
                    cleanUpMessage = "Released long lived hash   %s";
                } else if (prob >= MAX_PROBABILITY - (longProbability + mediumProbability)) {
                    cleanUpTime = shortLifetime + rand.nextInt(mediumLifetime - shortLifetime);
                    cleanUpMessage = "Released medium lived hash %s";
                }
                final String format = cleanUpMessage;
                cleanUpScheduler.schedule(() -> {
                    logger.info(String.format(format, allocator.getHash()));
                }, cleanUpTime, TimeUnit.MILLISECONDS);
                return null;
            });
        }
    }

    /**
     * Parses the command line arguments into a {@code Configuration} object.
     *
     * @param args the command line arguments
     * @return the parsed {@code Configuration} or {@code null} if help was printed.
     */
    public PossibleConfiguration parseCommandLine(String... args) {
        Options options = new Options();

        options.addOption(Option
                .builder("h")
                .longOpt("help")
                .optionalArg(true)
                .desc("Print this help")
                .build());

        options.addOption(Option
                .builder("b")
                .longOpt("block-size")
                .optionalArg(true)
                .hasArg(true)
                .argName("size")
                .desc(String.format("The max block size of memory to reserve (default: %d)", DEFAULT_BLOCK_SIZE))
                .build());

        options.addOption(Option
                .builder("c")
                .longOpt("create")
                .optionalArg(true)
                .hasArg(true)
                .argName("interval")
                .desc(String.format("The max interval (in ms) between object creation (default: %d)",
                        DEFAULT_CREATE_INTERVAL))
                .build());

        options.addOption(Option
                .builder("s")
                .longOpt("short-time")
                .optionalArg(true)
                .hasArg(true)
                .argName("duration")
                .desc(String.format("The max lifetime (in ms) of a short lived object (default: %d)",
                        DEFAULT_SHORT_LIFETIME))
                .build());

        options.addOption(Option
                .builder("m")
                .longOpt("medium-time")
                .optionalArg(true)
                .hasArg(true)
                .argName("duration")
                .desc(String.format("The max lifetime (in ms) of a medium lived object (default: %d)",
                        DEFAULT_MEDIUM_LIFETIME))
                .build());

        options.addOption(Option
                .builder("l")
                .longOpt("long-time")
                .optionalArg(true)
                .hasArg(true)
                .argName("duration")
                .desc(String.format("The max lifetime (in ms) of a long lived object (default: %d)",
                        DEFAULT_LONG_LIFETIME))
                .build());

        options.addOption(Option
                .builder()
                .longOpt("med-prob")
                .optionalArg(true)
                .hasArg(true)
                .argName("probability")
                .desc(String.format("The probability (in %%) to create a medium lived object (default: %3.1f)",
                        DEFAULT_MEDIUM_PROBABILITY))
                .build());

        options.addOption(Option
                .builder()
                .longOpt("long-prob")
                .optionalArg(true)
                .hasArg(true)
                .argName("probability")
                .desc(String.format("The probability (in %%) to create a long lived object (default: %3.1f)",
                        DEFAULT_LONG_PROBABILITY))
                .build());

        CommandLineParser clParser = new DefaultParser();
        CommandLine cl = new CommandLine.Builder().build();
        try {
            cl = clParser.parse(options, args, true);
        } catch (ParseException pex) {
            logger.log(Level.WARNING, "Problems while parsing the command line", pex);
        }

        int block = DEFAULT_BLOCK_SIZE;
        if (cl.hasOption("b")) {
            try {
                block = Integer.parseInt(cl.getOptionValue("b"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse block size. Using default.");
            }
        }

        int create = DEFAULT_CREATE_INTERVAL;
        if (cl.hasOption("c")) {
            try {
                create = Integer.parseInt(cl.getOptionValue("c"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse creation interval. Using default.");
            }
        }

        int shortLife = DEFAULT_SHORT_LIFETIME;
        if (cl.hasOption("s")) {
            try {
                shortLife = Integer.parseInt(cl.getOptionValue("s"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse short lifetime. Using default.");
            }
        }

        int medLife = DEFAULT_MEDIUM_LIFETIME;
        if (cl.hasOption("m")) {
            try {
                medLife = Integer.parseInt(cl.getOptionValue("m"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse medium lifetime. Using default.");
            }
        }

        int longLife = DEFAULT_LONG_LIFETIME;
        if (cl.hasOption("l")) {
            try {
                longLife = Integer.parseInt(cl.getOptionValue("l"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse long lifetime. Using default.");
            }
        }

        double medProb = DEFAULT_MEDIUM_PROBABILITY;
        if (cl.hasOption("med-prob")) {
            try {
                medProb = Double.parseDouble(cl.getOptionValue("med-prob"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse medium lived probability. Using default");
            }
        }

        double longProb = DEFAULT_LONG_PROBABILITY;
        if (cl.hasOption("long-prob")) {
            try {
                longProb = Double.parseDouble(cl.getOptionValue("long-prob"));
            } catch (NumberFormatException nfex) {
                logger.fine("Cannot parse long lived probability. Using default");
            }
        }

        PossibleConfiguration config;
        if (cl.hasOption("help")) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            HelpFormatter helper = new HelpFormatter();
            helper.printHelp(pw, 80, "HeapSpace",
                    "A small application that will (most likely) create an OutOfMemoryError " +
                            "with message \"Java heap space\"",
                    options, 1, 2, "", true);
            config = new PossibleConfiguration(sw.toString());
        } else {
            StringBuilder sb = new StringBuilder();
            if (block < 1) {
                appendNewLineIfNeeded(sb).append("The block size (-b) must be greater than 0.");
            }
            if (create < 1) {
                appendNewLineIfNeeded(sb).append("The creation interval (-c) must be greater than 0.");
            }
            if (shortLife < 1) {
                appendNewLineIfNeeded(sb).append("The short life time (-s) must be greater than 0.");
            }
            if (medLife < 1) {
                appendNewLineIfNeeded(sb).append("The medium life time (-m) must be greater than 0.");
            }
            if (longLife < 1) {
                appendNewLineIfNeeded(sb).append("The long life time (-l) must be greater than 0.");
            }
            if (medProb < 0.0) {
                appendNewLineIfNeeded(sb).append("The medium probability (--med-prob) must be greater than 0.0.");
            }
            if (longProb < 0.0) {
                appendNewLineIfNeeded(sb).append("The long probability (--long-prob) must be greater than 0.0.");
            }
            if (shortLife > medLife) {
                appendNewLineIfNeeded(sb).append("The short life time must be lower than the medium life time.");
            }
            if (medLife > longLife) {
                appendNewLineIfNeeded(sb).append("The medium life time must be lower than the long life time.");
            }
            if (medProb + longProb > MAX_PROBABILITY) {
                appendNewLineIfNeeded(sb).append(String.format("The sum of medium probability and long probability " +
                        "may not be larger than %4.1f.", MAX_PROBABILITY));
            }
            if (sb.length() > 0) {
                config = new PossibleConfiguration(sb.toString());
            } else {
                config = new PossibleConfiguration(block, create, shortLife, medLife, longLife, medProb, longProb);
            }
        }

        return config;
    }

    private StringBuilder appendNewLineIfNeeded(StringBuilder sb) {
        if (sb.length() > 0) {
            sb.append("\n");
        }
        return sb;
    }

    /**
     * Stops the creation of new objects.
     */
    @Override
    public void stop() {
        stopped = true;
    }

    @Override
    public int getBlockSize() {
        return blockSize;
    }

    @Override
    public void setBlockSize(int blockSize) {
        this.blockSize = blockSize;
    }

    @Override
    public void setCreateInterval(int millis) {
        createInterval = millis;
    }

    @Override
    public int getCreateInterval() {
        return createInterval;
    }

    @Override
    public int getShortLifetime() {
        return shortLifetime;
    }

    @Override
    public void setShortLifetime(int shortLifetime) {
        this.shortLifetime = shortLifetime;
    }

    @Override
    public int getMediumLifetime() {
        return mediumLifetime;
    }

    @Override
    public void setMediumLifetime(int mediumLifetime) {
        this.mediumLifetime = mediumLifetime;
    }

    @Override
    public int getLongLifetime() {
        return longLifetime;
    }

    @Override
    public void setLongLifetime(int longLifetime) {
        this.longLifetime = longLifetime;
    }

    @Override
    public void setMediumProbability(double prob) {
        mediumProbability = prob;
    }

    @Override
    public double getMediumProbability() {
        return mediumProbability;
    }

    @Override
    public void setLongProbability(double prob) {
        longProbability = prob;
    }

    @Override
    public double getLongProbability() {
        return longProbability;
    }

}