/*
* Copyright 2015-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.glowroot.common.model;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.PeekingIterator;
import com.google.common.collect.Queues;
import com.google.common.io.CharStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.common.util.ObjectMappers;
import org.glowroot.common.util.Traverser;
import org.glowroot.wire.api.model.ProfileOuterClass.Profile;
public class MutableProfile {
private static final Logger logger = LoggerFactory.getLogger(MutableProfile.class);
private static final ObjectMapper mapper = ObjectMappers.create();
// TODO use primitive maps, e.g. from GS collections
private final Map<String, Integer> packageNameIndexes = Maps.newHashMap();
private final Map<String, Integer> classNameIndexes = Maps.newHashMap();
private final Map<String, Integer> methodNameIndexes = Maps.newHashMap();
private final Map<String, Integer> fileNameIndexes = Maps.newHashMap();
private final List<String> packageNames = Lists.newArrayList();
private final List<String> classNames = Lists.newArrayList();
private final List<String> methodNames = Lists.newArrayList();
private final List<String> fileNames = Lists.newArrayList();
private final List<ProfileNode> rootNodes = Lists.newArrayList();
// retain original sample count for in case of filtered profile
private long unfilteredSampleCount = -1;
// this method is not used that often (only for traces with > 20 stack trace samples) so ok
// that it does not have most optimal implementation (converts unnecessarily to profile tree)
public void merge(MutableProfile profile) {
merge(profile.toProto());
}
public void merge(Profile profile) {
Merger merger = new Merger(profile);
merger.merge(profile.getNodeList(), rootNodes);
}
public void merge(List<StackTraceElement> stackTraceElements, Thread.State threadState) {
PeekingIterator<StackTraceElement> i =
Iterators.peekingIterator(Lists.reverse(stackTraceElements).iterator());
ProfileNode lastMatchedNode = null;
List<ProfileNode> mergeIntoNodes = rootNodes;
boolean lookingForMatch = true;
while (i.hasNext()) {
StackTraceElement stackTraceElement = i.next();
String fullClassName = stackTraceElement.getClassName();
int index = fullClassName.lastIndexOf('.');
String packageName;
String className;
if (index == -1) {
packageName = "";
className = fullClassName;
} else {
packageName = fullClassName.substring(0, index);
className = fullClassName.substring(index + 1);
}
int packageNameIndex = getNameIndex(packageName, packageNameIndexes, packageNames);
int classNameIndex = getNameIndex(className, classNameIndexes, classNames);
int methodNameIndex =
getNameIndex(MoreObjects.firstNonNull(stackTraceElement.getMethodName(),
"<null method name>"), methodNameIndexes, methodNames);
int fileNameIndex = getNameIndex(Strings.nullToEmpty(stackTraceElement.getFileName()),
fileNameIndexes, fileNames);
int lineNumber = stackTraceElement.getLineNumber();
Profile.LeafThreadState leafThreadState =
i.hasNext() ? Profile.LeafThreadState.NONE : getThreadState(threadState);
ProfileNode node = null;
if (lookingForMatch) {
for (ProfileNode childNode : mergeIntoNodes) {
if (isMatch(childNode, packageNameIndex, classNameIndex, methodNameIndex,
fileNameIndex, lineNumber, leafThreadState)) {
node = childNode;
break;
}
}
}
if (node == null) {
lookingForMatch = false;
node = new ProfileNode(packageNameIndex, classNameIndex, methodNameIndex,
fileNameIndex, lineNumber, leafThreadState);
mergeIntoNodes.add(node);
}
node.sampleCount++;
lastMatchedNode = node;
mergeIntoNodes = lastMatchedNode.childNodes;
}
}
public void filter(List<String> includes, List<String> excludes) {
unfilteredSampleCount = getSampleCount();
for (String include : includes) {
for (Iterator<ProfileNode> i = rootNodes.iterator(); i.hasNext();) {
ProfileNode rootNode = i.next();
new ProfileFilterer(rootNode, include, false).traverse();
if (rootNode.matched) {
new ProfileResetMatches(rootNode).traverse();
} else {
i.remove();
}
}
}
for (String exclude : excludes) {
for (Iterator<ProfileNode> i = rootNodes.iterator(); i.hasNext();) {
ProfileNode rootNode = i.next();
new ProfileFilterer(rootNode, exclude, true).traverse();
if (rootNode.matched) {
i.remove();
}
}
}
}
public void truncateBranches(int minSamples) {
Deque<ProfileNode> toBeVisited = new ArrayDeque<ProfileNode>();
for (ProfileNode rootNode : rootNodes) {
toBeVisited.add(rootNode);
}
ProfileNode node;
while ((node = toBeVisited.poll()) != null) {
for (Iterator<ProfileNode> i = node.childNodes.iterator(); i.hasNext();) {
ProfileNode childNode = i.next();
if (childNode.sampleCount < minSamples) {
i.remove();
// TODO capture sampleCount per timerName of non-ellipsed structure
// and use this in UI dropdown filter of timer names
// (currently sampleCount per timerName of ellipsed structure is used)
node.ellipsedSampleCount += childNode.sampleCount;
} else {
toBeVisited.add(childNode);
}
}
}
}
public long getSampleCount() {
long sampleCount = 0;
for (ProfileNode rootNode : rootNodes) {
sampleCount += rootNode.sampleCount;
}
return sampleCount;
}
public long getUnfilteredSampleCount() {
if (unfilteredSampleCount == -1) {
return getSampleCount();
} else {
return unfilteredSampleCount;
}
}
public Profile toProto() {
List<Profile.ProfileNode> nodes = Lists.newArrayList();
for (ProfileNode rootNode : rootNodes) {
new ProfileNodeCollector(rootNode, nodes).traverse();
}
return Profile.newBuilder()
.addAllPackageName(packageNames)
.addAllClassName(classNames)
.addAllMethodName(methodNames)
.addAllFileName(fileNames)
.addAllNode(nodes)
.build();
}
public String toJson() throws IOException {
StringBuilder sb = new StringBuilder();
JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
writeJson(jg);
jg.close();
return sb.toString();
}
public void writeJson(JsonGenerator jg) throws IOException {
jg.writeStartObject();
jg.writeNumberField("unfilteredSampleCount", getUnfilteredSampleCount());
jg.writeArrayFieldStart("rootNodes");
for (ProfileNode rootNode : rootNodes) {
new ProfileWriter(rootNode, jg).traverse();
}
jg.writeEndArray();
jg.writeEndObject();
}
public String toFlameGraphJson() throws IOException {
StringBuilder sb = new StringBuilder();
JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeNumberField("totalSampleCount", getSampleCount());
jg.writeArrayFieldStart("rootNodes");
int height = 0;
for (ProfileNode rootNode : rootNodes) {
if (rootNode.sampleCount > rootNode.ellipsedSampleCount) {
FlameGraphWriter flameGraphWriter = new FlameGraphWriter(rootNode, jg);
flameGraphWriter.traverse();
height = Math.max(height, flameGraphWriter.height);
}
}
jg.writeEndArray();
jg.writeNumberField("height", height);
jg.writeEndObject();
jg.close();
return sb.toString();
}
private static int getNameIndex(String name, Map<String, Integer> nameIndexes,
List<String> names) {
Integer index = nameIndexes.get(name);
if (index == null) {
index = names.size();
names.add(name);
nameIndexes.put(name, index);
}
return index;
}
private static Profile.LeafThreadState getThreadState(@Nullable Thread.State state) {
if (state == null) {
return Profile.LeafThreadState.NONE;
}
switch (state) {
case NEW:
return Profile.LeafThreadState.NEW;
case RUNNABLE:
return Profile.LeafThreadState.RUNNABLE;
case BLOCKED:
return Profile.LeafThreadState.BLOCKED;
case WAITING:
return Profile.LeafThreadState.WAITING;
case TIMED_WAITING:
return Profile.LeafThreadState.TIMED_WAITING;
case TERMINATED:
return Profile.LeafThreadState.TERMINATED;
default:
logger.warn("unexpected thread state: {}", state);
return Profile.LeafThreadState.NONE;
}
}
private static boolean isMatch(ProfileNode profileNode, int packageNameIndex,
int classNameIndex, int methodNameIndex, int fileNameIndex, int lineNumber,
Profile.LeafThreadState leafThreadState) {
// checking line number first since most likely to be different
return lineNumber == profileNode.lineNumber
&& fileNameIndex == profileNode.fileNameIndex
&& leafThreadState == profileNode.leafThreadState
&& methodNameIndex == profileNode.methodNameIndex
&& classNameIndex == profileNode.classNameIndex
&& packageNameIndex == profileNode.packageNameIndex;
}
private static int[] makeIndexMapping(List<String> toBeMergedNames,
Map<String, Integer> existingIndexes, List<String> existingNames) {
int[] indexMapping = new int[toBeMergedNames.size()];
for (int i = 0; i < toBeMergedNames.size(); i++) {
String toBeMergedName = toBeMergedNames.get(i);
Integer existingIndex = existingIndexes.get(toBeMergedName);
if (existingIndex == null) {
int newIndex = existingNames.size();
existingNames.add(toBeMergedName);
existingIndexes.put(toBeMergedName, newIndex);
indexMapping[i] = newIndex;
} else {
indexMapping[i] = existingIndex;
}
}
return indexMapping;
}
private class ProfileNode {
private final int packageNameIndex;
private final int classNameIndex;
private final int methodNameIndex;
private final int fileNameIndex;
private final int lineNumber;
private final Profile.LeafThreadState leafThreadState;
private long sampleCount;
private List<ProfileNode> childNodes = Lists.newArrayListWithCapacity(2);
// these fields are only used for filtering
private @Nullable String text;
private @Nullable String textUpper;
private boolean matched;
private long ellipsedSampleCount;
private ProfileNode(int packageNameIndex, int classNameIndex, int methodNameIndex,
int fileNameIndex, int lineNumber, Profile.LeafThreadState leafThreadState) {
this.packageNameIndex = packageNameIndex;
this.classNameIndex = classNameIndex;
this.methodNameIndex = methodNameIndex;
this.fileNameIndex = fileNameIndex;
this.lineNumber = lineNumber;
this.leafThreadState = leafThreadState;
}
private String getText() {
if (text == null) {
String packageName = packageNames.get(packageNameIndex);
String className = classNames.get(classNameIndex);
String fullClassName;
if (packageName.isEmpty()) {
fullClassName = className;
} else {
fullClassName = packageName + '.' + className;
}
text = new StackTraceElement(fullClassName, methodNames.get(methodNameIndex),
fileNames.get(fileNameIndex), lineNumber).toString();
}
return text;
}
private String getTextUpper() {
if (textUpper == null) {
textUpper = getText().toUpperCase(Locale.ENGLISH);
}
return textUpper;
}
}
private class Merger {
private final int[] packageNameIndexMapping;
private final int[] classNameIndexMapping;
private final int[] methodNameIndexMapping;
private final int[] fileNameIndexMapping;
private final Deque<List<ProfileNode>> destinationStack = Queues.newArrayDeque();
private Merger(Profile toBeMergedProfile) {
packageNameIndexMapping = makeIndexMapping(toBeMergedProfile.getPackageNameList(),
packageNameIndexes, packageNames);
classNameIndexMapping = makeIndexMapping(toBeMergedProfile.getClassNameList(),
classNameIndexes, classNames);
methodNameIndexMapping = makeIndexMapping(toBeMergedProfile.getMethodNameList(),
methodNameIndexes, methodNames);
fileNameIndexMapping = makeIndexMapping(toBeMergedProfile.getFileNameList(),
fileNameIndexes, fileNames);
}
private void merge(List<Profile.ProfileNode> flatNodes,
List<ProfileNode> destinationRootNodes) {
destinationStack.push(destinationRootNodes);
PeekingIterator<Profile.ProfileNode> i =
Iterators.peekingIterator(flatNodes.iterator());
while (i.hasNext()) {
Profile.ProfileNode flatNode = i.next();
int destinationDepth = destinationStack.size() - 1;
for (int j = 0; j < destinationDepth - flatNode.getDepth(); j++) {
// TODO optimize: faster way to pop multiple elements at once
destinationStack.pop();
}
ProfileNode destinationNode = mergeOne(flatNode, destinationStack.getFirst());
if (i.hasNext() && i.peek().getDepth() > flatNode.getDepth()) {
destinationStack.push(destinationNode.childNodes);
}
}
}
private ProfileNode mergeOne(Profile.ProfileNode toBeMergedNode,
List<ProfileNode> destinationNodes) {
int toBeMergedPackageNameIndex =
packageNameIndexMapping[toBeMergedNode.getPackageNameIndex()];
int toBeMergedClassNameIndex =
classNameIndexMapping[toBeMergedNode.getClassNameIndex()];
int toBeMergedMethodNameIndex =
methodNameIndexMapping[toBeMergedNode.getMethodNameIndex()];
int toBeMergedFileNameIndex = fileNameIndexMapping[toBeMergedNode.getFileNameIndex()];
int toBeMergedLineNumber = toBeMergedNode.getLineNumber();
Profile.LeafThreadState toBeMergedLeafThreadState = toBeMergedNode.getLeafThreadState();
for (ProfileNode destinationNode : destinationNodes) {
if (isMatch(destinationNode, toBeMergedPackageNameIndex, toBeMergedClassNameIndex,
toBeMergedMethodNameIndex, toBeMergedFileNameIndex, toBeMergedLineNumber,
toBeMergedLeafThreadState)) {
merge(toBeMergedNode, destinationNode);
return destinationNode;
}
}
// no match found
ProfileNode destinationNode = new ProfileNode(toBeMergedPackageNameIndex,
toBeMergedClassNameIndex, toBeMergedMethodNameIndex, toBeMergedFileNameIndex,
toBeMergedLineNumber, toBeMergedLeafThreadState);
destinationNodes.add(destinationNode);
merge(toBeMergedNode, destinationNode);
return destinationNode;
}
private void merge(Profile.ProfileNode toBeMergedNode, ProfileNode destinationNode) {
destinationNode.sampleCount += toBeMergedNode.getSampleCount();
}
}
// using Traverser to avoid StackOverflowError caused by a recursive algorithm
private static class ProfileNodeCollector extends Traverser<ProfileNode, RuntimeException> {
private final List<Profile.ProfileNode> nodes;
public ProfileNodeCollector(ProfileNode rootNode, List<Profile.ProfileNode> nodes) {
super(rootNode);
this.nodes = nodes;
}
@Override
public List<ProfileNode> visit(ProfileNode node, int depth) {
nodes.add(Profile.ProfileNode.newBuilder()
.setDepth(depth)
.setPackageNameIndex(node.packageNameIndex)
.setClassNameIndex(node.classNameIndex)
.setMethodNameIndex(node.methodNameIndex)
.setFileNameIndex(node.fileNameIndex)
.setLineNumber(node.lineNumber)
.setLeafThreadState(node.leafThreadState)
.setSampleCount(node.sampleCount)
.build());
return node.childNodes;
}
}
private static class ProfileFilterer extends Traverser<ProfileNode, RuntimeException> {
private final String filterTextUpper;
private final boolean exclusion;
private ProfileFilterer(ProfileNode rootNode, String filterText, boolean exclusion) {
super(rootNode);
this.filterTextUpper = filterText.toUpperCase(Locale.ENGLISH);
this.exclusion = exclusion;
}
@Override
public List<ProfileNode> visit(ProfileNode node, int depth) {
if (isMatch(node)) {
node.matched = true;
// no need to visit children
return ImmutableList.of();
}
return node.childNodes;
}
@Override
public void revisitAfterChildren(ProfileNode node) {
if (node.matched) {
// if exclusion then node will be removed by parent
// if not exclusion then keep node and all children
return;
}
if (node.childNodes.isEmpty()) {
return;
}
if (removeNode(node)) {
// node will be removed by parent
if (exclusion) {
node.matched = true;
}
return;
}
if (!exclusion) {
node.matched = true;
}
// node is a partial match, need to filter it out
long filteredSampleCount = 0;
for (Iterator<ProfileNode> i = node.childNodes.iterator(); i.hasNext();) {
ProfileNode childNode = i.next();
if (exclusion == !childNode.matched) {
filteredSampleCount += childNode.sampleCount;
} else {
i.remove();
}
}
node.sampleCount = filteredSampleCount;
}
private boolean isMatch(ProfileNode node) {
String textUpper = node.getTextUpper();
if (textUpper.contains(filterTextUpper)) {
return true;
}
Profile.LeafThreadState leafThreadState = node.leafThreadState;
if (leafThreadState != null) {
String leafThreadStateUpper = leafThreadState.name().toUpperCase(Locale.ENGLISH);
if (leafThreadStateUpper.contains(filterTextUpper)) {
return true;
}
}
return false;
}
private boolean removeNode(ProfileNode node) {
if (exclusion) {
return hasOnlyMatchedChildren(node);
} else {
return hasNoMatchedChildren(node);
}
}
private boolean hasOnlyMatchedChildren(ProfileNode node) {
for (ProfileNode childNode : node.childNodes) {
if (!childNode.matched) {
return false;
}
}
return true;
}
private boolean hasNoMatchedChildren(ProfileNode node) {
for (ProfileNode childNode : node.childNodes) {
if (childNode.matched) {
return false;
}
}
return true;
}
}
private static class ProfileResetMatches extends Traverser<ProfileNode, RuntimeException> {
private ProfileResetMatches(ProfileNode rootNode) {
super(rootNode);
}
@Override
public List<ProfileNode> visit(ProfileNode node, int depth) {
node.matched = false;
return node.childNodes;
}
}
private class ProfileWriter extends Traverser<ProfileNode, IOException> {
private final JsonGenerator jg;
private ProfileWriter(ProfileNode rootNode, JsonGenerator jg) throws IOException {
super(rootNode);
this.jg = jg;
}
@Override
public List<ProfileNode> visit(ProfileNode node, int depth) throws IOException {
jg.writeStartObject();
jg.writeStringField("stackTraceElement", node.getText());
Profile.LeafThreadState leafThreadState = node.leafThreadState;
if (leafThreadState != Profile.LeafThreadState.NONE) {
jg.writeStringField("leafThreadState", leafThreadState.name());
}
jg.writeNumberField("sampleCount", node.sampleCount);
long ellipsedSampleCount = node.ellipsedSampleCount;
if (ellipsedSampleCount > 0) {
jg.writeNumberField("ellipsedSampleCount", ellipsedSampleCount);
}
List<ProfileNode> childNodes = node.childNodes;
if (!childNodes.isEmpty()) {
jg.writeArrayFieldStart("childNodes");
}
return childNodes;
}
@Override
public void revisitAfterChildren(ProfileNode node) throws IOException {
if (!node.childNodes.isEmpty()) {
jg.writeEndArray();
}
jg.writeEndObject();
}
}
private class FlameGraphWriter extends Traverser<ProfileNode, IOException> {
private final JsonGenerator jg;
private int height;
private FlameGraphWriter(ProfileNode rootNode, JsonGenerator jg) throws IOException {
super(rootNode);
this.jg = jg;
}
@Override
public List<ProfileNode> visit(ProfileNode node, int depth) throws IOException {
height = Math.max(height, depth + 1);
jg.writeStartObject();
jg.writeStringField("name", node.getText());
jg.writeNumberField("value", node.sampleCount);
if (!node.childNodes.isEmpty()) {
jg.writeArrayFieldStart("children");
}
return node.childNodes;
}
@Override
public void revisitAfterChildren(ProfileNode node) throws IOException {
if (!node.childNodes.isEmpty()) {
jg.writeEndArray();
}
jg.writeEndObject();
}
}
}