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.general; 34 35 import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER; 36 import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER_UNITY; 37 38 import java.text.MessageFormat; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Map.Entry; 47 import java.util.concurrent.Callable; 48 import java.util.concurrent.FutureTask; 49 import java.util.concurrent.RunnableFuture; 50 import java.util.logging.Level; 51 import java.util.logging.Logger; 52 53 import org.openjdk.jmc.common.IMCType; 54 import org.openjdk.jmc.common.item.Aggregators; 55 import org.openjdk.jmc.common.item.IAttribute; 56 import org.openjdk.jmc.common.item.IItemCollection; 57 import org.openjdk.jmc.common.item.IItemFilter; 58 import org.openjdk.jmc.common.item.IItemQuery; 59 import org.openjdk.jmc.common.item.ItemFilters; 60 import org.openjdk.jmc.common.item.ItemQueryBuilder; 61 import org.openjdk.jmc.common.unit.IQuantity; 62 import org.openjdk.jmc.common.util.IPreferenceValueProvider; 63 import org.openjdk.jmc.common.util.TypedPreference; 64 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; 65 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters; 66 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs; 67 import org.openjdk.jmc.flightrecorder.rules.IRule; 68 import org.openjdk.jmc.flightrecorder.rules.Result; 69 import org.openjdk.jmc.flightrecorder.rules.jdk.messages.internal.Messages; 70 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ClassEntry; 71 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ColumnInfo; 72 import org.openjdk.jmc.flightrecorder.rules.jdk.util.IItemResultSet; 73 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ItemResultSetException; 74 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ItemResultSetFactory; 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 /** 80 * This rule looks at the loaded classes to try to figure out if multiple classes with the same name 81 * has been loaded. Note that this rule can get fairly expensive if you have load events with many 82 * (thousands) of unique classes. 83 */ 84 // FIXME: This rule could perhaps be improved by doing a linear regression of the metaspace usage the higher k, the higher score. 85 public final class ClassLeakingRule implements IRule { 86 private static final String RESULT_ID = "ClassLeak"; //$NON-NLS-1$ 87 private static final String COUNT_AGGREGATOR_ID = "count"; //$NON-NLS-1$ 88 89 public static final TypedPreference<IQuantity> WARNING_LIMIT = new TypedPreference<>("classLeaking.warning.limit", //$NON-NLS-1$ 90 Messages.getString(Messages.ClassLeakingRule_CONFIG_WARNING_LIMIT), 91 Messages.getString(Messages.ClassLeakingRule_CONFIG_WARNING_LIMIT_LONG), NUMBER, NUMBER_UNITY.quantity(25)); 92 93 public static final TypedPreference<IQuantity> MAX_NUMBER_OF_CLASSES_TO_REPORT = new TypedPreference<>( 94 "classLeaking.classesToReport.limit", //$NON-NLS-1$ 95 Messages.getString(Messages.General_CONFIG_CLASS_LIMIT), 96 Messages.getString(Messages.General_CONFIG_CLASS_LIMIT_LONG), NUMBER, NUMBER_UNITY.quantity(5)); 97 98 private static final List<TypedPreference<?>> CONFIG_ATTRIBUTES = Arrays.<TypedPreference<?>> asList(WARNING_LIMIT, 99 MAX_NUMBER_OF_CLASSES_TO_REPORT); 100 101 private Result getResult(IItemCollection items, IPreferenceValueProvider valueProvider) { 102 EventAvailability eventAvailability = RulesToolkit.getEventAvailability(items, JdkTypeIDs.CLASS_LOAD, 103 JdkTypeIDs.CLASS_UNLOAD); 104 if (eventAvailability == EventAvailability.UNKNOWN || eventAvailability == EventAvailability.DISABLED) { 105 return RulesToolkit.getEventAvailabilityResult(this, items, eventAvailability, JdkTypeIDs.CLASS_LOAD, 106 JdkTypeIDs.CLASS_UNLOAD); 107 } 108 int warningLimit = (int) valueProvider.getPreferenceValue(WARNING_LIMIT).longValue(); 109 110 ItemQueryBuilder queryLoad = ItemQueryBuilder.fromWhere(JdkFilters.CLASS_LOAD); 111 queryLoad.groupBy(JdkAttributes.CLASS_LOADED); 112 queryLoad.select(JdkAttributes.CLASS_LOADED); 113 queryLoad.select(Aggregators.count(COUNT_AGGREGATOR_ID, "classesLoaded")); //$NON-NLS-1$ 114 Map<String, ClassEntry> entriesLoad = extractClassEntriesFromQuery(items, queryLoad.build()); 115 116 ItemQueryBuilder queryUnload = ItemQueryBuilder.fromWhere(ItemFilters.and(JdkFilters.CLASS_UNLOAD, 117 createClassAttributeFilter(JdkAttributes.CLASS_UNLOADED, entriesLoad))); 118 queryUnload.groupBy(JdkAttributes.CLASS_UNLOADED); 119 queryUnload.select(JdkAttributes.CLASS_UNLOADED); 120 queryUnload.select(Aggregators.count(COUNT_AGGREGATOR_ID, "classesUnloaded")); //$NON-NLS-1$ 121 Map<String, ClassEntry> entriesUnload = extractClassEntriesFromQuery(items, queryUnload.build()); 122 Map<String, ClassEntry> diff = diff(entriesLoad, entriesUnload); 123 List<ClassEntry> entries = new ArrayList<>(diff.values()); 124 125 if (entries.size() > 0) { 126 StringBuilder longText = new StringBuilder(); 127 int classLimit = Math.min( 128 (int) valueProvider.getPreferenceValue(MAX_NUMBER_OF_CLASSES_TO_REPORT).longValue(), 129 entries.size()); 130 longText.append(MessageFormat.format(Messages.getString(Messages.ClassLeakingRule_TEXT_WARN_LONG), 131 String.valueOf(classLimit))); 132 133 int maxCount = 0; 134 Collections.sort(entries); 135 longText.append("<p><ul>"); //$NON-NLS-1$ 136 for (int i = 0; i < classLimit; i++) { 137 ClassEntry entry = entries.get(i); 138 longText.append("<li>"); //$NON-NLS-1$ 139 longText.append(entry); 140 longText.append("</li>"); //$NON-NLS-1$ 141 maxCount = Math.max(entry.getCount(), maxCount); 142 } 143 longText.append("</ul></p>"); //$NON-NLS-1$ 144 double maxScore = RulesToolkit.mapExp100(maxCount, warningLimit) * 0.75; 145 ClassEntry worst = entries.get(0); 146 return new Result(this, maxScore, 147 MessageFormat.format(Messages.getString(Messages.ClassLeakingRule_TEXT_WARN), 148 worst.getType().getFullName(), worst.getCount()), 149 longText.toString()); 150 } 151 return new Result(this, 0, Messages.getString(Messages.ClassLeakingRule_TEXT_OK)); 152 } 153 154 private static IItemFilter createClassAttributeFilter( 155 IAttribute<IMCType> attribute, Map<String, ClassEntry> entries) { 156 List<IItemFilter> allowedClasses = new ArrayList<>(); 157 for (ClassEntry entry : entries.values()) { 158 allowedClasses.add(ItemFilters.equals(attribute, entry.getType())); 159 } 160 return ItemFilters.or(allowedClasses.toArray(new IItemFilter[0])); 161 } 162 163 private Map<String, ClassEntry> diff(Map<String, ClassEntry> entriesLoad, Map<String, ClassEntry> entriesUnload) { 164 // Found no corresponding unloads, so short cutting this... 165 if (entriesUnload.isEmpty()) { 166 return entriesLoad; 167 } 168 Map<String, ClassEntry> diffMap = new HashMap<>(entriesLoad.size()); 169 for (Entry<String, ClassEntry> mapEntryLoad : entriesLoad.entrySet()) { 170 ClassEntry classEntryUnload = entriesUnload.get(mapEntryLoad.getKey()); 171 if (classEntryUnload != null) { 172 diffMap.put(mapEntryLoad.getKey(), new ClassEntry(mapEntryLoad.getValue().getType(), 173 mapEntryLoad.getValue().getCount() - classEntryUnload.getCount())); 174 } else { 175 diffMap.put(mapEntryLoad.getKey(), mapEntryLoad.getValue()); 176 } 177 } 178 return diffMap; 179 } 180 181 private Map<String, ClassEntry> extractClassEntriesFromQuery(IItemCollection items, IItemQuery query) { 182 Map<String, ClassEntry> entries = new HashMap<>(); 183 IItemResultSet resultSet = new ItemResultSetFactory().createResultSet(items, query); 184 ColumnInfo countColumn = resultSet.getColumnMetadata().get(COUNT_AGGREGATOR_ID); // $NON-NLS-1$ 185 ColumnInfo classColumn = resultSet.getColumnMetadata().get(query.getGroupBy().getIdentifier()); 186 187 while (resultSet.next()) { 188 IQuantity countObject; 189 try { 190 countObject = (IQuantity) resultSet.getValue(countColumn.getColumn()); 191 if (countObject != null) { 192 int count = (int) countObject.longValue(); 193 IMCType type = (IMCType) resultSet.getValue(classColumn.getColumn()); 194 if (type != null) { 195 ClassEntry entry = new ClassEntry(type, count); 196 entries.put(entry.getType().getFullName(), entry); 197 } 198 } 199 } catch (ItemResultSetException e) { 200 Logger.getLogger(getClass().getName()).log(Level.WARNING, "Failed to extract class entries from query!", //$NON-NLS-1$ 201 e); 202 } 203 } 204 return entries; 205 } 206 207 @Override 208 public RunnableFuture<Result> evaluate(final IItemCollection items, final IPreferenceValueProvider valueProvider) { 209 FutureTask<Result> evaluationTask = new FutureTask<>(new Callable<Result>() { 210 @Override 211 public Result call() throws Exception { 212 return getResult(items, valueProvider); 213 } 214 }); 215 return evaluationTask; 216 } 217 218 @Override 219 public Collection<TypedPreference<?>> getConfigurationAttributes() { 220 return CONFIG_ATTRIBUTES; 221 } 222 223 @Override 224 public String getId() { 225 return RESULT_ID; 226 } 227 228 @Override 229 public String getName() { 230 return Messages.getString(Messages.ClassLeakingRule_NAME); 231 } 232 233 @Override 234 public String getTopic() { 235 return JfrRuleTopics.CLASS_LOADING_TOPIC; 236 } 237 238 }