Sampler.java

/*
 * Copyright (c) 2001, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package org.spf4j.stackmonitor;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import javax.annotation.PreDestroy;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.spf4j.base.AbstractRunnable;
import org.spf4j.stackmonitor.proto.Converter;

/**
 * Utility that allow you to sample what the application is doing. It generates
 * a "Flame Graph" that allows you to quickly see you "heavy" operations.
 *
 * You can use JConsole to control the sampling while your application is
 * running.
 *
 * By using a sampling approach you can choose your overhead. (sampling takes
 * about 0.5 ms, so the default of 10Hz will give you 0.5% overhead)
 *
 * Collection is separated into CPU, WAIT and IO categories. I felt that most
 * important is to see what hogs your CPU because that is where, you can most
 * likely can do something about it.
 *
 * @author zoly
 */
@ThreadSafe
public final class Sampler implements SamplerMBean {

    private volatile boolean stopped;
    private volatile long sampleTimeMillis;
    private volatile long dumpTimeMillis;
    private volatile boolean isJmxRegistered;
    private final StackCollector stackCollector;
    private final ObjectName name;
    private long lastDumpTime = System.currentTimeMillis();
    
    @GuardedBy("this")
    private Thread samplingThread;
    private final String filePrefix = System.getProperty("perf.db.folder",
            System.getProperty("java.io.tmpdir")) + File.separator
            + System.getProperty("perf.db.name",
            ManagementFactory.getRuntimeMXBean().getName());

    public Sampler() {
        this(100, 3600000, new MxStackCollector());
    }

    public Sampler(final long sampleTimeMillis) {
        this(sampleTimeMillis, 3600000, new MxStackCollector());
    }

    public Sampler(final StackCollector collector) {
        this(100, 3600000, collector);
    }

    public Sampler(final long sampleTimeMillis, final long dumpTimeMillis, final StackCollector collector) {
        stopped = true;
        this.sampleTimeMillis = sampleTimeMillis;
        this.dumpTimeMillis = dumpTimeMillis;
        this.stackCollector = collector;
        try {
            this.name = new ObjectName("SPF4J:name=StackSampler");
        } catch (MalformedObjectNameException ex) {
            throw new RuntimeException(ex);
        }
        this.isJmxRegistered = false;
    }

    public void registerJmx()
            throws MalformedObjectNameException, InstanceAlreadyExistsException,
            MBeanRegistrationException, NotCompliantMBeanException {
        ManagementFactory.getPlatformMBeanServer().registerMBean(this, name);
        isJmxRegistered = true;
    }

    @Override
    public synchronized void start() {
        if (stopped) {
            stopped = false;
            final long stMillis = sampleTimeMillis;

            final long dumpCount = dumpTimeMillis / sampleTimeMillis;

            samplingThread = new Thread(new AbstractRunnable() {
                private long dumpCounter = 0;

                @SuppressWarnings("SleepWhileInLoop")
                @Override
                public void doRun() throws IOException, InterruptedException {
                    while (!stopped) {
                        stackCollector.sample();
                        Thread.sleep(stMillis);
                        dumpCounter++;
                        if (dumpCounter >= dumpCount) {
                            dumpCounter = 0;
                            dumpToFile();
                            lastDumpTime = System.currentTimeMillis();
                            clear();
                        }



                    }
                }
            }, "Stack Sampling Thread");
            samplingThread.start();
        } else {
            throw new IllegalStateException("Sampling can only be started once");
        }

    }
    private static final DateTimeFormatter TS_FORMAT = ISODateTimeFormat.basicDateTimeNoMillis();

    public synchronized void dumpToFile() throws IOException {
        final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePrefix + "_"
                + TS_FORMAT.print(lastDumpTime) + "_" + TS_FORMAT.print(System.currentTimeMillis()) + ".ssdump"));
        try {
            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    try {
                        Converter.fromSampleNodeToProto(input).writeTo(bos);
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                    return input;
                }
            });


        } finally {
            bos.close();
        }
        lastDumpTime = System.currentTimeMillis();

    }

    @Override
    public synchronized void generateHtmlMonitorReport(final String fileName, final int chartWidth, final int maxDepth)
            throws IOException {
        dumpToFile();
        final Writer writer
                = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), Charsets.UTF_8));
        try {
            writer.append("<html>");

            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {
                        SampleNode finput = input;
                        try {
                            writer.append("<h1>Total stats</h1>");
                            StackVisualizer.generateHtmlTable(writer, Method.ROOT, finput, chartWidth, maxDepth);
                        } catch (IOException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                    return input;
                }
            });


            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {
                        SampleNode finput = input.filteredBy(WaitMethodClassifier.INSTANCE);
                        try {
                            writer.append("<h1>CPU stats</h1>");
                            StackVisualizer.generateHtmlTable(writer, Method.ROOT, finput, chartWidth, maxDepth);
                        } catch (IOException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                    return input;
                }
            });

            writer.append("</html>");

        } finally {
            writer.close();
        }

    }

    @Override
    public synchronized void stop() throws InterruptedException {
        stopped = true;
        samplingThread.join();
    }

    @Override
    public long getSampleTimeMillis() {
        return sampleTimeMillis;
    }

    @Override
    public void setSampleTimeMillis(final long sampleTimeMillis) {
        this.sampleTimeMillis = sampleTimeMillis;
    }

    @Override
    public boolean isStopped() {
        return stopped;
    }

    @Override
    public List<String> generate(final Properties props) throws IOException {
        int width = Integer.valueOf(props.getProperty("width", "1200"));
        int maxDepth = Integer.valueOf(props.getProperty("maxDepth", "1200"));
        String fileName = File.createTempFile("stack", ".html").getAbsolutePath();
        generateHtmlMonitorReport(fileName, width, maxDepth);
        return Arrays.asList(fileName);
    }

    @Override
    public List<String> getParameters() {
        return Arrays.asList("width");
    }

    @Override
    public void clear() {
        stackCollector.clear();
    }

    public StackCollector getStackCollector() {
        return stackCollector;
    }

    @PreDestroy
    public void dispose() throws InterruptedException, InstanceNotFoundException {
        stop();
        try {
            if (isJmxRegistered) {
                ManagementFactory.getPlatformMBeanServer().unregisterMBean(name);
            }
        } catch (InstanceNotFoundException ex) {
            throw new RuntimeException(ex);
        } catch (MBeanRegistrationException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public void generateCpuSvg(final String fileName, final int chartWidth, final int maxDepth) throws IOException {
        final Writer writer
                = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), Charsets.UTF_8));
        try {

            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {
                        SampleNode finput = input.filteredBy(WaitMethodClassifier.INSTANCE);
                        try {
                            StackVisualizer.generateSvg(writer, Method.ROOT, finput, 0, 0, chartWidth, maxDepth, "a");
                        } catch (IOException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                    return input;
                }
            });
        } finally {
            writer.close();
        }
    }

    @Override
    public void generateTotalSvg(final String fileName, final int chartWidth, final int maxDepth) throws IOException {
        final Writer writer
                = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), Charsets.UTF_8));
        try {

            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {

                        try {
                            StackVisualizer.generateSvg(writer, Method.ROOT, input, 0, 0, chartWidth, maxDepth, "b");
                        } catch (IOException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                    return input;
                }
            });
        } finally {
            writer.close();
        }
    }

    @Override
    public void generateSvgHtmlMonitorReport(final String fileName, final int chartWidth, final int maxDepth)
            throws IOException {
        dumpToFile();
        final Writer writer
                = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), Charsets.UTF_8));
        try {
            writer.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
                    + "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\""
                    + " \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n");
            writer.append("<html>");


            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {

                        try {
                            writer.append("<h1>Total stats</h1>");
                            StackVisualizer.generateSvg(writer, Method.ROOT, input, 0, 0, chartWidth, maxDepth, "a");
                        } catch (IOException ex) {
                            throw new RuntimeException(ex);
                        }

                    }
                    return input;
                }
            });


            stackCollector.applyOnSamples(new Function<SampleNode, SampleNode>() {
                @Override
                public SampleNode apply(final SampleNode input) {
                    if (input != null) {
                        SampleNode finput = input.filteredBy(WaitMethodClassifier.INSTANCE);
                        if (finput != null) {
                            try {
                                writer.append("<h1>CPU stats</h1>");
                                StackVisualizer.generateSvg(writer, Method.ROOT,
                                        finput, 0, 0, chartWidth, maxDepth, "b");
                            } catch (IOException ex) {
                                throw new RuntimeException(ex);
                            }
                        }
                    }
                    return input;
                }
            });


            writer.append("</html>");

        } finally {
            writer.close();
        }

    }

    @Override
    public long getDumpTimeMillis() {
        return dumpTimeMillis;
    }

    @Override
    public void setDumpTimeMillis(final long dumpTimeMillis) {
        this.dumpTimeMillis = dumpTimeMillis;
    }
}