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

org.glowroot.local.ui.TransactionCommonService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014-2015 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.local.ui;

import java.io.IOException;
import java.sql.SQLException;
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.shaded.google.common.collect.ImmutableList;
import org.glowroot.shaded.google.common.collect.Lists;
import org.glowroot.shaded.google.common.collect.Maps;

import org.glowroot.collector.Aggregate;
import org.glowroot.collector.AggregateCollector;
import org.glowroot.collector.AggregateIntervalCollector;
import org.glowroot.collector.ProfileAggregate;
import org.glowroot.collector.QueryAggregate;
import org.glowroot.collector.QueryComponent.AggregateQuery;
import org.glowroot.collector.TransactionSummary;
import org.glowroot.common.ScratchBuffer;
import org.glowroot.common.Traverser;
import org.glowroot.config.ConfigService;
import org.glowroot.local.store.AggregateDao;
import org.glowroot.local.store.AggregateDao.MergedAggregate;
import org.glowroot.local.store.AggregateDao.TransactionSummarySortOrder;
import org.glowroot.local.store.QueryResult;
import org.glowroot.local.store.TransactionSummaryQuery;
import org.glowroot.transaction.model.ProfileNode;

import static org.glowroot.shaded.google.common.base.Preconditions.checkNotNull;

class TransactionCommonService {

    private final AggregateDao aggregateDao;
    private final @Nullable AggregateCollector aggregateCollector;
    private final ConfigService configService;

    TransactionCommonService(AggregateDao aggregateDao,
            @Nullable AggregateCollector aggregateCollector, ConfigService configService) {
        this.aggregateDao = aggregateDao;
        this.aggregateCollector = aggregateCollector;
        this.configService = configService;
    }

    // from is non-inclusive
    TransactionSummary readOverallSummary(String transactionType, long from, long to)
            throws SQLException {
        List orderedIntervalCollectors =
                getOrderedIntervalCollectorsInRange(from, to);
        if (orderedIntervalCollectors.isEmpty()) {
            return aggregateDao.readOverallSummary(transactionType, from, to);
        }
        long revisedTo = getRevisedTo(to, orderedIntervalCollectors);
        TransactionSummary overallSummary =
                aggregateDao.readOverallSummary(transactionType, from, revisedTo);
        return mergeInLiveOverallSummaries(transactionType, overallSummary,
                orderedIntervalCollectors);
    }

    // query.from() is non-inclusive
    QueryResult readTransactionSummaries(TransactionSummaryQuery query)
            throws SQLException {
        List orderedIntervalCollectors =
                getOrderedIntervalCollectorsInRange(query.from(), query.to());
        if (orderedIntervalCollectors.isEmpty()) {
            return aggregateDao.readTransactionSummaries(query);
        }
        long revisedTo = getRevisedTo(query.to(), orderedIntervalCollectors);
        TransactionSummaryQuery revisedQuery = query.withTo(revisedTo);
        QueryResult queryResult =
                aggregateDao.readTransactionSummaries(revisedQuery);
        if (orderedIntervalCollectors.isEmpty()) {
            return queryResult;
        }
        return mergeInLiveTransactionSummaries(revisedQuery, queryResult,
                orderedIntervalCollectors);
    }

    // from is non-inclusive
    boolean shouldHaveQueries(String transactionType, @Nullable String transactionName, long from,
            long to) throws SQLException {
        if (transactionName == null) {
            return aggregateDao.shouldHaveOverallQueries(transactionType, from, to);
        } else {
            return aggregateDao.shouldHaveTransactionQueries(transactionType, transactionName, from,
                    to);
        }
    }

    // from is non-inclusive
    boolean shouldHaveProfile(String transactionType, @Nullable String transactionName, long from,
            long to) throws SQLException {
        if (transactionName == null) {
            return aggregateDao.shouldHaveOverallProfile(transactionType, from, to);
        } else {
            return aggregateDao.shouldHaveTransactionProfile(transactionType, transactionName, from,
                    to);
        }
    }

    // from is INCLUSIVE
    List getAggregates(String transactionType, @Nullable String transactionName,
            long from, long to, long liveCaptureTime) throws Exception {
        int rollupLevel = aggregateDao.getRollupLevelForView(from, to);
        List orderedIntervalCollectors =
                getOrderedIntervalCollectorsInRange(from - 1, to);
        long revisedTo = getRevisedTo(to, orderedIntervalCollectors);
        List aggregates = getAggregatesFromDao(transactionType, transactionName, from,
                revisedTo, rollupLevel);
        if (rollupLevel == 0) {
            aggregates = Lists.newArrayList(aggregates);
            aggregates.addAll(getLiveAggregates(transactionType, transactionName,
                    orderedIntervalCollectors, liveCaptureTime));
            return aggregates;
        }
        long nonRolledUpFrom = from;
        if (!aggregates.isEmpty()) {
            long lastRolledUpTime = aggregates.get(aggregates.size() - 1).captureTime();
            nonRolledUpFrom = Math.max(nonRolledUpFrom, lastRolledUpTime + 1);
        }
        List orderedNonRolledUpAggregates = Lists.newArrayList();
        orderedNonRolledUpAggregates.addAll(getAggregatesFromDao(transactionType, transactionName,
                nonRolledUpFrom, revisedTo, 0));
        orderedNonRolledUpAggregates.addAll(getLiveAggregates(transactionType, transactionName,
                orderedIntervalCollectors, liveCaptureTime));
        aggregates = Lists.newArrayList(aggregates);
        aggregates.addAll(rollUp(transactionType, transactionName, orderedNonRolledUpAggregates,
                liveCaptureTime, rollupLevel));
        return aggregates;
    }

    // from is non-inclusive
    Map> getQueries(String transactionType,
            @Nullable String transactionName, long from, long to) throws Exception {
        List queryAggregates =
                getQueryAggregates(transactionType, transactionName, from, to);
        return AggregateMerging.getOrderedAndTruncatedQueries(queryAggregates,
                configService.getAdvancedConfig().maxAggregateQueriesPerQueryType());
    }

    // from is non-inclusive
    ProfileNode getProfile(String transactionType, @Nullable String transactionName, long from,
            long to, List includes, List excludes, double truncateLeafPercentage)
                    throws Exception {
        List profileAggregate =
                getProfileAggregates(transactionType, transactionName, from, to);
        ProfileNode syntheticRootNode = AggregateMerging.getMergedProfile(profileAggregate);
        long syntheticRootNodeSampleCount = syntheticRootNode.getSampleCount();
        if (!includes.isEmpty() || !excludes.isEmpty()) {
            filter(syntheticRootNode, includes, excludes);
        }
        if (truncateLeafPercentage != 0) {
            int minSamples =
                    (int) Math.ceil(syntheticRootNode.getSampleCount() * truncateLeafPercentage);
            // don't truncate any root nodes
            truncateLeafs(syntheticRootNode.getChildNodes(), minSamples);
        }
        // retain original sample count for synthetic root node in case of filtered profile
        syntheticRootNode.setSampleCount(syntheticRootNodeSampleCount);
        return syntheticRootNode;
    }

    // from is non-inclusive
    private List getOrderedIntervalCollectorsInRange(long from,
            long to) {
        if (aggregateCollector == null) {
            return ImmutableList.of();
        }
        return aggregateCollector.getOrderedIntervalCollectorsInRange(from, to);
    }

    // from is INCLUSIVE
    private List getAggregatesFromDao(String transactionType,
            @Nullable String transactionName, long from, long to, int rollupLevel)
                    throws SQLException {
        if (transactionName == null) {
            return aggregateDao.readOverallAggregates(transactionType, from, to, rollupLevel);
        } else {
            return aggregateDao.readTransactionAggregates(transactionType, transactionName, from,
                    to, rollupLevel);
        }
    }

    // this method may return some rolled up query aggregates and some non-rolled up
    // they are all distinct though
    // this is ok since the results of this method are currently just aggregated into single
    // result as opposed to charted over time period
    //
    // from is non-inclusive
    private List getQueryAggregates(String transactionType,
            @Nullable String transactionName, long from, long to) throws Exception {
        int initialRollupLevel = aggregateDao.getRollupLevelForView(from, to);
        List orderedIntervalCollectors =
                getOrderedIntervalCollectorsInRange(from, to);
        long revisedTo = getRevisedTo(to, orderedIntervalCollectors);
        long revisedFrom = from;
        List orderedQueryAggregates = Lists.newArrayList();
        for (int rollupLevel = initialRollupLevel; rollupLevel >= 0; rollupLevel--) {
            List queryAggregates = getQueryAggregatesFromDao(transactionType,
                    transactionName, from, revisedTo, rollupLevel);
            if (!queryAggregates.isEmpty()) {
                long lastRolledUpTime =
                        queryAggregates.get(queryAggregates.size() - 1).captureTime();
                revisedFrom = Math.max(revisedFrom, lastRolledUpTime + 1);
            }
            orderedQueryAggregates.addAll(queryAggregates);
            if (revisedFrom > revisedTo) {
                break;
            }
        }
        orderedQueryAggregates.addAll(getLiveQueryAggregates(transactionType, transactionName,
                orderedIntervalCollectors));
        return orderedQueryAggregates;
    }

    // from is non-inclusive
    private List getQueryAggregatesFromDao(String transactionType,
            @Nullable String transactionName, long from, long to, int rollupLevel)
                    throws SQLException {
        if (transactionName == null) {
            return aggregateDao.readOverallQueryAggregates(transactionType, from, to, rollupLevel);
        } else {
            return aggregateDao.readTransactionQueryAggregates(transactionType, transactionName,
                    from, to, rollupLevel);
        }
    }

    // this method may return some rolled up profile aggregates and some non-rolled up
    // they are all distinct though
    // this is ok since the results of this method are currently just aggregated into single
    // result as opposed to charted over time period
    //
    // from is non-inclusive
    private List getProfileAggregates(String transactionType,
            @Nullable String transactionName, long from, long to) throws Exception {
        int initialRollupLevel = aggregateDao.getRollupLevelForView(from, to);
        List orderedIntervalCollectors =
                getOrderedIntervalCollectorsInRange(from, to);
        long revisedTo = getRevisedTo(to, orderedIntervalCollectors);
        long revisedFrom = from;
        List orderedProfileAggregates = Lists.newArrayList();
        for (int rollupLevel = initialRollupLevel; rollupLevel >= 0; rollupLevel--) {
            List profileAggregates = getProfileAggregatesFromDao(transactionType,
                    transactionName, revisedFrom, revisedTo, rollupLevel);
            if (!profileAggregates.isEmpty()) {
                long lastRolledUpTime =
                        profileAggregates.get(profileAggregates.size() - 1).captureTime();
                revisedFrom = Math.max(revisedFrom, lastRolledUpTime + 1);
            }
            orderedProfileAggregates.addAll(profileAggregates);
            if (revisedFrom > revisedTo) {
                break;
            }
        }
        orderedProfileAggregates.addAll(getLiveProfileAggregates(transactionType, transactionName,
                orderedIntervalCollectors));
        return orderedProfileAggregates;
    }

    // from is non-inclusive
    private List getProfileAggregatesFromDao(String transactionType,
            @Nullable String transactionName, long from, long to, int rollupLevel)
                    throws SQLException {
        if (transactionName == null) {
            return aggregateDao.readOverallProfileAggregates(transactionType, from, to,
                    rollupLevel);
        } else {
            return aggregateDao.readTransactionProfileAggregates(transactionType, transactionName,
                    from, to, rollupLevel);
        }
    }

    private List rollUp(String transactionType, @Nullable String transactionName,
            List orderedNonRolledUpAggregates, long liveCaptureTime, int rollupLevel)
                    throws Exception {
        long fixedIntervalMillis =
                configService.getRollupConfigs().get(rollupLevel).intervalMillis();
        List rolledUpAggregates = Lists.newArrayList();
        ScratchBuffer scratchBuffer = new ScratchBuffer();
        MergedAggregate currMergedAggregate = null;
        long currRollupTime = Long.MIN_VALUE;
        for (Aggregate nonRolledUpAggregate : orderedNonRolledUpAggregates) {
            long rollupTime = AggregateDao.getNextRollupTime(
                    nonRolledUpAggregate.captureTime(), fixedIntervalMillis);
            if (rollupTime != currRollupTime && currMergedAggregate != null) {
                rolledUpAggregates.add(currMergedAggregate.toAggregate(scratchBuffer));
                currMergedAggregate = new MergedAggregate(Math.min(rollupTime, liveCaptureTime),
                        transactionType, transactionName,
                        configService.getAdvancedConfig().maxAggregateQueriesPerQueryType());
            }
            if (currMergedAggregate == null) {
                currMergedAggregate = new MergedAggregate(Math.min(rollupTime, liveCaptureTime),
                        transactionType, transactionName,
                        configService.getAdvancedConfig().maxAggregateQueriesPerQueryType());
            }
            currRollupTime = rollupTime;
            currMergedAggregate.addTotalMicros(nonRolledUpAggregate.totalMicros());
            currMergedAggregate.addErrorCount(nonRolledUpAggregate.errorCount());
            currMergedAggregate.addTransactionCount(nonRolledUpAggregate.transactionCount());
            currMergedAggregate.addTotalCpuMicros(nonRolledUpAggregate.totalCpuMicros());
            currMergedAggregate.addTotalBlockedMicros(nonRolledUpAggregate.totalBlockedMicros());
            currMergedAggregate.addTotalWaitedMicros(nonRolledUpAggregate.totalWaitedMicros());
            currMergedAggregate
                    .addTotalAllocatedKBytes(nonRolledUpAggregate.totalAllocatedKBytes());
            currMergedAggregate.addTimers(nonRolledUpAggregate.timers());
            currMergedAggregate.addHistogram(nonRolledUpAggregate.histogram());
        }
        if (currMergedAggregate != null) {
            // roll up final one
            rolledUpAggregates.add(currMergedAggregate.toAggregate(scratchBuffer));
        }
        return rolledUpAggregates;
    }

    private static long getRevisedTo(long to,
            List orderedIntervalCollectors) {
        if (orderedIntervalCollectors.isEmpty()) {
            return to;
        } else {
            // -1 since query 'to' is inclusive
            // this way don't need to worry about de-dupping between live and stored aggregates
            return orderedIntervalCollectors.get(0).getEndTime() - 1;
        }
    }

    private TransactionSummary mergeInLiveOverallSummaries(String transactionType,
            TransactionSummary overallSummary,
            List intervalCollectors) {
        for (AggregateIntervalCollector intervalCollector : intervalCollectors) {
            TransactionSummary liveOverallSummary =
                    intervalCollector.getLiveOverallSummary(transactionType);
            if (liveOverallSummary != null) {
                overallSummary =
                        combineTransactionSummaries(null, overallSummary, liveOverallSummary);
            }
        }
        return overallSummary;
    }

    private static QueryResult mergeInLiveTransactionSummaries(
            TransactionSummaryQuery query, QueryResult queryResult,
            List intervalCollectors) {
        List transactionSummaries = queryResult.records();
        Map transactionSummaryMap = Maps.newHashMap();
        for (TransactionSummary transactionSummary : transactionSummaries) {
            String transactionName = transactionSummary.transactionName();
            // transaction name is only null for overall summary
            checkNotNull(transactionName);
            transactionSummaryMap.put(transactionName, transactionSummary);
        }
        for (AggregateIntervalCollector intervalCollector : intervalCollectors) {
            List liveTransactionSummaries =
                    intervalCollector.getLiveTransactionSummaries(query.transactionType());
            for (TransactionSummary liveTransactionSummary : liveTransactionSummaries) {
                String transactionName = liveTransactionSummary.transactionName();
                // transaction name is only null for overall summary
                checkNotNull(transactionName);
                TransactionSummary transactionSummary = transactionSummaryMap.get(transactionName);
                if (transactionSummary == null) {
                    transactionSummaryMap.put(transactionName, liveTransactionSummary);
                } else {
                    transactionSummaryMap.put(transactionName, combineTransactionSummaries(
                            transactionName, transactionSummary, liveTransactionSummary));
                }
            }
        }
        transactionSummaries =
                sortTransactionSummaries(transactionSummaryMap.values(), query.sortOrder());
        boolean moreAvailable = queryResult.moreAvailable();
        if (transactionSummaries.size() > query.limit()) {
            moreAvailable = true;
            transactionSummaries = transactionSummaries.subList(0, query.limit());
        }
        return new QueryResult(transactionSummaries, moreAvailable);
    }

    private static List getLiveAggregates(String transactionType,
            @Nullable String transactionName, List intervalCollectors,
            long liveCaptureTime) throws IOException {
        List aggregates = Lists.newArrayList();
        for (AggregateIntervalCollector intervalCollector : intervalCollectors) {
            Aggregate liveAggregate = intervalCollector.getLiveAggregate(transactionType,
                    transactionName, liveCaptureTime);
            if (liveAggregate != null) {
                aggregates.add(liveAggregate);
            }
        }
        return aggregates;
    }

    private static List getLiveQueryAggregates(String transactionType,
            @Nullable String transactionName, List intervalCollectors)
                    throws IOException {
        List queryAggregates = Lists.newArrayList();
        for (AggregateIntervalCollector intervalCollector : intervalCollectors) {
            QueryAggregate liveQueryAggregate =
                    intervalCollector.getLiveQueryAggregate(transactionType, transactionName);
            if (liveQueryAggregate != null) {
                queryAggregates.add(liveQueryAggregate);
            }
        }
        return queryAggregates;
    }

    private static List getLiveProfileAggregates(String transactionType,
            @Nullable String transactionName, List intervalCollectors)
                    throws IOException {
        List profileAggregates = Lists.newArrayList();
        for (AggregateIntervalCollector intervalCollector : intervalCollectors) {
            ProfileAggregate liveProfileAggregate =
                    intervalCollector.getLiveProfileAggregate(transactionType, transactionName);
            if (liveProfileAggregate != null) {
                profileAggregates.add(liveProfileAggregate);
            }
        }
        return profileAggregates;
    }

    private static TransactionSummary combineTransactionSummaries(@Nullable String transactionName,
            TransactionSummary summary1, TransactionSummary summary2) {
        return TransactionSummary.builder()
                .transactionName(transactionName)
                .totalMicros(summary1.totalMicros() + summary2.totalMicros())
                .transactionCount(summary1.transactionCount() + summary2.transactionCount())
                .build();
    }

    // using non-recursive algorithm to avoid stack overflow error on deep profiles
    private static void filter(ProfileNode syntheticRootNode, List includes,
            List excludes) {
        for (String include : includes) {
            if (syntheticRootNode.isMatched()) {
                new ProfileResetMatches(syntheticRootNode).traverse();
            }
            new ProfileFilterer(syntheticRootNode, include, false).traverse();
        }
        for (String exclude : excludes) {
            // reset is only needed prior to first exclusion, since exclusions won't leave behind
            // any matched nodes
            if (syntheticRootNode.isMatched()) {
                new ProfileResetMatches(syntheticRootNode).traverse();
            }
            new ProfileFilterer(syntheticRootNode, exclude, true).traverse();
        }
    }

    // using non-recursive algorithm to avoid stack overflow error on deep profiles
    private static void truncateLeafs(Iterable rootNodes, int minSamples) {
        Deque toBeVisited = new ArrayDeque();
        for (ProfileNode rootNode : rootNodes) {
            toBeVisited.add(rootNode);
        }
        ProfileNode node;
        while ((node = toBeVisited.poll()) != null) {
            for (Iterator i = node.getChildNodes().iterator(); i.hasNext();) {
                ProfileNode childNode = i.next();
                if (childNode.getSampleCount() < 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.incrementEllipsedSampleCount((int) childNode.getSampleCount());
                } else {
                    toBeVisited.add(childNode);
                }
            }
        }
    }

    private static List sortTransactionSummaries(
            Iterable transactionSummaries,
            TransactionSummarySortOrder sortOrder) {
        switch (sortOrder) {
            case TOTAL_TIME:
                return TransactionSummary.orderingByTotalTimeDesc
                        .immutableSortedCopy(transactionSummaries);
            case AVERAGE_TIME:
                return TransactionSummary.orderingByAverageTimeDesc
                        .immutableSortedCopy(transactionSummaries);
            case THROUGHPUT:
                return TransactionSummary.orderingByTransactionCountDesc
                        .immutableSortedCopy(transactionSummaries);
            default:
                throw new AssertionError("Unexpected sort order: " + sortOrder);
        }
    }

    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) {
            if (isMatch(node)) {
                node.setMatched();
                // no need to visit children
                return ImmutableList.of();
            }
            return ImmutableList.copyOf(node.getChildNodes());
        }

        @Override
        public void revisitAfterChildren(ProfileNode node) {
            if (node.isMatched()) {
                // if exclusion then node will be removed by parent
                // if not exclusion then keep node and all children
                return;
            }
            if (node.isChildNodesEmpty()) {
                return;
            }
            if (removeNode(node)) {
                // node will be removed by parent
                if (exclusion) {
                    node.setMatched();
                }
                return;
            }
            if (!exclusion) {
                node.setMatched();
            }
            // node is a partial match, need to filter it out
            long filteredSampleCount = 0;
            for (Iterator i = node.iterator(); i.hasNext();) {
                ProfileNode childNode = i.next();
                if (exclusion == !childNode.isMatched()) {
                    filteredSampleCount += childNode.getSampleCount();
                } else {
                    i.remove();
                }
            }
            node.setSampleCount(filteredSampleCount);
        }

        private boolean isMatch(ProfileNode node) {
            String stackTraceElementUpper =
                    node.getStackTraceElementStr().toUpperCase(Locale.ENGLISH);
            if (stackTraceElementUpper.contains(filterTextUpper)) {
                return true;
            }
            String leafThreadState = node.getLeafThreadState();
            if (leafThreadState != null) {
                String leafThreadStateUpper = leafThreadState.toUpperCase(Locale.ENGLISH);
                if (leafThreadStateUpper.contains(filterTextUpper)) {
                    return true;
                }
            }
            return false;
        }

        private boolean removeNode(ProfileNode node) {
            if (node.isSyntheticRootNode()) {
                return false;
            }
            if (exclusion) {
                return hasOnlyMatchedChildren(node);
            } else {
                return hasNoMatchedChildren(node);
            }
        }

        private boolean hasOnlyMatchedChildren(ProfileNode node) {
            for (ProfileNode childNode : node) {
                if (!childNode.isMatched()) {
                    return false;
                }
            }
            return true;
        }

        private boolean hasNoMatchedChildren(ProfileNode node) {
            for (ProfileNode childNode : node) {
                if (childNode.isMatched()) {
                    return false;
                }
            }
            return true;
        }
    }

    private static class ProfileResetMatches extends Traverser {

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

        @Override
        public List visit(ProfileNode node) throws RuntimeException {
            node.resetMatched();
            return ImmutableList.copyOf(node.getChildNodes());
        }

        @Override
        public void revisitAfterChildren(ProfileNode node) throws RuntimeException {}
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy