blob: 7130f4f06807b0ca857c4a27f7fc320ac3930732 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 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;
}
}