/* * Copyright 2016-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.agent.model; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.hash.Hashing; import com.google.common.primitives.Doubles; import org.immutables.value.Value; import org.glowroot.common.config.StorageConfig; import org.glowroot.common.util.Styles; import org.glowroot.wire.api.model.AggregateOuterClass.Aggregate; public class QueryCollector { private static final Ordering<Entry<String, MutableQuery>> bySmallestTotalDuration = new Ordering<Entry<String, MutableQuery>>() { @Override public int compare(Entry<String, MutableQuery> left, Entry<String, MutableQuery> right) { return Doubles.compare(left.getValue().getTotalDurationNanos(), right.getValue().getTotalDurationNanos()); } }; private static final int REMOVE_SMALLEST_N = 10; // first key is query type, second key is query text private final Map<String, Map<String, MutableQuery>> queries = Maps.newHashMap(); private final int limit; private final int maxMultiplierWhileBuilding; private final boolean traceLevel; private final Map<String, MinQuery> minQueryPerType = Maps.newHashMap(); public QueryCollector(int limit, int maxMultiplierWhileBuilding, boolean traceLevel) { this.limit = limit; this.maxMultiplierWhileBuilding = maxMultiplierWhileBuilding; this.traceLevel = traceLevel; } public List<Aggregate.QueriesByType> toAggregateProto( SharedQueryTextCollector sharedQueryTextCollector) { if (queries.isEmpty()) { return ImmutableList.of(); } List<Aggregate.QueriesByType> proto = Lists.newArrayList(); for (Entry<String, Map<String, MutableQuery>> outerEntry : queries.entrySet()) { List<Aggregate.Query> queries = Lists.newArrayListWithCapacity(outerEntry.getValue().values().size()); for (Entry<String, MutableQuery> entry : outerEntry.getValue().entrySet()) { queries.add(entry.getValue().toAggregateProto(entry.getKey(), sharedQueryTextCollector)); } if (queries.size() > limit) { orderAggregateQueries(queries); queries = queries.subList(0, limit); } proto.add(Aggregate.QueriesByType.newBuilder() .setType(outerEntry.getKey()) .addAllQuery(queries) .build()); } return proto; } public void mergeQuery(String queryType, String queryText, long totalDurationNanos, long executionCount, boolean hasTotalRows, long totalRows) { Map<String, MutableQuery> queriesForType = queries.get(queryType); if (queriesForType == null) { queriesForType = Maps.newHashMap(); queries.put(queryType, queriesForType); } mergeQuery(queryType, queryText, totalDurationNanos, executionCount, totalRows, hasTotalRows, queriesForType); } public void mergeQueriesInto(org.glowroot.common.model.QueryCollector collector) { for (Entry<String, Map<String, MutableQuery>> outerEntry : queries.entrySet()) { for (Entry<String, MutableQuery> entry : outerEntry.getValue().entrySet()) { String fullQueryText = entry.getKey(); String truncatedQueryText; String fullQueryTextSha1; if (fullQueryText.length() > StorageConfig.AGGREGATE_QUERY_TEXT_TRUNCATE) { truncatedQueryText = fullQueryText.substring(0, StorageConfig.AGGREGATE_QUERY_TEXT_TRUNCATE); fullQueryTextSha1 = Hashing.sha1().hashString(fullQueryText, Charsets.UTF_8).toString(); } else { truncatedQueryText = fullQueryText; fullQueryTextSha1 = null; } MutableQuery query = entry.getValue(); collector.mergeQuery(outerEntry.getKey(), truncatedQueryText, fullQueryTextSha1, query.getTotalDurationNanos(), query.getExecutionCount(), query.hasTotalRows(), query.getTotalRows()); } } } public @Nullable String getFullQueryText(String fullQueryTextSha1) { for (Entry<String, Map<String, MutableQuery>> entry : queries.entrySet()) { for (String fullQueryText : entry.getValue().keySet()) { if (fullQueryText.length() <= StorageConfig.AGGREGATE_QUERY_TEXT_TRUNCATE) { continue; } String sha1 = Hashing.sha1().hashString(fullQueryText, Charsets.UTF_8).toString(); if (fullQueryTextSha1.equals(sha1)) { return fullQueryText; } } } return null; } private void mergeQuery(String queryType, String queryText, long totalDurationNanos, long executionCount, long totalRows, boolean hasTotalRows, Map<String, MutableQuery> queriesForType) { MutableQuery aggregateQuery = queriesForType.get(queryText); boolean truncateAndRecalculateMinQuery = false; if (aggregateQuery == null) { if (maxMultiplierWhileBuilding != 0 && queriesForType.size() >= limit * maxMultiplierWhileBuilding) { MinQuery minQuery = minQueryPerType.get(queryType); if (minQuery != null && totalDurationNanos < minQuery.totalDurationNanos()) { return; } truncateAndRecalculateMinQuery = true; } aggregateQuery = new MutableQuery(traceLevel); queriesForType.put(queryText, aggregateQuery); } aggregateQuery.addToTotalDurationNanos(totalDurationNanos); aggregateQuery.addToExecutionCount(executionCount); aggregateQuery.addToTotalRows(hasTotalRows, totalRows); if (truncateAndRecalculateMinQuery) { // TODO report checker framework issue that occurs without this suppression @SuppressWarnings("assignment.type.incompatible") List<Entry<String, MutableQuery>> sortedEntries = bySmallestTotalDuration.sortedCopy(queriesForType.entrySet()); // remove smallest N (instead of just smallest 1) to avoid re-sort again so quickly for (int i = 0; i < REMOVE_SMALLEST_N; i++) { queriesForType.remove(sortedEntries.get(i).getKey()); } MutableQuery lastQuery = sortedEntries.get(REMOVE_SMALLEST_N).getValue(); minQueryPerType.put(queryType, ImmutableMinQuery.of(lastQuery, lastQuery.getTotalDurationNanos())); } } private static void orderAggregateQueries(List<Aggregate.Query> queries) { // reverse sort by total Collections.sort(queries, new Comparator<Aggregate.Query>() { @Override public int compare(Aggregate.Query left, Aggregate.Query right) { return Doubles.compare(right.getTotalDurationNanos(), left.getTotalDurationNanos()); } }); } @Value.Immutable @Styles.AllParameters interface MinQuery { MutableQuery query(); double totalDurationNanos(); } public static class SharedQueryTextCollector { private final Map<String, Integer> sharedQueryTextIndexes = Maps.newHashMap(); private List<String> latestSharedQueryTexts = Lists.newArrayList(); public List<String> getAndClearLastestSharedQueryTexts() { List<String> latestSharedQueryTexts = this.latestSharedQueryTexts; this.latestSharedQueryTexts = Lists.newArrayList(); return latestSharedQueryTexts; } int getIndex(String queryText) { Integer sharedQueryTextIndex = sharedQueryTextIndexes.get(queryText); if (sharedQueryTextIndex == null) { sharedQueryTextIndex = sharedQueryTextIndexes.size(); sharedQueryTextIndexes.put(queryText, sharedQueryTextIndex); latestSharedQueryTexts.add(queryText); } return sharedQueryTextIndex; } } }