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 }