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.latency;
  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.HashSet;
  45 import java.util.List;
  46 import java.util.Map;
  47 import java.util.Map.Entry;
  48 import java.util.Set;
  49 import java.util.concurrent.Callable;
  50 import java.util.concurrent.FutureTask;
  51 import java.util.concurrent.RunnableFuture;
  52 import java.util.logging.Level;
  53 import java.util.logging.Logger;
  54 
  55 import org.openjdk.jmc.common.IMCType;
  56 import org.openjdk.jmc.common.item.Aggregators;
  57 import org.openjdk.jmc.common.item.IAttribute;
  58 import org.openjdk.jmc.common.item.IItemCollection;
  59 import org.openjdk.jmc.common.item.IItemFilter;
  60 import org.openjdk.jmc.common.item.IItemQuery;
  61 import org.openjdk.jmc.common.item.ItemFilters;
  62 import org.openjdk.jmc.common.item.ItemQueryBuilder;
  63 import org.openjdk.jmc.common.unit.IQuantity;
  64 import org.openjdk.jmc.common.unit.UnitLookup;
  65 import org.openjdk.jmc.common.util.IPreferenceValueProvider;
  66 import org.openjdk.jmc.common.util.TypedPreference;
  67 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  68 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters;
  69 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  70 import org.openjdk.jmc.flightrecorder.rules.IRule;
  71 import org.openjdk.jmc.flightrecorder.rules.Result;
  72 import org.openjdk.jmc.flightrecorder.rules.jdk.messages.internal.Messages;
  73 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ClassEntry;
  74 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ColumnInfo;
  75 import org.openjdk.jmc.flightrecorder.rules.jdk.util.IItemResultSet;
  76 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ItemResultSetException;
  77 import org.openjdk.jmc.flightrecorder.rules.jdk.util.ItemResultSetFactory;
  78 import org.openjdk.jmc.flightrecorder.rules.util.JfrRuleTopics;
  79 import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit;
  80 import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit.EventAvailability;
  81 
  82 /**
  83  * This rule is making use of the new dedicated biased locking revocation events available in JDK
  84  * 10/18.3. It will fire whenever a class is excluded from biased lockings, or whenever there have
  85  * been more than 15 revocations (can be configured) for a particular class.
  86  */
  87 public final class BiasedLockingRevocationRule implements IRule {
  88         public static final TypedPreference<IQuantity> WARNING_LIMIT = new TypedPreference<>(
  89                         "biasedRevocation.warning.limit", //$NON-NLS-1$
  90                         Messages.getString(Messages.BiasedLockingRevocationRule_CONFIG_WARNING_LIMIT),
  91                         Messages.getString(Messages.BiasedLockingRevocationRule_CONFIG_WARNING_LIMIT_LONG), NUMBER,
  92                         NUMBER_UNITY.quantity(15));
  93 
  94         public static final TypedPreference<IQuantity> MAX_NUMBER_OF_CLASSES_TO_REPORT = new TypedPreference<>(
  95                         "biasedRevocation.classesToReport.limit", //$NON-NLS-1$
  96                         Messages.getString(Messages.General_CONFIG_CLASS_LIMIT),
  97                         Messages.getString(Messages.General_CONFIG_CLASS_LIMIT_LONG), NUMBER, NUMBER_UNITY.quantity(5));
  98 
  99         private static final TypedPreference<String> FILTERED_CLASSES = new TypedPreference<>(
 100                         "biasedRevocation.filtered.classes", //$NON-NLS-1$
 101                         Messages.getString(Messages.BiasedLockingRevocationRule_CONFIG_FILTERED_CLASSES),
 102                         Messages.getString(Messages.BiasedLockingRevocationRule_CONFIG_FILTERED_CLASSES_LONG),
 103                         UnitLookup.PLAIN_TEXT.getPersister(), "java.lang.ref.ReferenceQueue$Lock"); //$NON-NLS-1$
 104 
 105         private static final List<TypedPreference<?>> CONFIG_ATTRIBUTES = Arrays.<TypedPreference<?>> asList(WARNING_LIMIT,
 106                         MAX_NUMBER_OF_CLASSES_TO_REPORT, FILTERED_CLASSES);
 107 
 108         private Result getResult(IItemCollection items, IPreferenceValueProvider valueProvider) {
 109                 EventAvailability eventAvailability = RulesToolkit.getEventAvailability(items,
 110                                 JdkTypeIDs.BIASED_LOCK_CLASS_REVOCATION);
 111                 if (eventAvailability == EventAvailability.UNKNOWN || eventAvailability == EventAvailability.DISABLED) {
 112                         return RulesToolkit.getEventAvailabilityResult(this, items, eventAvailability,
 113                                         JdkTypeIDs.BIASED_LOCK_CLASS_REVOCATION);
 114                 }
 115 
 116                 IItemCollection revokationEvents = items.apply(JdkFilters.BIASED_LOCKING_REVOCATIONS); // $NON-NLS-1$
 117                 if (!revokationEvents.hasItems()) {
 118                         return new Result(this, 0, Messages.getString(Messages.BiasedLockingRevocationPauseRule_TEXT_OK));
 119                 }
 120 
 121                 Set<String> filteredTypes = getFilteredTypes(valueProvider.getPreferenceValue(FILTERED_CLASSES));
 122 
 123                 IItemCollection revokedClassesEvents = revokationEvents
 124                                 .apply(ItemFilters.and(ItemFilters.hasAttribute(JdkAttributes.BIASED_REVOCATION_CLASS),
 125                                                 ItemFilters.equals(JdkAttributes.BIASED_REVOCATION_DISABLE_BIASING, Boolean.TRUE)));
 126                 Set<IMCType> revokedTypes = filter(filteredTypes,
 127                                 revokedClassesEvents.getAggregate(Aggregators.distinct(JdkAttributes.BIASED_REVOCATION_CLASS)));
 128 
 129                 StringBuilder shortMessage = new StringBuilder();
 130                 StringBuilder longMessage = new StringBuilder();
 131 
 132                 float totalScore = 0;
 133 
 134                 if (!revokedTypes.isEmpty()) {
 135                         totalScore = 25; // Base penalty for having fully revoked types not filtered out.
 136                         totalScore += RulesToolkit.mapExp(revokedTypes.size(), 25, 7, 20); // Up to 25 more points if you have plenty of revoked types.
 137                         shortMessage.append(Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_REVOKED_CLASSES_FOUND));
 138                         shortMessage.append(" "); //$NON-NLS-1$
 139                         longMessage
 140                                         .append(Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_REVOKED_CLASSES_FOUND_LONG));
 141                         longMessage.append("<p><ul>"); //$NON-NLS-1$
 142                         for (IMCType offender : revokedTypes) {
 143                                 longMessage.append("<li>"); //$NON-NLS-1$
 144                                 longMessage.append(offender.toString());
 145                                 longMessage.append("</li>"); //$NON-NLS-1$
 146                         }
 147                         longMessage.append("</ul></p>"); //$NON-NLS-1$
 148                 }
 149                 int warningLimit = (int) valueProvider.getPreferenceValue(WARNING_LIMIT).longValue();
 150 
 151                 Map<IMCType, ClassEntry> revocationMap = extractRevocations(revokationEvents,
 152                                 ItemFilters.or(ItemFilters.type(JdkTypeIDs.BIASED_LOCK_REVOCATION),
 153                                                 ItemFilters.type(JdkTypeIDs.BIASED_LOCK_SELF_REVOCATION)),
 154                                 JdkAttributes.BIASED_REVOCATION_LOCK_CLASS);
 155                 Map<IMCType, ClassEntry> classRevocationMap = extractRevocations(revokationEvents,
 156                                 ItemFilters.type(JdkTypeIDs.BIASED_LOCK_CLASS_REVOCATION), JdkAttributes.BIASED_REVOCATION_CLASS);
 157 
 158                 List<ClassEntry> revocationClasses = filteredMerge(filteredTypes, revokedTypes, classRevocationMap,
 159                                 revocationMap);
 160                 totalScore += calculateRevocationCountScore(revocationClasses);
 161 
 162                 Collections.sort(revocationClasses);
 163 
 164                 if (revocationClasses.size() > 0) {
 165                         int maxClasses = (int) valueProvider.getPreferenceValue(MAX_NUMBER_OF_CLASSES_TO_REPORT).longValue();
 166                         shortMessage
 167                                         .append(Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_REVOKE_LIMIT_CLASSES_FOUND));
 168                         longMessage.append(MessageFormat.format(
 169                                         Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_REVOKE_LIMIT_CLASSES_FOUND_LONG),
 170                                         warningLimit));
 171                         longMessage.append("<p><ul>"); //$NON-NLS-1$
 172                         int classLimit = Math.min(revocationClasses.size(), maxClasses);
 173                         for (int i = 0; i < classLimit; i++) {
 174                                 ClassEntry classEntry = revocationClasses.get(i);
 175                                 if (classEntry.getCount() < warningLimit) {
 176                                         break;
 177                                 }
 178                                 longMessage.append("<li>"); //$NON-NLS-1$
 179                                 longMessage.append(classEntry);
 180                                 longMessage.append("</li>"); //$NON-NLS-1$
 181                         }
 182                         longMessage.append("</ul></p>"); //$NON-NLS-1$
 183                 }
 184                 if (totalScore == 0) {
 185                         return new Result(this, 0, Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_OK));
 186                 } else {
 187                         longMessage
 188                                         .append(MessageFormat.format(Messages.getString(Messages.BiasedLockingRevocationRule_TEXT_EPILOGUE),
 189                                                         String.valueOf(filteredTypes)));
 190                 }
 191                 return new Result(this, totalScore, shortMessage.toString(), longMessage.toString());
 192         }
 193 
 194         private int calculateRevocationCountScore(List<ClassEntry> offendingClasses) {
 195                 int score = 0;
 196                 for (ClassEntry entry : offendingClasses) {
 197                         // Can get maximum the base score for a full revocation if there are plenty of
 198                         // revocation events for a single class.
 199                         score = Math.max(Math.min(entry.getCount() / 2, 25), score);
 200                 }
 201                 return score;
 202         }
 203 
 204         /**
 205          * @param filteredTypes
 206          *            user filtered types
 207          * @param revokedTypes
 208          *            the types that were revoked during this recording.
 209          * @param offendingClassRevocations
 210          * @param offendingRevocations
 211          * @return
 212          */
 213         private List<ClassEntry> filteredMerge(
 214                 Set<String> filteredTypes, Set<IMCType> revokedTypes, Map<IMCType, ClassEntry> offendingClassRevocations,
 215                 Map<IMCType, ClassEntry> offendingRevocations) {
 216                 Map<IMCType, ClassEntry> merged = new HashMap<>();
 217 
 218                 for (Entry<IMCType, ClassEntry> entry : offendingRevocations.entrySet()) {
 219                         putIfNotInFiltered(filteredTypes, revokedTypes, merged, entry);
 220                 }
 221 
 222                 // Likely far fewer class revocations
 223                 for (Entry<IMCType, ClassEntry> entry : offendingClassRevocations.entrySet()) {
 224                         ClassEntry mergedEntry = merged.get(entry.getKey());
 225                         if (mergedEntry != null) {
 226                                 merged.put(entry.getKey(),
 227                                                 new ClassEntry(entry.getKey(), entry.getValue().getCount() + mergedEntry.getCount()));
 228                         } else {
 229                                 putIfNotInFiltered(filteredTypes, revokedTypes, merged, entry);
 230                         }
 231                 }
 232                 return new ArrayList<>(merged.values());
 233         }
 234 
 235         private static void putIfNotInFiltered(
 236                 Set<String> filteredTypes, Set<IMCType> revokedTypes, Map<IMCType, ClassEntry> merged,
 237                 Entry<IMCType, ClassEntry> entry) {
 238                 IMCType type = entry.getKey();
 239                 if (type != null && !filteredTypes.contains(type.getFullName()) && !revokedTypes.contains(type)) {
 240                         merged.put(entry.getKey(), entry.getValue());
 241                 }
 242         }
 243 
 244         private Map<IMCType, ClassEntry> extractRevocations(
 245                 IItemCollection revokationEvents, IItemFilter filter, IAttribute<IMCType> classAttribute) {
 246                 ItemQueryBuilder itemQueryBuilder = ItemQueryBuilder.fromWhere(filter);
 247                 itemQueryBuilder.groupBy(classAttribute);
 248                 itemQueryBuilder.select(classAttribute);
 249                 itemQueryBuilder.select(Aggregators.count());
 250                 IItemQuery query = itemQueryBuilder.build();
 251 
 252                 IItemResultSet resultSet = new ItemResultSetFactory().createResultSet(revokationEvents, query);
 253                 ColumnInfo countColumn = resultSet.getColumnMetadata().get(Aggregators.count().getName());
 254                 ColumnInfo classColumn = resultSet.getColumnMetadata().get(classAttribute.getIdentifier());
 255 
 256                 Map<IMCType, ClassEntry> offendingClasses = new HashMap<>();
 257                 while (resultSet.next()) {
 258                         try {
 259                                 IQuantity countObject = (IQuantity) resultSet.getValue(countColumn.getColumn());
 260                                 IMCType type = (IMCType) resultSet.getValue(classColumn.getColumn());
 261                                 if (countObject != null && type != null) {
 262                                         offendingClasses.put(type, new ClassEntry(type, (int) countObject.longValue()));
 263 
 264                                 }
 265                         } catch (ItemResultSetException e) {
 266                                 Logger.getLogger(getClass().getName()).log(Level.WARNING,
 267                                                 "Unexpected problem looking at biased revocation events.", e); //$NON-NLS-1$
 268                         }
 269                 }
 270                 return offendingClasses;
 271         }
 272 
 273         private Set<IMCType> filter(Set<String> filteredTypes, Set<IMCType> types) {
 274                 Set<IMCType> result = new HashSet<>();
 275                 for (IMCType type : types) {
 276                         if (!filteredTypes.contains(type.getFullName())) {
 277                                 result.add(type);
 278                         }
 279                 }
 280                 return result;
 281         }
 282 
 283         private static Set<String> getFilteredTypes(String preferenceValue) {
 284                 Set<String> acceptedOptionNames = new HashSet<>();
 285                 if (preferenceValue != null) {
 286                         String[] optionNames = preferenceValue.split("[, ]+"); //$NON-NLS-1$
 287                         for (String optionName : optionNames) {
 288                                 acceptedOptionNames.add(optionName);
 289                         }
 290                 }
 291                 return acceptedOptionNames;
 292         }
 293 
 294         @Override
 295         public RunnableFuture<Result> evaluate(final IItemCollection items, final IPreferenceValueProvider valueProvider) {
 296                 FutureTask<Result> evaluationTask = new FutureTask<>(new Callable<Result>() {
 297                         @Override
 298                         public Result call() throws Exception {
 299                                 return getResult(items, valueProvider);
 300                         }
 301                 });
 302                 return evaluationTask;
 303         }
 304 
 305         @Override
 306         public Collection<TypedPreference<?>> getConfigurationAttributes() {
 307                 return CONFIG_ATTRIBUTES;
 308         }
 309 
 310         @Override
 311         public String getId() {
 312                 return "biasedLockingRevocation"; //$NON-NLS-1$
 313         }
 314 
 315         @Override
 316         public String getName() {
 317                 return Messages.getString(Messages.BiasedLockingRevocationRule_NAME);
 318         }
 319 
 320         @Override
 321         public String getTopic() {
 322                 return JfrRuleTopics.BIASED_LOCKING;
 323         }
 324 
 325 }