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 }