/*
* The MIT License
*
* Copyright (c) 2014 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.olivergondza.dumpling.factory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import com.github.olivergondza.dumpling.model.ProcessRuntime;
import com.github.olivergondza.dumpling.model.StackTrace;
import com.github.olivergondza.dumpling.model.ThreadLock;
import com.github.olivergondza.dumpling.model.ThreadLock.Monitor;
import com.github.olivergondza.dumpling.model.ThreadStatus;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpRuntime;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpThread;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpThread.Builder;
/**
* Instantiate {@link ProcessRuntime} from threaddump produced by <tt>jstack</tt> or similar tool.
*
* @author ogondza
*/
public class ThreadDumpFactory {
private static final Logger LOG = Logger.getLogger(ThreadDumpFactory.class.getName());
private static final StackTraceElement WAIT_TRACE_ELEMENT = StackTrace.nativeElement("java.lang.Object", "wait");
private static final String NL = "(?:\\r\\n|\\n)";
private static final String LOCK_SUBPATTERN = "<(?:0x)?(\\w+)> \\(a ([^\\)]+)\\)";
private static final Pattern THREAD_DELIMITER = Pattern.compile(NL + "(?:" + NL + "(?!\\s)|(?=\"))");
// TODO the regex is ignoring module name and version at the time: java.lang.Thread.sleep(java.base@9-ea/Native Method)
private static final Pattern STACK_TRACE_ELEMENT_LINE = Pattern.compile(" *at (\\S+)\\.(\\S+)\\((?:.+/)?([^:]+?)(\\:\\d+)?\\)");
private static final Pattern ACQUIRED_LINE = Pattern.compile("- locked " + LOCK_SUBPATTERN);
// Oracle/OpenJdk puts unnecessary space after 'parking to wait for'
private static final Pattern WAITING_ON_LINE = Pattern.compile("- (?:waiting on|parking to wait for ?) " + LOCK_SUBPATTERN);
private static final Pattern WAITING_TO_LOCK_LINE = Pattern.compile("- waiting to lock " + LOCK_SUBPATTERN);
private static final Pattern OWNABLE_SYNCHRONIZER_LINE = Pattern.compile("- " + LOCK_SUBPATTERN);
private static final Pattern THREAD_HEADER = Pattern.compile(
"^\"(.*)\" ([^\\n\\r]+)(?:" + NL + "\\s+java.lang.Thread.State: ([^\\n\\r]+)(?:" + NL + "(.+))?)?",
Pattern.DOTALL
);
private boolean failOnErrors = false;
/**
* Historically, dumpling tolerates some of the errors silently.
*
* Turning this on will replace log records for failures to parse the threaddump.
*/
public ThreadDumpFactory failOnErrors(boolean failOnErrors) {
this.failOnErrors = failOnErrors;
return this;
}
/**
* Create runtime from thread dump.
*
* @throws IOException File could not be loaded.
*/
public @Nonnull ThreadDumpRuntime fromFile(@Nonnull File threadDump) throws IOException {
FileInputStream fis = new FileInputStream(threadDump);
try {
return fromStream(fis);
} finally {
fis.close();
}
}
public @Nonnull ThreadDumpRuntime fromStream(@Nonnull InputStream stream) {
Set<ThreadDumpThread.Builder> threads = new LinkedHashSet<ThreadDumpThread.Builder>();
List<String> header = new ArrayList<String>();
Scanner scanner = new Scanner(stream);
scanner.useDelimiter(THREAD_DELIMITER);
try {
while (scanner.hasNext()) {
String singleChunk = scanner.next();
if (singleChunk.startsWith("JNI global references")) {
// Nothing interesting is expected after this point. Also, this is a convenient way to eliminate the
// deadlock report that is spread over several chunks
break;
}
ThreadDumpThread.Builder thread = thread(singleChunk);
if (thread != null) {
threads.add(thread);
continue;
}
if (header.isEmpty()) { // Still reading header
header.addAll(Arrays.asList(singleChunk.split(NL)));
continue;
}
String msg = "Skipping unrecognized chunk: " + singleChunk;
if (failOnErrors) {
throw new IllegalRuntimeStateException(msg);
} else {
LOG.warning(msg);
}
}
} finally {
scanner.close();
}
if (threads.isEmpty()) throw new IllegalRuntimeStateException(
"No threads found in threaddump"
);
return new ThreadDumpRuntime(threads, header);
}
public @Nonnull ThreadDumpRuntime fromString(@Nonnull String runtime) {
try {
InputStream is = new ByteArrayInputStream(runtime.getBytes("UTF-8"));
try {
return fromStream(is);
} finally {
try {
is.close();
} catch (IOException ex) {} // Ignore
}
} catch (UnsupportedEncodingException ex) {
throw new AssertionError(ex);
}
}
private ThreadDumpThread.Builder thread(String singleThread) {
Matcher matcher = THREAD_HEADER.matcher(singleThread);
if (!matcher.find()) return null;
ThreadDumpThread.Builder builder = new ThreadDumpThread.Builder();
builder.setName(matcher.group(1));
initHeader(builder, matcher.group(2));
String status = matcher.group(3);
if (status != null) {
builder.setThreadStatus(ThreadStatus.fromString(status));
}
final String trace = matcher.group(4);
if (trace != null) {
builder = initStacktrace(builder, trace, singleThread);
}
return builder;
}
private Builder initStacktrace(Builder builder, String trace, String wholeThread) {
ArrayList<StackTraceElement> traceElements = new ArrayList<StackTraceElement>();
List<ThreadLock.Monitor> monitors = new ArrayList<ThreadLock.Monitor>();
List<ThreadLock> synchronizers = new ArrayList<ThreadLock>();
ThreadLock waitingToLock = null; // Block waiting on monitor
ThreadLock waitingOnLock = null; // in Object.wait()
int depth = -1;
StringTokenizer tokenizer = new StringTokenizer(trace, "\n");
while (tokenizer.hasMoreTokens()) {
String line = tokenizer.nextToken();
StackTraceElement elem = traceElement(line);
if (elem != null) {
traceElements.add(elem);
depth++;
continue;
}
Matcher acquiredMatcher = ACQUIRED_LINE.matcher(line);
if (acquiredMatcher.find()) {
monitors.add(new ThreadLock.Monitor(createLock(acquiredMatcher), depth));
continue;
}
Matcher waitingToMatcher = WAITING_TO_LOCK_LINE.matcher(line);
if (waitingToMatcher.find()) {
if (waitingToLock != null) throw new IllegalRuntimeStateException(
"Waiting to lock reported several times per single thread >>>%n%s%n<<<%n", trace
);
waitingToLock = createLock(waitingToMatcher);
continue;
}
Matcher waitingOnMatcher = WAITING_ON_LINE.matcher(line);
if (waitingOnMatcher.find()) {
if (waitingOnLock != null) throw new IllegalRuntimeStateException(
"Waiting on lock reported several times per single thread >>>%n%s%n<<<%n", trace
);
waitingOnLock = createLock(waitingOnMatcher);
continue;
}
if (line.contains("Locked ownable synchronizers:")) {
while (tokenizer.hasMoreTokens()) {
line = tokenizer.nextToken();
if (line.contains("- None")) break;
Matcher matcher = OWNABLE_SYNCHRONIZER_LINE.matcher(line);
if (matcher.find()) {
synchronizers.add(createLock(matcher));
} else {
throw new IllegalRuntimeStateException("Unable to parse ownable synchronizer: " + line);
}
}
}
}
builder.setStacktrace(new StackTrace(traceElements));
ThreadStatus status = builder.getThreadStatus();
StackTraceElement innerFrame = builder.getStacktrace().getElement(0);
// Probably a bug in JVM/jstack but let's see what we can do
if (waitingOnLock == null && !status.isRunnable() && WAIT_TRACE_ELEMENT.equals(innerFrame)) {
HashSet<ThreadLock> acquiredLocks = new HashSet<ThreadLock>(monitors.size());
for (Monitor m: monitors) {
acquiredLocks.add(m.getLock());
}
if (acquiredLocks.size() == 1) {
waitingOnLock = acquiredLocks.iterator().next();
LOG.fine("FIXUP: Adjust lock state from 'locked' to 'waiting on' when thread entering Object.wait()");
LOG.fine(wholeThread);
}
}
if (waitingOnLock != null) {
// Eliminate self lock that is presented in threaddumps when in Object.wait(). It is a matter or convenience - not really a FIXUP
filterMonitors(monitors, waitingOnLock);
// 'waiting on' is reported even when blocked re-entering the monitor. Convert it from waitingOn to waitingTo
if (builder.getThreadStatus().isBlocked()) {
LOG.fine("FIXUP: Adjust lock state from 'waiting on' to 'waiting to' when thread re-acquiring the monitor after Object.wait()");
LOG.fine(wholeThread);
waitingToLock = waitingOnLock;
waitingOnLock = null;
}
}
// https://github.com/olivergondza/dumpling/issues/43
if (waitingOnLock != null && status.isRunnable()) {
// Presumably when entering or leaving the parked state.
// Remove the lock instead of fixing the thread status as there is
// no general way to tell PARKED and PARKED_TIMED apart.
LOG.fine("FIXUP: Remove 'waiting to' lock declared on RUNNABLE thread");
LOG.fine(wholeThread);
waitingOnLock = null;
}
// https://github.com/olivergondza/dumpling/issues/46
// The lock state is changed ahead of the thread state while there can be other threads still holding the monitor
if (status.isBlocked() && waitingToLock == null) {
Monitor monitor = getMonitorJustAcquired(monitors);
if (monitor != null) {
LOG.fine("FIXUP: Adjust lock state from 'locked' to 'waiting to' on BLOCKED thread");
LOG.fine(wholeThread);
waitingToLock = monitor.getLock();
monitors.remove(0);
} else {
LOG.fine("FIXUP: Adjust thread state from 'BLOCKED' to 'RUNNABLE' when monitor is missing");
LOG.fine(wholeThread);
builder.setThreadStatus(status = ThreadStatus.RUNNABLE);
}
}
if (waitingToLock != null && !status.isBlocked()) throw new IllegalRuntimeStateException(
"%s thread declares waitingTo lock: >>>%n%s%n<<<%n", status, wholeThread
);
if (waitingOnLock != null && !status.isWaiting() && !status.isParked()) throw new IllegalRuntimeStateException(
"%s thread declares waitingOn lock: >>>%n%s%n<<<%n", status, wholeThread
);
builder.setAcquiredMonitors(monitors);
builder.setAcquiredSynchronizers(synchronizers);
builder.setWaitingToLock(waitingToLock);
builder.setWaitingOnLock(waitingOnLock);
return builder;
}
// get monitor acquired on current stackframe, null when it was acquired earlier or not monitor is held
private Monitor getMonitorJustAcquired(List<ThreadLock.Monitor> monitors) {
if (monitors.isEmpty()) return null;
Monitor monitor = monitors.get(0);
if (monitor.getDepth() != 0) return null;
for (Monitor duplicateCandidate: monitors) {
if (monitor.equals(duplicateCandidate)) continue; // skip first - equality includes monitor depth
if (monitor.getLock().equals(duplicateCandidate.getLock())) return null; // Acquired earlier
}
return monitor;
}
private static final WeakHashMap<String, StackTraceElement> traceElementCache = new WeakHashMap<String, StackTraceElement>();
private StackTraceElement traceElement(String line) {
if (!line.startsWith("\tat ") && !line.startsWith(" at ")) return null;
StackTraceElement cached = traceElementCache.get(line);
if (cached != null) return cached;
Matcher match = STACK_TRACE_ELEMENT_LINE.matcher(line);
if (!match.find()) return null;
String sourceFile = match.group(3);
int sourceLine = match.group(4) == null
? -1
: Integer.parseInt(match.group(4).substring(1))
;
if (sourceLine == -1 && "Native Method".equals(match.group(3))) {
sourceFile = null;
sourceLine = -2; // Magic value for native methods
}
StackTraceElement element = StackTrace.element(
match.group(1), match.group(2), sourceFile, sourceLine
);
traceElementCache.put(line, element);
return element;
}
private void filterMonitors(List<ThreadLock.Monitor> monitors, ThreadLock lock) {
for (Iterator<Monitor> it = monitors.iterator(); it.hasNext();) {
Monitor m = it.next();
if (m.getLock().equals(lock)) {
it.remove();
}
}
}
private @Nonnull ThreadLock createLock(Matcher matcher) {
return new ThreadLock(matcher.group(2), parseLong(matcher.group(1)));
}
private void initHeader(ThreadDumpThread.Builder builder, String attrs) {
StringTokenizer tknzr = new StringTokenizer(attrs, " ");
while (tknzr.hasMoreTokens()) {
String token = tknzr.nextToken();
if ("daemon".equals(token)) builder.setDaemon(true);
else if (token.startsWith("prio=")) builder.setPriority(Integer.parseInt(token.substring(5)));
else if (token.startsWith("tid=")) builder.setTid(parseLong(token.substring(4)));
else if (token.startsWith("nid=")) builder.setNid(parseNid(token.substring(4)));
else if (token.matches("#\\d+")) builder.setId(Integer.parseInt(token.substring(1)));
}
}
private long parseNid(String value) {
return value.startsWith("0x")
? parseLong(value.substring(2))
: Long.parseLong(value) // Dumpling human readable output
;
}
/*package*/ static long parseLong(String value) {
if (value.startsWith("0x")) {
// Oracle JDK on OS X do not use prefix for tid - so we need to be able to read both
// https://github.com/olivergondza/dumpling/issues/59
value = value.substring(2);
}
// Long.parseLong is faster but unsuitable in some cases: https://github.com/olivergondza/dumpling/issues/71
return new BigInteger(value, 16).longValue();
}
}