All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glowroot.common.model.MutableProfile Maven / Gradle / Ivy

There is a newer version: 0.9.24
Show newest version
/*
 * Copyright 2015-2016 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 org.glowroot.agent.shaded.fasterxml.jackson.core.JsonGenerator;
import org.glowroot.agent.shaded.fasterxml.jackson.databind.ObjectMapper;
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 packageNameIndexes = Maps.newHashMap();
    private final Map classNameIndexes = Maps.newHashMap();
    private final Map methodNameIndexes = Maps.newHashMap();
    private final Map fileNameIndexes = Maps.newHashMap();

    private final List packageNames = Lists.newArrayList();
    private final List classNames = Lists.newArrayList();
    private final List methodNames = Lists.newArrayList();
    private final List fileNames = Lists.newArrayList();

    private final List 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 stackTraceElements, Thread.State threadState) {

        for (StackTraceElement stackTraceElement : stackTraceElements) {
            if (stackTraceElement.getMethodName() == null) {
                // methodName can be null after hotswapping under Eclipse debugger
                // in which case seems best to just ignore the stack trace capture altogether
                return;
            }
        }
        PeekingIterator i =
                Iterators.peekingIterator(Lists.reverse(stackTraceElements).iterator());
        ProfileNode lastMatchedNode = null;
        List 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(Strings.nullToEmpty(stackTraceElement.getMethodName()),
                            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 includes, List excludes) {
        unfilteredSampleCount = getSampleCount();
        for (String include : includes) {
            for (Iterator 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 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 toBeVisited = new ArrayDeque();
        for (ProfileNode rootNode : rootNodes) {
            toBeVisited.add(rootNode);
        }
        ProfileNode node;
        while ((node = toBeVisited.poll()) != null) {
            for (Iterator 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 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 nameIndexes,
            List 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 toBeMergedNames,
            Map existingIndexes, List 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 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> 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 flatNodes,
                List destinationRootNodes) {
            destinationStack.push(destinationRootNodes);
            PeekingIterator 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 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 {

        private final List nodes;

        public ProfileNodeCollector(ProfileNode rootNode, List nodes) {
            super(rootNode);
            this.nodes = nodes;
        }

        @Override
        public List 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 {

        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 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 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 {

        private ProfileResetMatches(ProfileNode rootNode) {
            super(rootNode);
        }

        @Override
        public List visit(ProfileNode node, int depth) {
            node.matched = false;
            return node.childNodes;
        }
    }

    private class ProfileWriter extends Traverser {

        private final JsonGenerator jg;

        private ProfileWriter(ProfileNode rootNode, JsonGenerator jg) throws IOException {
            super(rootNode);
            this.jg = jg;
        }

        @Override
        public List 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 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 {

        private final JsonGenerator jg;
        private int height;

        private FlameGraphWriter(ProfileNode rootNode, JsonGenerator jg) throws IOException {
            super(rootNode);
            this.jg = jg;
        }

        @Override
        public List 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();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy