1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.flightrecorder.rules.jdk.memory;
  34 
  35 import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER;
  36 import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER_UNITY;
  37 import static org.openjdk.jmc.common.unit.UnitLookup.PERCENT;
  38 import static org.openjdk.jmc.common.unit.UnitLookup.PERCENTAGE;
  39 import static org.openjdk.jmc.common.unit.UnitLookup.PERCENT_UNITY;
  40 
  41 import java.text.MessageFormat;
  42 import java.util.Arrays;
  43 import java.util.Collection;
  44 import java.util.Iterator;
  45 import java.util.List;
  46 import java.util.concurrent.Callable;
  47 import java.util.concurrent.FutureTask;
  48 import java.util.concurrent.RunnableFuture;
  49 
  50 import org.openjdk.jmc.common.IDisplayable;
  51 import org.openjdk.jmc.common.IMCType;
  52 import org.openjdk.jmc.common.collection.MapToolkit.IntEntry;
  53 import org.openjdk.jmc.common.item.Aggregators;
  54 import org.openjdk.jmc.common.item.IItem;
  55 import org.openjdk.jmc.common.item.IItemCollection;
  56 import org.openjdk.jmc.common.item.IItemIterable;
  57 import org.openjdk.jmc.common.item.IMemberAccessor;
  58 import org.openjdk.jmc.common.item.ItemFilters;
  59 import org.openjdk.jmc.common.unit.BinaryPrefix;
  60 import org.openjdk.jmc.common.unit.IQuantity;
  61 import org.openjdk.jmc.common.unit.UnitLookup;
  62 import org.openjdk.jmc.common.util.IPreferenceValueProvider;
  63 import org.openjdk.jmc.common.util.TypedPreference;
  64 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  65 import org.openjdk.jmc.flightrecorder.jdk.JdkAggregators;
  66 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  67 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters;
  68 import org.openjdk.jmc.flightrecorder.jdk.JdkQueries;
  69 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  70 import org.openjdk.jmc.flightrecorder.memleak.ReferenceTreeModel;
  71 import org.openjdk.jmc.flightrecorder.memleak.ReferenceTreeObject;
  72 import org.openjdk.jmc.flightrecorder.rules.IRule;
  73 import org.openjdk.jmc.flightrecorder.rules.Result;
  74 import org.openjdk.jmc.flightrecorder.rules.jdk.messages.internal.Messages;
  75 import org.openjdk.jmc.flightrecorder.rules.util.JfrRuleTopics;
  76 import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit;
  77 import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit.EventAvailability;
  78 
  79 public class IncreasingLiveSetRule implements IRule {
  80 
  81         /**
  82          * Defines the relative amount of live set increase per second that corresponds to a rule score
  83          * of 75.
  84          */
  85         private static final double PERCENT_OF_HEAP_INCREASE_PER_SECOND = 0.01;
  86 
  87         private static final String RESULT_ID = "IncreasingLiveSet"; //$NON-NLS-1$
  88 
  89         public static final TypedPreference<IQuantity> CLASSES_LOADED_PERCENT = new TypedPreference<>(
  90                         "memleak.classload.percent", Messages.getString(Messages.IncreasingLiveSetRule_LOADED_CLASSES_PERCENT), //$NON-NLS-1$
  91                         Messages.getString(Messages.IncreasingLiveSetRule_LOADED_CLASSES_PERCENT_DESC), PERCENTAGE,
  92                         PERCENT.quantity(90));
  93         public static final TypedPreference<IQuantity> RELEVANCE_THRESHOLD = new TypedPreference<>(
  94                         "memleak.reference.tree.depth", Messages.getString(Messages.IncreasingLiveSetRule_RELEVANCE_THRESHOLD), //$NON-NLS-1$
  95                         Messages.getString(Messages.IncreasingLiveSetRule_RELEVANCE_THRESHOLD_DESC), NUMBER,
  96                         NUMBER_UNITY.quantity(0.5d));
  97         private static final List<TypedPreference<?>> CONFIG_ATTRIBUTES = Arrays
  98                         .<TypedPreference<?>> asList(CLASSES_LOADED_PERCENT, RELEVANCE_THRESHOLD);
  99 
 100         private Result getResult(IItemCollection items, IPreferenceValueProvider valueProvider) {
 101                 EventAvailability eventAvailability = RulesToolkit.getEventAvailability(items, JdkTypeIDs.HEAP_SUMMARY);
 102                 if (eventAvailability == EventAvailability.UNAVAILABLE || eventAvailability == EventAvailability.DISABLED) {
 103                         return RulesToolkit.getEventAvailabilityResult(this, items, eventAvailability, JdkTypeIDs.HEAP_SUMMARY);
 104                 }
 105 
 106                 IQuantity postWarmupTime = getPostWarmupTime(items, valueProvider.getPreferenceValue(CLASSES_LOADED_PERCENT));
 107                 Iterator<? extends IItemIterable> allAfterItems = items.apply(JdkFilters.HEAP_SUMMARY_AFTER_GC).iterator();
 108                 double score = 0;
 109                 IQuantity liveSetIncreasePerSecond = UnitLookup.MEMORY.getUnit(BinaryPrefix.MEBI).quantity(0);
 110                 if (allAfterItems.hasNext()) {
 111                         // FIXME: Handle multiple IItemIterable
 112                         IItemIterable afterItems = allAfterItems.next();
 113                         IMemberAccessor<IQuantity, IItem> timeAccessor = JfrAttributes.END_TIME.getAccessor(afterItems.getType());
 114                         IMemberAccessor<IQuantity, IItem> memAccessor = JdkAttributes.HEAP_USED.getAccessor(afterItems.getType());
 115 
 116                         liveSetIncreasePerSecond = UnitLookup.MEMORY.getUnit(BinaryPrefix.MEBI)
 117                                         .quantity(RulesToolkit.leastSquareMemory(afterItems.iterator(), timeAccessor, memAccessor));
 118 
 119                         if (postWarmupTime == null) {
 120                                 return RulesToolkit.getTooFewEventsResult(this);
 121                         }
 122                         IQuantity postWarmupHeapSize = items
 123                                         .apply(ItemFilters.and(JdkFilters.HEAP_SUMMARY_AFTER_GC,
 124                                                         ItemFilters.moreOrEqual(JfrAttributes.START_TIME, postWarmupTime)))
 125                                         .getAggregate(JdkAggregators.first(JdkAttributes.HEAP_USED));
 126                         if (postWarmupHeapSize == null) {
 127                                 return RulesToolkit.getTooFewEventsResult(this);
 128                         }
 129                         double relativeIncreasePerSecond = liveSetIncreasePerSecond.ratioTo(postWarmupHeapSize);
 130                         score = RulesToolkit.mapExp100(relativeIncreasePerSecond, PERCENT_OF_HEAP_INCREASE_PER_SECOND);
 131                 }
 132                 // If we have Old Object Sample events we can attempt to find suitable memory leak class candidates
 133                 // otherwise we just return the basic increasing live set score
 134                 EventAvailability ea = RulesToolkit.getEventAvailability(items, JdkTypeIDs.OLD_OBJECT_SAMPLE);
 135                 // FIXME: Should construct an message using memoryIncrease, not use a hard limit
 136                 if (score >= 25 && (ea == EventAvailability.DISABLED || ea == EventAvailability.UNAVAILABLE)) {
 137                         String shortMessage = MessageFormat.format(
 138                                         Messages.getString(Messages.IncreasingLiveSetRuleFactory_TEXT_INFO),
 139                                         liveSetIncreasePerSecond.displayUsing(IDisplayable.AUTO));
 140                         String longMessage = shortMessage + "<p>" //$NON-NLS-1$
 141                                         + Messages.getString(Messages.IncreasingLiveSetRuleFactory_TEXT_INFO_LONG);
 142                         return new Result(this, score, shortMessage, longMessage, JdkQueries.HEAP_SUMMARY_AFTER_GC);
 143                 } else if (score < 25) {
 144                         return new Result(this, score, Messages.getString(Messages.IncreasingLiveSetRule_TEXT_OK));
 145                 }
 146 
 147                 // step 1. extract events from after the estimated warmup period
 148                 IItemCollection oldObjectItems = items.apply(ItemFilters.and(ItemFilters.type(JdkTypeIDs.OLD_OBJECT_SAMPLE),
 149                                 ItemFilters.more(JfrAttributes.START_TIME, postWarmupTime)));
 150 
 151                 ReferenceTreeModel tree = ReferenceTreeModel.buildReferenceTree(oldObjectItems);
 152 
 153                 // step 2. perform a balance calculation on the old object sample events aggregated by class count
 154                 boolean anyReferrerChains = false;
 155                 for (ReferenceTreeObject referenceTreeObject : tree.getLeakObjects()) {
 156                         if (referenceTreeObject.getParent() != null) {
 157                                 anyReferrerChains = true;
 158                                 break;
 159                         }
 160                 }
 161                 if (!anyReferrerChains) {
 162                         List<IntEntry<IMCType>> calculateGroupingScore = RulesToolkit.calculateGroupingScore(oldObjectItems,
 163                                         JdkAttributes.OLD_OBJECT_CLASS);
 164                         double calculateBalanceScore = RulesToolkit.calculateBalanceScore(calculateGroupingScore);
 165                         String shortDescription = MessageFormat.format(Messages.IncreasingLiveSetRuleFactory_TEXT_INFO,
 166                                         liveSetIncreasePerSecond.displayUsing(IDisplayable.AUTO))
 167                                         + (calculateBalanceScore >= 25
 168                                                         ? Messages.getString(Messages.IncreasingLiveSetRule_TEXT_INFO_UNBALANCED)
 169                                                         : Messages.getString(Messages.IncreasingLiveSetRule_TEXT_INFO_BALANCED));
 170                         return new Result(this, Math.min(calculateBalanceScore, 25), // because we already know that there is a leak.
 171                                         shortDescription, Messages.getString(Messages.IncreasingLiveSetRule_TEXT_INFO_LONG));
 172                 }
 173 
 174                 List<ReferenceTreeObject> leakCandidates = tree.getLeakCandidates(
 175                                 valueProvider.getPreferenceValue(RELEVANCE_THRESHOLD).doubleValueIn(UnitLookup.NUMBER_UNITY));
 176                 if (leakCandidates.size() > 0) {
 177                         StringBuilder descriptionBuilder = new StringBuilder();
 178                         descriptionBuilder
 179                                         .append(MessageFormat.format(Messages.getString(Messages.IncreasingLiveSetRuleFactory_TEXT_INFO),
 180                                                         liveSetIncreasePerSecond.displayUsing(IDisplayable.AUTO)));
 181                         descriptionBuilder.append("<br/>"); //$NON-NLS-1$
 182                         descriptionBuilder.append(MessageFormat
 183                                         .format(Messages.getString(Messages.IncreasingLiveSetRule_LEAK_CANDIDATES), leakCandidates.size()));
 184                         descriptionBuilder.append("<ul>"); //$NON-NLS-1$
 185                         int objectFormat = ReferenceTreeObject.FORMAT_PACKAGE | ReferenceTreeObject.FORMAT_FIELD
 186                                         | ReferenceTreeObject.FORMAT_ARRAY_INFO;
 187                         for (ReferenceTreeObject candidate : leakCandidates) {
 188                                 descriptionBuilder.append("<li>"); //$NON-NLS-1$
 189                                 descriptionBuilder.append(candidate.toString(objectFormat));
 190                                 descriptionBuilder.append("<br/>"); //$NON-NLS-1$
 191                                 descriptionBuilder.append(Messages.getString(Messages.IncreasingLiveSetRule_CANDIDATE_REFERRED_BY));
 192                                 descriptionBuilder.append("<ul>"); //$NON-NLS-1$
 193                                 ReferenceTreeObject chainObject = candidate.getParent();
 194                                 for (int i = 0; i < 10 && chainObject != null; i++) {
 195                                         descriptionBuilder.append("<li>"); //$NON-NLS-1$
 196                                         descriptionBuilder.append(chainObject.toString(objectFormat));
 197                                         if (chainObject.getParent() == null) { // aborting the loop because we have found the root
 198                                                 descriptionBuilder.append(" ("); //$NON-NLS-1$
 199                                                 descriptionBuilder.append(chainObject.getRootDescription());
 200                                                 descriptionBuilder.append(")</li>"); //$NON-NLS-1$
 201                                                 break;
 202                                         }
 203                                         descriptionBuilder.append("</li>"); //$NON-NLS-1$
 204                                         chainObject = chainObject.getParent();
 205                                 }
 206                                 if (chainObject != null && chainObject.getParent() != null) { // we never iterated to the object
 207                                         while (chainObject.getParent() != null) {
 208                                                 chainObject = chainObject.getParent();
 209                                         }
 210                                         descriptionBuilder.append("<li>"); //$NON-NLS-1$
 211                                         descriptionBuilder.append(Messages.getString(Messages.IncreasingLiveSetRule_ELLIPSIS));
 212                                         descriptionBuilder.append("</li><li>"); //$NON-NLS-1$
 213                                         descriptionBuilder.append(chainObject.toString(objectFormat));
 214                                         descriptionBuilder.append(" ("); //$NON-NLS-1$
 215                                         descriptionBuilder.append(chainObject.getRootDescription());
 216                                         descriptionBuilder.append(")</li>"); //$NON-NLS-1$
 217                                 }
 218                                 descriptionBuilder.append("</ul>"); //$NON-NLS-1$
 219                                 descriptionBuilder.append("</li>"); //$NON-NLS-1$
 220                         }
 221                         descriptionBuilder.append("</ul>"); //$NON-NLS-1$
 222                         return new Result(this, score, descriptionBuilder.toString());
 223                 }
 224                 String description = ""; //$NON-NLS-1$
 225                 if (score >= 25) {
 226                         description = MessageFormat.format(Messages.getString(Messages.IncreasingLiveSetRuleFactory_TEXT_INFO),
 227                                         liveSetIncreasePerSecond.displayUsing(IDisplayable.AUTO)) + "</br>"; //$NON-NLS-1$
 228                 }
 229                 return new Result(this, score,
 230                                 description + MessageFormat.format(
 231                                                 Messages.getString(Messages.IncreasingLiveSetRule_TEXT_INFO_NO_CANDIDATES),
 232                                                 postWarmupTime.displayUsing(IDisplayable.AUTO)),
 233                                 null, JdkQueries.HEAP_SUMMARY_AFTER_GC);
 234         }
 235 
 236         private IQuantity getPostWarmupTime(IItemCollection items, IQuantity classesLoadedPercent) {
 237                 IItemCollection classLoadItems = items.apply(JdkFilters.CLASS_LOAD_STATISTICS);
 238                 IQuantity maxLoadedClasses = classLoadItems
 239                                 .getAggregate(Aggregators.max(JdkAttributes.CLASSLOADER_LOADED_COUNT));
 240                 if (maxLoadedClasses == null) {
 241                         return null;
 242                 }
 243                 double doubleValue = classesLoadedPercent.doubleValueIn(PERCENT_UNITY);
 244                 IQuantity loadedClassesLimit = maxLoadedClasses.multiply(doubleValue);
 245                 return classLoadItems.apply(ItemFilters.more(JdkAttributes.CLASSLOADER_LOADED_COUNT, loadedClassesLimit))
 246                                 .getAggregate(Aggregators.min(JfrAttributes.START_TIME));
 247         }
 248 
 249         @Override
 250         public RunnableFuture<Result> evaluate(final IItemCollection items, final IPreferenceValueProvider valueProvider) {
 251                 FutureTask<Result> evaluationTask = new FutureTask<>(new Callable<Result>() {
 252                         @Override
 253                         public Result call() throws Exception {
 254                                 return getResult(items, valueProvider);
 255                         }
 256                 });
 257                 return evaluationTask;
 258         }
 259 
 260         @Override
 261         public Collection<TypedPreference<?>> getConfigurationAttributes() {
 262                 return CONFIG_ATTRIBUTES;
 263         }
 264 
 265         @Override
 266         public String getId() {
 267                 return RESULT_ID;
 268         }
 269 
 270         @Override
 271         public String getName() {
 272                 return Messages.getString(Messages.IncreasingLiveSetRuleFactory_RULE_NAME);
 273         }
 274 
 275         @Override
 276         public String getTopic() {
 277                 return JfrRuleTopics.MEMORY_LEAK_TOPIC;
 278         }
 279 }