//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  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 org.eclipse.jetty.monitor;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jetty.monitor.thread.ThreadMonitorException;
import org.eclipse.jetty.monitor.thread.ThreadMonitorInfo;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

@ManagedObject("Busy Thread Monitor")
public class ThreadMonitor extends AbstractLifeCycle implements Runnable
{
    private static final Logger LOG = Log.getLogger(ThreadMonitor.class);

    private int _scanInterval;
    private int _logInterval;
    private int _busyThreshold;
    private int _logThreshold;
    private int _stackDepth;
    private int _trailLength;
    
    private ThreadMXBean _threadBean;
    
    private Thread _runner;
    private Logger _logger;
    private volatile boolean _done = true;
    private Dumpable _dumpable;

    private Map<Long,ThreadMonitorInfo> _monitorInfo;
    
    /* ------------------------------------------------------------ */
    /**
     * Instantiates a new thread monitor.
     *
     * @throws Exception
     */
    public ThreadMonitor() throws Exception
    {
        this(5000);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Instantiates a new thread monitor.
     *
     * @param intervalMs scan interval
     * @throws Exception
     */
    public ThreadMonitor(int intervalMs) throws Exception
    {
        this(intervalMs, 95);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Instantiates a new thread monitor.
     *
     * @param intervalMs scan interval
     * @param threshold busy threshold
     * @throws Exception
     */
    public ThreadMonitor(int intervalMs, int threshold) throws Exception
    {
        this(intervalMs, threshold, 3);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Instantiates a new thread monitor.
     *
     * @param intervalMs scan interval
     * @param threshold busy threshold
     * @param depth stack compare depth
     * @throws Exception
     */
    public ThreadMonitor(int intervalMs, int threshold, int depth) throws Exception
    {
        this(intervalMs, threshold, depth, 3);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Instantiates a new thread monitor.
     *
     * @param intervalMs scan interval
     * @param threshold busy threshold
     * @param depth stack compare depth
     * @param trail length of stack trail
     * @throws Exception
     */
    public ThreadMonitor(int intervalMs, int threshold, int depth, int trail) throws Exception
    {
        _scanInterval = intervalMs;
        _busyThreshold = threshold;
        _stackDepth = depth;
        _trailLength = trail;
        
        _logger = Log.getLogger(ThreadMonitor.class.getName());
        _monitorInfo = new HashMap<Long, ThreadMonitorInfo>();
       
        init();
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Gets the scan interval.
     *
     * @return the scan interval
     */
    public int getScanInterval()
    {
        return _scanInterval;
    }

    /* ------------------------------------------------------------ */
    /**
     * Sets the scan interval.
     *
     * @param ms the new scan interval
     */
    public void setScanInterval(int ms)
    {
        _scanInterval = ms;
    }

    /* ------------------------------------------------------------ */
    /**
     * Gets the log interval.
     *
     * @return the log interval
     */
    public int getLogInterval()
    {
        return _logInterval;
    }

    /* ------------------------------------------------------------ */
    /**
     * Sets the log interval.
     *
     * @param ms the new log interval
     */
    public void setLogInterval(int ms)
    {
        _logInterval = ms;
    }

    /* ------------------------------------------------------------ */
    /**
     * Gets the busy threshold.
     *
     * @return the busy threshold
     */
    public int getBusyThreshold()
    {
        return _busyThreshold;
    }

    /* ------------------------------------------------------------ */
    /**
     * Sets the busy threshold.
     *
     * @param percent the new busy threshold
     */
    public void setBusyThreshold(int percent)
    {
        _busyThreshold = percent;
    }

    /* ------------------------------------------------------------ */
    /**
     * Gets the log threshold.
     *
     * @return the log threshold
     */
    public int getLogThreshold()
    {
        return _logThreshold;
    }

    /* ------------------------------------------------------------ */
    /**
     * Sets the log threshold.
     *
     * @param percent the new log threshold
     */
    public void setLogThreshold(int percent)
    {
        _logThreshold = percent;
    }

    /* ------------------------------------------------------------ */
    /**
     * Gets the stack depth.
     *
     * @return the stack depth
     */
    public int getStackDepth()
    {
        return _stackDepth;
    }

    /* ------------------------------------------------------------ */
    /**
     * Sets the stack depth.
     *
     * @param stackDepth the new stack depth
     */
    public void setStackDepth(int stackDepth)
    {
        _stackDepth = stackDepth;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Sets the stack trace trail length.
     *
     * @param trailLength the new trail length
     */
    public void setTrailLength(int trailLength)
    {
        _trailLength = trailLength;
    }

    /* ------------------------------------------------------------ */
    /**
     * Gets the stack trace trail length.
     *
     * @return the trail length
     */
    public int getTrailLength()
    {
        return _trailLength;
    }

    /* ------------------------------------------------------------ */
    /**
     * Enable logging of CPU usage.
     *
     * @param frequencyMs the logging frequency 
     * @param thresholdPercent the logging threshold
     */
    public void logCpuUsage(int frequencyMs, int thresholdPercent)
    {
        setLogInterval(frequencyMs);
        setLogThreshold(thresholdPercent);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @return A {@link Dumpable} that is dumped whenever spinning threads are detected
     */
    public Dumpable getDumpable()
    {
        return _dumpable;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param dumpable A {@link Dumpable} that is dumped whenever spinning threads are detected
     */
    public void setDumpable(Dumpable dumpable)
    {
        _dumpable = dumpable;
    }

    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
     */
    public void doStart()
    {
        _done = false;
        
        _runner = new Thread(this);
        _runner.setDaemon(true);
        _runner.start();

        LOG.info("Thread Monitor started successfully");
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
     */
    public void doStop()
    {
        if (_runner != null)
        {
            _done = true;
            try
            {
                _runner.join();
            }
            catch (InterruptedException ex) {}
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * Retrieve all avaliable thread ids
     *
     * @return array of thread ids
     */
    protected long[] getAllThreadIds()
    {
        return _threadBean.getAllThreadIds();
    }

    /* ------------------------------------------------------------ */
    /**
     * Retrieve the cpu time for specified thread.
     *
     * @param id thread id
     * @return cpu time of the thread
     */
    protected long getThreadCpuTime(long id)
    {
        return _threadBean.getThreadCpuTime(id);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Initialize JMX objects.
     */
    protected void init()
    {
        _threadBean = ManagementFactory.getThreadMXBean();
        if (_threadBean.isThreadCpuTimeSupported())
        {
            _threadBean.setThreadCpuTimeEnabled(true);
        }
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @see java.lang.Runnable#run()
     */
    public void run()
    {
        // Initialize repeat flag
        boolean repeat = false;
        boolean scanNow, logNow;

        // Set next scan time and log time
        long nextScanTime = System.currentTimeMillis();
        long nextLogTime = nextScanTime + _logInterval;
        
        while (!_done)
        {
            long currTime = System.currentTimeMillis();
            scanNow = (currTime > nextScanTime);
            logNow = (_logInterval > 0 && currTime > nextLogTime);
            if (repeat || scanNow || logNow)
            {
                repeat = collectThreadInfo();
                logThreadInfo(logNow);

                if (scanNow)
                {
                    nextScanTime = System.currentTimeMillis() + _scanInterval;
                }
                if (logNow)
                {
                    nextLogTime = System.currentTimeMillis() + _logInterval;
                }
            }

            // Sleep only if not going to repeat scanning immediately
            if (!repeat)
            {
                try
                {
                    Thread.sleep(100);
                }
                catch (InterruptedException ex)
                {
                    LOG.ignore(ex);
                }
            }
        }
        
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Collect thread info.
     */
    private boolean collectThreadInfo()
    {
        boolean repeat = false;
        try
        {
            // Retrieve stack traces for all threads at once as it 
            // was proven to be an order of magnitude faster when
            // retrieving a single thread stack trace.
            Map<Thread,StackTraceElement[]> all = Thread.getAllStackTraces();
            
            for (Map.Entry<Thread,StackTraceElement[]> entry : all.entrySet())
            {
                Thread thread = entry.getKey();
                long threadId = thread.getId();
                
                // Skip our own runner thread
                if (threadId == _runner.getId())
                {
                    continue;
                }

                ThreadMonitorInfo currMonitorInfo = _monitorInfo.get(Long.valueOf(threadId));
                if (currMonitorInfo == null)
                {
                    // Create thread info object for a new thread 
                    currMonitorInfo = new ThreadMonitorInfo(thread);
                    currMonitorInfo.setStackTrace(entry.getValue());
                    currMonitorInfo.setCpuTime(getThreadCpuTime(threadId));
                    currMonitorInfo.setSampleTime(System.nanoTime());
                    _monitorInfo.put(Long.valueOf(threadId), currMonitorInfo);
                }
                else
                {
                    // Update the existing thread info object
                    currMonitorInfo.setStackTrace(entry.getValue());
                    currMonitorInfo.setCpuTime(getThreadCpuTime(threadId));
                    currMonitorInfo.setSampleTime(System.nanoTime());
    
                    // Stack trace count holds logging state
                    int count = currMonitorInfo.getTraceCount();
                    if (count >= 0 && currMonitorInfo.isSpinning())
                    {
                        // Thread was spinning and was logged before
                        if (count < _trailLength) 
                        {
                            // Log another stack trace
                            currMonitorInfo.setTraceCount(count+1);
                            repeat = true;
                            continue;
                        }
                        
                        // Reset spin flag and trace count
                        currMonitorInfo.setSpinning(false);
                        currMonitorInfo.setTraceCount(-1);
                    }
                    if (currMonitorInfo.getCpuUtilization() > _busyThreshold)
                    {
                        // Thread is busy
                        StackTraceElement[] lastStackTrace = currMonitorInfo.getStackTrace();
    
                        if (lastStackTrace != null 
                        && matchStackTraces(lastStackTrace, entry.getValue()))
                        {
                            // Thread is spinning
                            currMonitorInfo.setSpinning(true);
                            if (count < 0)
                            {
                                // Enable logging of spin status and stack traces 
                                // only if the incoming trace count is negative
                                // that indicates a new scan for this thread
                                currMonitorInfo.setTraceCount(0);
                                repeat = (_trailLength > 0);
                            }
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            LOG.debug(ex);
        }
        return repeat;
    }

    /* ------------------------------------------------------------ */
    protected void logThreadInfo(boolean logAll)
    {
        if (_monitorInfo.size() > 0)
        {
            // Select thread objects for all live threads
            long[] running = getAllThreadIds();
            List<ThreadMonitorInfo> all = new ArrayList<ThreadMonitorInfo>();
            for (int idx=0; idx<running.length; idx++)
            {
                ThreadMonitorInfo info = _monitorInfo.get(running[idx]); 
                if (info != null)
                {
                    all.add(info);
                }
            }

            // Sort selected thread objects by their CPU utilization
            Collections.sort(all, new Comparator<ThreadMonitorInfo>()
            {
                /* ------------------------------------------------------------ */
                public int compare(ThreadMonitorInfo info1, ThreadMonitorInfo info2)
                {
                    return (int)Math.signum(info2.getCpuUtilization()-info1.getCpuUtilization());
                }
            });
            
            String format = "Thread '%2$s'[%3$s,id:%1$d,cpu:%4$.2f%%]%5$s";
            
            // Log thread information for threads that exceed logging threshold
            // or log spinning threads if their trace count is zero
            boolean spinning=false;
            for (ThreadMonitorInfo info : all)
            {
                if (logAll && info.getCpuUtilization() > _logThreshold 
                || info.isSpinning() && info.getTraceCount() == 0)
                {
                    String message = String.format(format, 
                            info.getThreadId(), info.getThreadName(), 
                            info.getThreadState(), info.getCpuUtilization(),
                            info.isSpinning() ? " SPINNING" : "");
                   _logger.info(message);
                   spinning=true;
                }
            }
            
            // Dump info
            if (spinning && _dumpable!=null)
            {
                System.err.println(_dumpable.dump());
            }

            // Log stack traces for spinning threads with positive trace count
            for (ThreadMonitorInfo info : all)
            {
                if (info.isSpinning() && info.getTraceCount() >= 0)
                {
                    String message = String.format(format,
                            info.getThreadId(), info.getThreadName(), 
                            info.getThreadState(), info.getCpuUtilization(),
                            " STACK TRACE");
                    _logger.warn(new ThreadMonitorException(message, info.getStackTrace()));
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * Match stack traces.
     *
     * @param lastStackTrace last stack trace
     * @param stackTrace current stack trace
     * @return true, if successful
     */
    private boolean matchStackTraces(StackTraceElement[] lastStackTrace, StackTraceElement[] stackTrace)
    {
        boolean match = true;
        int count = Math.min(_stackDepth, Math.min(lastStackTrace.length, stackTrace.length));
        
        for (int idx=0; idx < count; idx++)
        {
            if (!stackTrace[idx].equals(lastStackTrace[idx]))
            {
                match = false;
                break;
            }
        }
        return match;
    }
}
