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.util;
  34 
  35 import java.text.MessageFormat;
  36 import java.util.ArrayList;
  37 import java.util.Arrays;
  38 import java.util.Collection;
  39 import java.util.Collections;
  40 import java.util.Comparator;
  41 import java.util.HashMap;
  42 import java.util.HashSet;
  43 import java.util.Iterator;
  44 import java.util.LinkedHashMap;
  45 import java.util.List;
  46 import java.util.Map;
  47 import java.util.Queue;
  48 import java.util.Set;
  49 import java.util.concurrent.ConcurrentLinkedQueue;
  50 import java.util.concurrent.ExecutionException;
  51 import java.util.concurrent.Future;
  52 import java.util.concurrent.RunnableFuture;
  53 import java.util.regex.Matcher;
  54 import java.util.regex.Pattern;
  55 
  56 import org.openjdk.jmc.common.IDisplayable;
  57 import org.openjdk.jmc.common.IMCThread;
  58 import org.openjdk.jmc.common.IPredicate;
  59 import org.openjdk.jmc.common.collection.EntryHashMap;
  60 import org.openjdk.jmc.common.collection.IteratorToolkit;
  61 import org.openjdk.jmc.common.collection.MapToolkit;
  62 import org.openjdk.jmc.common.collection.MapToolkit.IntEntry;
  63 import org.openjdk.jmc.common.item.Aggregators;
  64 import org.openjdk.jmc.common.item.IAccessorFactory;
  65 import org.openjdk.jmc.common.item.IAttribute;
  66 import org.openjdk.jmc.common.item.IItem;
  67 import org.openjdk.jmc.common.item.IItemCollection;
  68 import org.openjdk.jmc.common.item.IItemFilter;
  69 import org.openjdk.jmc.common.item.IItemIterable;
  70 import org.openjdk.jmc.common.item.IMemberAccessor;
  71 import org.openjdk.jmc.common.item.IType;
  72 import org.openjdk.jmc.common.item.ItemFilters;
  73 import org.openjdk.jmc.common.item.ItemToolkit;
  74 import org.openjdk.jmc.common.unit.BinaryPrefix;
  75 import org.openjdk.jmc.common.unit.IQuantity;
  76 import org.openjdk.jmc.common.unit.LinearUnit;
  77 import org.openjdk.jmc.common.unit.QuantityConversionException;
  78 import org.openjdk.jmc.common.unit.UnitLookup;
  79 import org.openjdk.jmc.common.util.IPreferenceValueProvider;
  80 import org.openjdk.jmc.common.util.LabeledIdentifier;
  81 import org.openjdk.jmc.common.util.PredicateToolkit;
  82 import org.openjdk.jmc.common.util.StringToolkit;
  83 import org.openjdk.jmc.common.version.JavaVersion;
  84 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  85 import org.openjdk.jmc.flightrecorder.jdk.JdkAggregators;
  86 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  87 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters;
  88 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  89 import org.openjdk.jmc.flightrecorder.rules.IRule;
  90 import org.openjdk.jmc.flightrecorder.rules.Result;
  91 import org.openjdk.jmc.flightrecorder.rules.RuleRegistry;
  92 import org.openjdk.jmc.flightrecorder.rules.messages.internal.Messages;
  93 import org.openjdk.jmc.flightrecorder.rules.tree.Range;
  94 import org.openjdk.jmc.flightrecorder.rules.tree.TimeRangeFilter;
  95 import org.openjdk.jmc.flightrecorder.rules.tree.TimeRangeThreadFilter;
  96 import org.openjdk.jmc.flightrecorder.stacktrace.FrameSeparator;
  97 import org.openjdk.jmc.flightrecorder.stacktrace.StacktraceFormatToolkit;
  98 import org.openjdk.jmc.flightrecorder.stacktrace.StacktraceFrame;
  99 import org.openjdk.jmc.flightrecorder.stacktrace.StacktraceModel;
 100 import org.openjdk.jmc.flightrecorder.stacktrace.StacktraceModel.Branch;
 101 
 102 /**
 103  * A collection of useful methods when evaluating rules.
 104  */
 105 // FIXME: Should probably be broken apart into methods related to rules (kept here) and methods related to JDK events (which should be moved to wherever JfrAttributes etc. are)
 106 public class RulesToolkit {
 107 
 108         // FIXME: Quick and dirty inlining of constants constants defined in SettingsTransformer. These should be handled in some other way.
 109         private static final String REC_SETTING_NAME_ENABLED = "enabled"; //$NON-NLS-1$
 110         private static final String REC_SETTING_NAME_THRESHOLD = "threshold"; //$NON-NLS-1$
 111         public static final String REC_SETTING_NAME_PERIOD = "period"; //$NON-NLS-1$
 112         public static final String REC_SETTING_PERIOD_EVERY_CHUNK = "everyChunk"; //$NON-NLS-1$
 113 
 114         private static final IAccessorFactory<String> TYPE_NAME_ACCESSOR_FACTORY = new IAccessorFactory<String>() {
 115 
 116                 @Override
 117                 public <T> IMemberAccessor<String, T> getAccessor(IType<T> type) {
 118                         final IMemberAccessor<LabeledIdentifier, T> ta = JdkAttributes.REC_SETTING_FOR.getAccessor(type);
 119                         return new IMemberAccessor<String, T>() {
 120 
 121                                 @Override
 122                                 public String getMember(T inObject) {
 123                                         LabeledIdentifier eventType = ta.getMember(inObject);
 124                                         return eventType == null ? null : eventType.getName();
 125                                 }
 126 
 127                         };
 128                 }
 129         };
 130         private final static LinearUnit MEBIBYTES = UnitLookup.MEMORY.getUnit(BinaryPrefix.MEBI);
 131 
 132         /**
 133          * Matches strings containing an identifiable version number as presented in a JVM info event.
 134          * The minimal matching form is "JRE (" followed by 1 to 4 numbers on the format a.b.c_d or
 135          * a.b.c.d. Examples are 1.7.0, 1.8.0_70, 9, 9.1, and 9.1.2.3. Match group 1 will contain the
 136          * matched version string.
 137          */
 138         private static final Pattern VERSION_PATTERN = Pattern
 139                         .compile(".*?JRE \\((\\d+(?:\\.\\d+(?:\\.\\d+(?:[\\._]\\d+)?)?)?(?:-ea)?).*"); //$NON-NLS-1$
 140 
 141         /**
 142          * Knowledge about the state of affairs of an event type in an IItemCollection.
 143          */
 144         public enum EventAvailability {
 145                                 /**
 146                                  * The type has events available in the collection.
 147                                  */
 148                                 AVAILABLE(4),
 149                                 /**
 150                                  * The type was actively enabled in the collection.
 151                                  */
 152                                 ENABLED(3),
 153                                 /**
 154                                  * The type was actively disabled in the collection.
 155                                  */
 156                                 DISABLED(2),
 157                                 /**
 158                                  * The type is known in the collection, but no events were found.
 159                                  */
 160                                 NONE(1),
 161                                 /**
 162                                  * The type is unknown in the collection.
 163                                  */
 164                                 UNKNOWN(0);
 165 
 166                 /*
 167                  * Used to determine the ordering of availabilities.
 168                  */
 169                 private final int availabilityScore;
 170 
 171                 EventAvailability(int availabilityScore) {
 172                         this.availabilityScore = availabilityScore;
 173                 }
 174 
 175                 /**
 176                  * Returns true if this EventAvailability is less available than the provided one.
 177                  *
 178                  * @param availability
 179                  *            the {@link EventAvailability} to compare to.
 180                  * @return true if this EventAvailability is less available than the provided one, false
 181                  *         otherwise.
 182                  */
 183                 public boolean isLessAvailableThan(EventAvailability availability) {
 184                         return availabilityScore < availability.availabilityScore;
 185                 }
 186         }
 187 
 188         /**
 189          * @return a least squares approximation of the increase in memory over the given time period,
 190          *         in mebibytes/second
 191          */
 192         public static double leastSquareMemory(
 193                 Iterator<? extends IItem> items, IMemberAccessor<IQuantity, IItem> timeField,
 194                 IMemberAccessor<IQuantity, IItem> memField) {
 195                 double sumX = 0;
 196                 double sumY = 0;
 197                 double sumX2 = 0;
 198                 double sumXY = 0;
 199                 double num = 0;
 200                 double startTime = 0;
 201 
 202                 while (items.hasNext()) {
 203                         IItem item = items.next();
 204                         long time = timeField.getMember(item).clampedLongValueIn(UnitLookup.EPOCH_S);
 205                         long mem = memField.getMember(item).clampedLongValueIn(MEBIBYTES);
 206                         if (num == 0) {
 207                                 startTime = time;
 208                         }
 209                         time -= startTime;
 210                         sumX += time;
 211                         sumY += mem;
 212                         sumX2 += time * time;
 213                         sumXY += time * mem;
 214                         num++;
 215                 }
 216                 double value = (num * sumXY - sumX * sumY) / (num * sumX2 - sumX * sumX);
 217                 return Double.isNaN(value) ? 0 : value;
 218         }
 219 
 220         /**
 221          * Finds items of a specific type where the given attribute has a value matching that of the
 222          * provided match string.
 223          *
 224          * @param typeId
 225          *            the event type to find matches in
 226          * @param items
 227          *            the set of items to search
 228          * @param attribute
 229          *            the attribute to match
 230          * @param match
 231          *            the pattern to find
 232          * @param ignoreCase
 233          *            whether or not to ignore case when matching
 234          * @return a comma-delimited string with all matching attributes
 235          */
 236         public static String findMatches(
 237                 String typeId, IItemCollection items, IAttribute<String> attribute, String match, boolean ignoreCase) {
 238                 String regexp = ".*(" + (ignoreCase ? "?i:" : "") + match + ").*"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
 239                 return items.getAggregate(Aggregators.filter(Aggregators.distinctAsString(typeId, attribute),
 240                                 ItemFilters.and(ItemFilters.type(typeId), ItemFilters.matches(attribute, regexp))));
 241         }
 242 
 243         /**
 244          * Gets the value of a certain attribute for a given item
 245          *
 246          * @param item
 247          *            the item to get the attribute from
 248          * @param attribute
 249          *            the attribute to get
 250          * @return the value of the specified attribute for the given item
 251          */
 252         public static <T> T getValue(IItem item, IAccessorFactory<T> attribute) {
 253                 IType<IItem> itemType = ItemToolkit.getItemType(item);
 254                 IMemberAccessor<? extends T, IItem> accessor = attribute.getAccessor(itemType);
 255                 if (accessor == null) {
 256                         throw new IllegalArgumentException("The accessor factory could not build accessor for type " //$NON-NLS-1$
 257                                         + itemType.getIdentifier()
 258                                         + ". This is likely due to an old unsupported recording where an attribute has changed name."); //$NON-NLS-1$
 259                 }
 260                 return accessor.getMember(item);
 261         }
 262 
 263         /**
 264          * Gets a filter for a specific setting for the provided types.
 265          *
 266          * @param settingsName
 267          *            the specific setting to find
 268          * @param typeIds
 269          *            the ids of the types to find the setting for
 270          * @return a filter for a specified setting for the provided type ids
 271          */
 272         public static IItemFilter getSettingsFilter(String settingsName, String ... typeIds) {
 273                 final Set<String> types = new HashSet<>(Arrays.asList(typeIds));
 274                 IItemFilter typeFilter = new IItemFilter() {
 275                         @Override
 276                         public IPredicate<IItem> getPredicate(IType<IItem> type) {
 277                                 final IMemberAccessor<LabeledIdentifier, IItem> ma = JdkAttributes.REC_SETTING_FOR.getAccessor(type);
 278                                 if (ma != null) {
 279                                         return new IPredicate<IItem>() {
 280 
 281                                                 @Override
 282                                                 public boolean evaluate(IItem o) {
 283                                                         LabeledIdentifier eventType = ma.getMember(o);
 284                                                         return eventType != null && types.contains(eventType.getInterfaceId());
 285                                                 }
 286 
 287                                         };
 288                                 }
 289                                 return PredicateToolkit.falsePredicate();
 290                         }
 291                 };
 292                 return ItemFilters.and(JdkFilters.RECORDING_SETTING, typeFilter,
 293                                 ItemFilters.equals(JdkAttributes.REC_SETTING_NAME, settingsName));
 294         }
 295 
 296         /**
 297          * Gets the maximum period setting for the specified event types in the given item collection.
 298          *
 299          * @param items
 300          *            the items to find the period setting in
 301          * @param typeIds
 302          *            the event type ids to find settings for
 303          * @return the maximum period setting for the specified event types
 304          */
 305         public static IQuantity getSettingMaxPeriod(IItemCollection items, String ... typeIds) {
 306                 Set<String> values = getPeriodSettings(items, typeIds);
 307                 return values == null || values.isEmpty() ? null : getSettingMaxPeriod(values);
 308         }
 309 
 310         /**
 311          * If possible, gets the longest period setting that is longer than the specified minimum period
 312          * for the given event types.
 313          *
 314          * @param items
 315          *            the item collection to search through
 316          * @param minPeriod
 317          *            the minimum period setting
 318          * @param typeIds
 319          *            the event type ids to find the period setting for
 320          * @return a string representation of the longest period longer than the {@code minPeriod}, or
 321          *         {@code null} if all periods are shorter than {@code minPeriod}
 322          */
 323         public static String getPeriodIfGreaterThan(IItemCollection items, IQuantity minPeriod, String ... typeIds) {
 324                 Set<String> values = getPeriodSettings(items, typeIds);
 325                 if (values != null && !values.isEmpty()) {
 326                         IQuantity max = getSettingMaxPeriod(values);
 327                         if (max == null) {
 328                                 return Messages.getString(Messages.RulesToolkit_EVERY_CHUNK);
 329                         } else if (max.compareTo(minPeriod) > 0) {
 330                                 return max.displayUsing(IDisplayable.AUTO);
 331                         }
 332                 }
 333                 return null;
 334         }
 335 
 336         /**
 337          * Converts a value persisted as a string by the JVM into an {@link IQuantity}.
 338          *
 339          * @param persistedValue
 340          *            the persisted value to convert
 341          * @return the resulting {@link IQuantity}
 342          */
 343         public static IQuantity parsePersistedJvmTimespan(String persistedValue) throws QuantityConversionException {
 344                 // FIXME: Copied from CommonConstraints.TimePersisterBrokenSI. Use that when it is exposed.
 345                 if (persistedValue.endsWith("m")) { //$NON-NLS-1$
 346                         persistedValue += "in"; //$NON-NLS-1$
 347                 }
 348                 return UnitLookup.TIMESPAN.parsePersisted(persistedValue);
 349         }
 350 
 351         /**
 352          * Returns a string describing the subset of event types given which have no duration threshold
 353          * set.
 354          *
 355          * @param items
 356          *            the item collection to search
 357          * @param typeIds
 358          *            the event type ids to find thresholds for
 359          * @return a comma-delimited string describing the event types with no threshold
 360          */
 361         public static String getTypesWithZeroThreshold(IItemCollection items, String ... typeIds) {
 362                 IItemFilter f = new IItemFilter() {
 363 
 364                         @Override
 365                         public IPredicate<IItem> getPredicate(IType<IItem> type) {
 366                                 final IMemberAccessor<String, IItem> accessor = JdkAttributes.REC_SETTING_VALUE.getAccessor(type);
 367                                 return new IPredicate<IItem>() {
 368 
 369                                         @Override
 370                                         public boolean evaluate(IItem o) {
 371                                                 try {
 372                                                         String thresholdValue = accessor.getMember(o);
 373                                                         return parsePersistedJvmTimespan(thresholdValue).longValue() == 0L;
 374                                                 } catch (QuantityConversionException e) {
 375                                                         throw new RuntimeException(e);
 376                                                 }
 377                                         }
 378                                 };
 379                         }
 380                 };
 381                 IItemFilter filter = ItemFilters.and(getSettingsFilter(REC_SETTING_NAME_THRESHOLD, typeIds), f);
 382                 return getEventTypeNames(items.apply(filter));
 383         }
 384 
 385         /**
 386          * This method checks if the provided event types were explicitly enabled by checking the
 387          * recording setting events.
 388          *
 389          * @param items
 390          *            the collection to check.
 391          * @param typeIds
 392          *            the identifiers for the event types to check.
 393          * @return true if all of the required event types were known to be explicitly enabled.
 394          */
 395         public static boolean isEventsEnabled(IItemCollection items, String ... typeIds) {
 396                 IQuantity aggregate = items.apply(createEnablementFilter(true, typeIds)).getAggregate(Aggregators.count());
 397                 return aggregate != null && aggregate.longValue() == typeIds.length;
 398         }
 399 
 400         /**
 401          * This method returns false if any {@link EventAvailability} is disabled or unavailable.
 402          * Otherwise true.
 403          *
 404          * @param eventAvailabilities
 405          *            the {@link EventAvailability} to check
 406          * @return false if any {@link EventAvailability} is disabled or unavailable. Otherwise true.
 407          */
 408         public static boolean isEventsEnabled(EventAvailability ... eventAvailabilities) {
 409                 for (EventAvailability availability : eventAvailabilities) {
 410                         if (availability == EventAvailability.DISABLED || availability == EventAvailability.UNKNOWN) {
 411                                 return false;
 412                         }
 413                 }
 414                 return true;
 415         }
 416 
 417         /**
 418          * This method checks if the provided event types were explicitly disabled by checking the
 419          * recording setting events.
 420          *
 421          * @param items
 422          *            the collection to check.
 423          * @param typeIds
 424          *            the identifiers for the event types to check.
 425          * @return true if all of the required event types were known to be explicitly enabled.
 426          */
 427         private static boolean isEventsDisabled(IItemCollection items, String ... typeIds) {
 428                 IQuantity aggregate = items.apply(createEnablementFilter(false, typeIds)).getAggregate(Aggregators.count());
 429                 return aggregate != null && aggregate.longValue() == typeIds.length;
 430         }
 431 
 432         /**
 433          * Checks the event availability for the event types.
 434          * <p>
 435          * Care should be taken when used with multiple typeIds. Use it when all of the provided typeIds
 436          * are expected to have the same availability; if mixed, the lowest common availability for all
 437          * types will be returned.
 438          *
 439          * @param items
 440          *            the collection to check
 441          * @param typeIds
 442          *            the type identifiers to check
 443          * @return the availability for the event types
 444          */
 445         public static EventAvailability getEventAvailability(IItemCollection items, final String ... typeIds) {
 446                 // Only AVAILABLE if exactly all types have events
 447                 if (hasEvents(items, typeIds)) {
 448                         return EventAvailability.AVAILABLE;
 449                 }
 450                 // If enabled at any point, it was indeed enabled, and events could have been recorded
 451                 if (isEventsEnabled(items, typeIds)) {
 452                         return EventAvailability.ENABLED;
 453                 }
 454                 if (isEventsDisabled(items, typeIds)) {
 455                         return EventAvailability.DISABLED;
 456                 }
 457                 if (isEventsKnown(items, typeIds)) {
 458                         return EventAvailability.NONE;
 459                 }
 460                 return EventAvailability.UNKNOWN;
 461         }
 462 
 463         /**
 464          * Returns the least available EventAvailability from the ones provided. See
 465          * {@link EventAvailability}.
 466          *
 467          * @return the least available EventAvailability from the ones provided.
 468          */
 469         public static EventAvailability getLeastAvailable(EventAvailability ... availabilites) {
 470                 EventAvailability lowest = EventAvailability.AVAILABLE;
 471 
 472                 for (EventAvailability availability : availabilites) {
 473                         if (availability.isLessAvailableThan(lowest)) {
 474                                 lowest = availability;
 475                         }
 476                 }
 477                 return lowest;
 478         }
 479 
 480         /**
 481          * Checks if the event types are known in the collection. Note that it does not necessarily mean
 482          * that there are events of the event type.
 483          *
 484          * @param items
 485          *            the collection to check
 486          * @param typeIds
 487          *            the event types to check
 488          * @return true if all the event types exists in the collection.
 489          */
 490         private static boolean isEventsKnown(IItemCollection items, String ... typeIds) {
 491                 Set<String> availableTypes = getAvailableTypeIds(items);
 492                 if (availableTypes.containsAll(Arrays.asList(typeIds))) {
 493                         return true;
 494                 }
 495                 return false;
 496         }
 497 
 498         /**
 499          * Returns true if precisely all of the event types have events.
 500          *
 501          * @param items
 502          *            the events.
 503          * @param typeIds
 504          *            the identifiers of the event types to check.
 505          * @return true if all of the types have events.
 506          */
 507         private static boolean hasEvents(IItemCollection items, String ... typeIds) {
 508                 for (String typeId : typeIds) {
 509                         if (!internalHasEvents(items, typeId)) {
 510                                 return false;
 511                         }
 512                 }
 513                 return true;
 514         }
 515 
 516         /**
 517          * Returns a proper result for the availability problem. The result will be a "Not Applicable"
 518          * result and the text provided will be based upon the assumption that the provided
 519          * EventAvailability is the availability that makes it impossible to evaluate the rule.
 520          *
 521          * @param rule
 522          *            the rule for which this result will be generated
 523          * @param items
 524          *            the items for which the availability was tested
 525          * @param eventAvailability
 526          *            the availability making the rule N/A
 527          * @param typeIds
 528          *            the types for which the availability was tested
 529          * @return the result for the provided availability problem
 530          */
 531         public static Result getEventAvailabilityResult(
 532                 IRule rule, IItemCollection items, EventAvailability eventAvailability, String ... typeIds) {
 533                 switch (eventAvailability) {
 534                 case ENABLED:
 535                 case NONE:
 536                         String requiredEventsTypeNames = getEventTypeNames(items, typeIds);
 537                         return getNotApplicableResult(rule,
 538                                         MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_EVENTS),
 539                                                         requiredEventsTypeNames),
 540                                         MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_EVENTS_LONG),
 541                                                         rule.getName(), requiredEventsTypeNames));
 542                 case DISABLED:
 543                         String disabledEventTypeNames = getDisabledEventTypeNames(items, typeIds);
 544                         return getNotApplicableResult(rule,
 545                                         MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_EVENT_TYPE),
 546                                                         disabledEventTypeNames),
 547                                         MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_EVENT_TYPE_LONG),
 548                                                         rule.getName(), disabledEventTypeNames));
 549                 case UNKNOWN:
 550                         // Can't get type names if the event type is unavailable
 551                         List<String> quotedTypeIds = new ArrayList<>();
 552                         for (String typeId : typeIds) {
 553                                 quotedTypeIds.add("'" + typeId + "'"); //$NON-NLS-1$ //$NON-NLS-2$
 554                         }
 555                         Collections.sort(quotedTypeIds);
 556                         String unavailableTypeNames = StringToolkit.join(quotedTypeIds, ", "); //$NON-NLS-1$
 557                         return getNotApplicableResult(rule,
 558                                         MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_UNAVAILABLE_EVENT_TYPE),
 559                                                         rule.getName(), unavailableTypeNames),
 560                                         MessageFormat.format(
 561                                                         Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_UNAVAILABLE_EVENT_TYPE_LONG),
 562                                                         rule.getName(), unavailableTypeNames));
 563                 case AVAILABLE:
 564                         String availableEventTypeNames = getEventTypeNames(items, typeIds);
 565                         return getNotApplicableResult(rule,
 566                                         MessageFormat.format(
 567                                                         Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_EVENT_TYPE_NOT_AVAILABLE),
 568                                                         availableEventTypeNames),
 569                                         MessageFormat.format(Messages.RulesToolkit_RULE_REQUIRES_EVENT_TYPE_NOT_AVAILABLE_LONG,
 570                                                         rule.getName(), availableEventTypeNames));
 571                 default:
 572                         throw new IllegalArgumentException("Unsupported event availability: " + eventAvailability); //$NON-NLS-1$
 573                 }
 574         }
 575 
 576         /**
 577          * Creates a {@link Result} object for the given {@link IRule} object representing a result
 578          * where there are too few events to properly evaluate a rule.
 579          *
 580          * @param rule
 581          *            the rule to create a {@link Result} object for
 582          * @return an object describing that the rule could not be evaluated due to there not being
 583          *         enough data
 584          */
 585         public static Result getTooFewEventsResult(IRule rule) {
 586                 return getNotApplicableResult(rule, Messages.getString(Messages.RulesToolkit_TOO_FEW_EVENTS));
 587         }
 588 
 589         /**
 590          * Creates a {@link Result} object with a generic not applicable (N/A) result for a given rule
 591          * with a specified message.
 592          *
 593          * @param rule
 594          *            the rule to create a {@link Result} object for
 595          * @param message
 596          *            the description of the result
 597          * @return an object representing a generic not applicable result for the provided rule
 598          */
 599         public static Result getNotApplicableResult(IRule rule, String message) {
 600                 return getNotApplicableResult(rule, message, null);
 601         }
 602 
 603         /**
 604          * Creates a {@link Result} object with a generic not applicable (N/A) result for a given rule
 605          * with a specified message.
 606          *
 607          * @param rule
 608          *            the rule to create a {@link Result} object for
 609          * @param shortMessage
 610          *            the description of the result, as a short description
 611          * @param longMessage
 612          *            a longer version of the description, used to explain in more detail why the rule
 613          *            could not be evaluated
 614          * @return an object representing a generic not applicable result for the provided rule
 615          */
 616         private static Result getNotApplicableResult(IRule rule, String shortMessage, String longMessage) {
 617                 return new Result(rule, Result.NOT_APPLICABLE, shortMessage, longMessage);
 618         }
 619 
 620         /**
 621          * Creates a {@link Result} object describing that at least one of the specified event types
 622          * must be present in the rule's input.
 623          *
 624          * @param rule
 625          *            the rule to create a {@link Result} object for
 626          * @param typeIds
 627          *            the ids of the event types required for this rule
 628          * @return an object representing a not applicable result due to not missing event types
 629          */
 630         public static Result getRuleRequiresAtLeastOneEventTypeResult(IRule rule, String ... typeIds) {
 631                 return getNotApplicableResult(rule,
 632                                 MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_REQUIRES_SOME_EVENTS),
 633                                                 rule.getName(), StringToolkit.join(typeIds, ", "))); //$NON-NLS-1$
 634         }
 635 
 636         /**
 637          * Creates a text message informing that event types are recommended
 638          *
 639          * @param items
 640          *            the events.
 641          * @param typeIds
 642          *            the identifiers of the event types to check.
 643          * @return a text message informing that event types are recommended
 644          */
 645         public static String getEnabledEventTypesRecommendation(IItemCollection items, String ... typeIds) {
 646                 return MessageFormat.format(Messages.getString(Messages.RulesToolkit_RULE_RECOMMENDS_EVENTS),
 647                                 getDisabledEventTypeNames(items, typeIds));
 648         }
 649 
 650         /**
 651          * Gets the Java version for the recording the provided {@link IItemCollection} represents.
 652          *
 653          * @param items
 654          *            the recording to find the version of
 655          * @return an object representing the Java version of the VM the items originate from
 656          */
 657         public static JavaVersion getJavaSpecVersion(IItemCollection items) {
 658                 IItemCollection versionProperties = items.apply(ItemFilters.and(JdkFilters.SYSTEM_PROPERTIES,
 659                                 ItemFilters.equals(JdkAttributes.ENVIRONMENT_KEY, "java.vm.specification.version"))); //$NON-NLS-1$
 660                 Set<String> vmSpecificationVersions = versionProperties
 661                                 .getAggregate(Aggregators.distinct(JdkAttributes.ENVIRONMENT_VALUE));
 662                 if (vmSpecificationVersions != null && vmSpecificationVersions.size() >= 1) {
 663                         return new JavaVersion(vmSpecificationVersions.iterator().next());
 664                 }
 665                 // Fall back to using the major version number of the JVM version as Java specification version
 666                 JavaVersion jvmVersion = getJavaVersion(items);
 667                 if (jvmVersion != null) {
 668                         return new JavaVersion(jvmVersion.getMajorVersion());
 669                 }
 670                 return null;
 671         }
 672 
 673         /**
 674          * @param items
 675          *            the items to look for the JVM version in.
 676          * @return the parsed JavaVersion, or null if no VM information event with the JVM version could
 677          *         be found.
 678          */
 679         public static JavaVersion getJavaVersion(IItemCollection items) {
 680                 String jvmVersion = items.getAggregate(JdkAggregators.JVM_VERSION);
 681                 return getJavaVersion(jvmVersion);
 682         }
 683 
 684         /**
 685          * An exponential mapping from 0/infinity to 0/74 passing through 25 at limit. This approaches
 686          * 74 at about 300-400% of the limit.
 687          *
 688          * @param value
 689          *            Input value. Negative values will be treated as zero.
 690          * @param x1
 691          *            Return 25 if value is equal to this. Must be more than zero.
 692          * @return A value between 0 and 74.
 693          */
 694         public static double mapExp74(double value, double x1) {
 695                 return mapExp(value, 74, x1, 25);
 696         }
 697 
 698         /**
 699          * An exponential mapping from 0/infinity to 0/100 passing through 75 at limit. This approaches
 700          * 100 at about 300-400% of the limit.
 701          *
 702          * @param value
 703          *            Input value. Negative values will be treated as zero.
 704          * @param x1
 705          *            Return 75 if value is equal to this. Must be more than zero.
 706          * @return A value between 0 and 100.
 707          */
 708         public static double mapExp100(double value, double x1) {
 709                 return mapExp(value, 100, x1, 75);
 710         }
 711 
 712         /**
 713          * An exponential mapping from 0/infinity to 0/100 passing through y1 at x1.
 714          *
 715          * @param value
 716          *            Input value. Negative values will be treated as zero.
 717          * @param x1
 718          *            Return y1 if value is equal to this. Must be more than zero.
 719          * @param y1
 720          *            Return value at x1. Must be more than zero and less than 100.
 721          * @return A value between 0 and 100.
 722          */
 723         public static double mapExp100Y(double value, double x1, double y1) {
 724                 return mapExp(value, 100, x1, y1);
 725         }
 726 
 727         /**
 728          * An exponential mapping from 0/infinity to 0/100 passing through 25 and 75 at limits. This
 729          * approaches 100 at about 300-400% of the 75 limit.
 730          *
 731          * @param value
 732          *            Input value. Negative values will be treated as zero.
 733          * @param x1
 734          *            Return 25 if value is equal to this. Must be more than zero.
 735          * @param x2
 736          *            Return 75 if value is equal to this. Must be more than x1.
 737          * @return A value between 0 and 100.
 738          */
 739         public static double mapExp100(double value, double x1, double x2) {
 740                 return mapExp(value, 100, x1, 25, x2, 75);
 741         }
 742 
 743         /**
 744          * @param value
 745          *            Input value. Negative values will be treated as zero.
 746          * @param ceiling
 747          *            Max return value. Must be more than zero.
 748          * @param x1
 749          *            Return y1 if value is equal to this. Must be more than zero.
 750          * @param y1
 751          *            Return value at x1. Must be more than zero and less than ceiling.
 752          * @return A value between 0 and ceiling.
 753          */
 754         // FIXME: We might want to have a wider input range that produces discernible output values.
 755         public static double mapExp(double value, double ceiling, double x1, double y1) {
 756                 if (value < 0) {
 757                         return 0;
 758                 }
 759                 double k = Math.log(1 - y1 / ceiling) / x1;
 760                 return ceiling * (1 - Math.exp(k * value));
 761         }
 762 
 763         /**
 764          * @param value
 765          *            Input value. Negative values will be treated as zero.
 766          * @param ceiling
 767          *            Max return value. Must be more than zero.
 768          * @param x1
 769          *            Return y1 if value is equal to this. Must be more than zero.
 770          * @param y1
 771          *            Return y1 at x1. Must be more than zero and less than y2.
 772          * @param x2
 773          *            Return y2 if value is equal to this. Must be more than x1.
 774          * @param y2
 775          *            Return y2 at x2. Must be more than y1 and less than ceiling.
 776          * @return A value between 0 and ceiling.
 777          */
 778         private static double mapExp(double value, double ceiling, double x1, double y1, double x2, double y2) {
 779                 if (value < 0) {
 780                         return 0;
 781                 }
 782                 if (value < x1) {
 783                         return y1 / x1 * value;
 784                 }
 785                 return y1 + mapExp(value - x1, ceiling - y1, x2 - x1, y2 - y1);
 786         }
 787 
 788         /**
 789          * An multi-linear mapping from 0/1 to 0/100 passing through 25 and 75 at limits.
 790          *
 791          * @param value
 792          *            Input value. Negative values will be treated as zero.
 793          * @param x1
 794          *            Return 25 if value is equal to this. Must be more than zero.
 795          * @param x2
 796          *            Return 75 if value is equal to this. Must be more than x1.
 797          * @return A value between 0 and 100.
 798          */
 799         public static double mapLin100(double value, double x1, double x2) {
 800                 if (value <= 0) {
 801                         return 0;
 802                 }
 803                 if (value >= 1) {
 804                         return 1;
 805                 }
 806                 if (value <= x1) {
 807                         return value * 25 / x1;
 808                 }
 809                 if (value <= x2) {
 810                         return 25 + (value - x1) * (75 - 25) / (x2 - x1);
 811                 }
 812                 return 75 + (value - x2) * (100 - 75) / (1 - x2);
 813         }
 814 
 815         /**
 816          * Each group is represented by the number of elements that belong in that group, elements are
 817          * grouped by accessor value.
 818          * <p>
 819          * For example, the items {A, B, C, A, B, A, A} will become {1, 2, 4}
 820          *
 821          * @param items
 822          *            input items
 823          * @param accessorFactory
 824          *            a factory that provides accessors for the input item types
 825          * @return A sorted list of counts, one for each unique value that the accessor computes from
 826          *         the input items, that tells how many input items gave that accessor value.
 827          */
 828         public static <T> List<IntEntry<T>> calculateGroupingScore(
 829                 IItemCollection items, IAccessorFactory<T> accessorFactory) {
 830                 EntryHashMap<T, IntEntry<T>> map = MapToolkit.createIntMap(1000, 0.5f);
 831                 for (IItemIterable ii : items) {
 832                         IMemberAccessor<? extends T, IItem> accessor = accessorFactory.getAccessor(ii.getType());
 833                         if (accessor == null) {
 834                                 continue;
 835                         }
 836                         for (IItem item : ii) {
 837                                 T member = accessor.getMember(item);
 838                                 if (member != null) {
 839                                         IntEntry<T> entry = map.get(member, true);
 840                                         entry.setValue(entry.getValue() + 1);
 841                                 }
 842                         }
 843                 }
 844                 List<IntEntry<T>> array = IteratorToolkit.toList(map.iterator(), map.size());
 845                 Collections.sort(array);
 846                 return array;
 847         }
 848 
 849         /**
 850          * Calculates a balance for entries, where later elements get a higher relevance than earlier
 851          * elements.
 852          * <p>
 853          * For example the values 1, 1, 2, 5 will get the total score 5/9/1 + 2/9/2 + 1/9/3 + 1/9/4
 854          *
 855          * @param array
 856          *            input values
 857          * @return the balance score
 858          */
 859         public static <T> double calculateBalanceScore(List<IntEntry<T>> array) {
 860                 int totalCount = 0;
 861                 for (IntEntry<T> e : array) {
 862                         totalCount += e.getValue();
 863                 }
 864                 double score = 0;
 865                 for (int i = array.size() - 1; i >= 0; i--) {
 866                         int index = array.size() - i;
 867                         score += ((double) array.get(i).getValue()) / totalCount / index;
 868                 }
 869                 return score;
 870         }
 871 
 872         /**
 873          * Get the duration for item within the specified window
 874          *
 875          * @param windowStart
 876          *            window start
 877          * @param windowEnd
 878          *            window end
 879          * @param item
 880          *            item to get duration for
 881          * @return duration within window
 882          */
 883         public static IQuantity getDurationInWindow(IQuantity windowStart, IQuantity windowEnd, IItem item) {
 884                 IQuantity start = getStartTime(item);
 885                 IQuantity end = getEndTime(item);
 886                 IQuantity startCapped = start.compareTo(windowStart) > 0 ? start : windowStart;
 887                 IQuantity endCapped = end.compareTo(windowEnd) > 0 ? windowEnd : end;
 888                 IQuantity durationCapped = endCapped.subtract(startCapped);
 889                 return durationCapped;
 890         }
 891 
 892         /**
 893          * Maps the input value into a value between the minimum and maximum values (exclusive) using a
 894          * sigmoidal curve with the given parameters. Minimum and maximum values are asymptotes, so will
 895          * never be mapped to. If you want to map from [0,1] to (0,100) using this you should set a low
 896          * inflection point and a single digit high curve fit with a low curve fit around 150. This will
 897          * lead to exponential growth until the midway point where it will start growing
 898          * logarithmically.
 899          *
 900          * @param input
 901          *            the value to map
 902          * @param minimum
 903          *            the maximum value to map to (exclusive)
 904          * @param maximum
 905          *            the minimum value to map to (exclusive)
 906          * @param lowCurveFit
 907          *            fitting parameter for the lower end of the curve
 908          * @param inflectionPoint
 909          *            the inflection point of the curve (where input leads to 1/3 between min and max)
 910          * @param highCurveFit
 911          *            fitting parameter for the higher end of the curve
 912          * @return a mapped value in the range (minimum,maximum)
 913          */
 914         public static double mapSigmoid(
 915                 double input, double minimum, double maximum, double lowCurveFit, double inflectionPoint, double highCurveFit) {
 916                 // https://www.desmos.com/calculator/aylt9wv1x0 to view the curve
 917                 double g = Math.exp(lowCurveFit * (inflectionPoint - input));
 918                 double h = Math.exp(highCurveFit * (inflectionPoint - input));
 919                 return minimum + (maximum / (1 + g + h));
 920         }
 921 
 922         private static IQuantity getSettingMaxPeriod(Iterable<String> settingsValues) {
 923                 IQuantity maxPeriod = null;
 924                 for (String s : settingsValues) {
 925                         try {
 926                                 if (REC_SETTING_PERIOD_EVERY_CHUNK.equals(s)) {
 927                                         return null;
 928                                 }
 929                                 IQuantity p = parsePersistedJvmTimespan(s);
 930                                 if (maxPeriod == null || maxPeriod.compareTo(p) < 0) {
 931                                         maxPeriod = p;
 932                                 }
 933                         } catch (QuantityConversionException e) {
 934                                 throw new RuntimeException(e);
 935                         }
 936                 }
 937                 return maxPeriod;
 938         }
 939 
 940         private static Set<String> getPeriodSettings(IItemCollection items, String ... typeIds) {
 941                 IItemFilter filter = getSettingsFilter(REC_SETTING_NAME_PERIOD, typeIds);
 942                 return items.apply(filter).getAggregate(Aggregators.distinct(JdkAttributes.REC_SETTING_VALUE));
 943         }
 944 
 945         private static String getDisabledEventTypeNames(IItemCollection items, String ... typeIds) {
 946                 return getEventTypeNames(items.apply(createEnablementFilter(false, typeIds)));
 947         }
 948 
 949         private static String getEventTypeNames(IItemCollection items, String ... typeIds) {
 950                 return getEventTypeNames(items.apply(getSettingsFilter(REC_SETTING_NAME_ENABLED, typeIds)));
 951         }
 952 
 953         private static String getEventTypeNames(IItemCollection items) {
 954                 Set<String> names = items.getAggregate(Aggregators.distinct("", TYPE_NAME_ACCESSOR_FACTORY)); //$NON-NLS-1$
 955                 if (names == null) {
 956                         return null;
 957                 }
 958                 List<String> quotedNames = new ArrayList<>();
 959                 for (String name : names) {
 960                         quotedNames.add("'" + name + "'"); //$NON-NLS-1$ //$NON-NLS-2$
 961                 }
 962                 Collections.sort(quotedNames);
 963                 return StringToolkit.join(quotedNames, ", "); //$NON-NLS-1$
 964         }
 965 
 966         private static IItemFilter createEnablementFilter(boolean enabled, String ... typeIds) {
 967                 IItemFilter settingsFilter = getSettingsFilter(REC_SETTING_NAME_ENABLED, typeIds);
 968                 IItemFilter enabledFilter = ItemFilters.equals(JdkAttributes.REC_SETTING_VALUE,
 969                                 enabled ? Boolean.TRUE.toString() : Boolean.FALSE.toString());
 970                 IItemFilter enablementFilter = ItemFilters.and(settingsFilter, enabledFilter);
 971                 return enablementFilter;
 972         }
 973 
 974         private static boolean internalHasEvents(IItemCollection items, String typeId) {
 975                 return items.apply(ItemFilters.type(typeId)).hasItems();
 976         }
 977 
 978         private static Set<String> getAvailableTypeIds(IItemCollection items) {
 979                 Set<String> ids = new HashSet<>();
 980                 for (IItemIterable iterable : items) {
 981                         ids.add(iterable.getType().getIdentifier());
 982                 }
 983                 return ids;
 984         }
 985 
 986         /**
 987          * @param vmInfoVersionString
 988          *            the JVM version information as presented in the VM information events, containing
 989          *            both the JVM and JDK version numbers.
 990          * @return the parsed JavaVersion.
 991          */
 992         public static JavaVersion getJavaVersion(String vmInfoVersionString) {
 993                 if (vmInfoVersionString != null) {
 994                         Matcher versionMatcher = VERSION_PATTERN.matcher(vmInfoVersionString);
 995                         if (versionMatcher.matches()) {
 996                                 String versionString = versionMatcher.group(1);
 997                                 return new JavaVersion(versionString);
 998                         }
 999                 }
1000                 return null;
1001         }
1002 
1003         /**
1004          * Gets the {@link IType} representation of a specific event type in an {@link IItemCollection}.
1005          *
1006          * @param items
1007          *            the items to find the type in
1008          * @param typeId
1009          *            the event type id to find the type object of
1010          * @return an object representing the type with the specified id in the provided item collection
1011          */
1012         public static IType<IItem> getType(IItemCollection items, String typeId) {
1013                 for (IItemIterable iter : items) {
1014                         if (iter.getType().getIdentifier().equals(typeId)) {
1015                                 return iter.getType();
1016                         }
1017                 }
1018                 return null;
1019         }
1020 
1021         /**
1022          * Gets a {@link Result} object representing a not applicable result due to a missing attribute.
1023          *
1024          * @param rule
1025          *            the rule which could not be evaluated
1026          * @param type
1027          *            the type of the item which is missing a required attribute
1028          * @param attribute
1029          *            the attribute that is missing
1030          * @return an object that represents the not applicable result due to the missing attribute
1031          */
1032         public static Result getMissingAttributeResult(IRule rule, IType<IItem> type, IAttribute<?> attribute) {
1033                 return getNotApplicableResult(rule,
1034                                 MessageFormat.format(Messages.getString(Messages.RulesToolkit_ATTRIBUTE_NOT_FOUND),
1035                                                 attribute.getIdentifier(), type.getIdentifier()),
1036                                 MessageFormat.format(Messages.getString(Messages.RulesToolkit_ATTRIBUTE_NOT_FOUND_LONG),
1037                                                 attribute.getIdentifier(), type.getIdentifier()));
1038         }
1039 
1040         /**
1041          * Creates a thread and range filter for a set of related events. It assumes all the events
1042          * provided are related and spans one contiguous time range per thread. The resulting filter
1043          * will include all the original events plus any other occurred in the same thread during the
1044          * time period (per thread) spanned by the events in the collection. Note that this is an
1045          * expensive operation. Use with care.
1046          *
1047          * @param items
1048          *            a collection of related items.
1049          * @return a filter for the thread and time range.
1050          */
1051         public static TimeRangeThreadFilter createThreadsAndRangesFilter(IItemCollection items) {
1052                 Map<IMCThread, Range> rangeMap = new HashMap<>();
1053 
1054                 for (IItemIterable iter : items) {
1055                         for (IItem item : iter) {
1056                                 IMCThread thread = getThread(item);
1057                                 if (thread == null) {
1058                                         continue;
1059                                 }
1060                                 if (!rangeMap.containsKey(thread)) {
1061                                         rangeMap.put(thread, new Range(getStartTime(item), getEndTime(item)));
1062                                 } else {
1063                                         Range r = rangeMap.get(thread);
1064                                         IQuantity startTime = getStartTime(item);
1065                                         IQuantity endTime = getEndTime(item);
1066                                         if (startTime.compareTo(r.startTime) < 0 || endTime.compareTo(r.endTime) > 0) {
1067                                                 IQuantity newStartTime = startTime.compareTo(r.startTime) < 0 ? startTime : r.startTime;
1068                                                 IQuantity newEndTime = endTime.compareTo(r.endTime) > 0 ? endTime : r.endTime;
1069                                                 rangeMap.put(thread, new Range(newStartTime, newEndTime));
1070                                         }
1071                                 }
1072                         }
1073                 }
1074                 return new TimeRangeThreadFilter(rangeMap);
1075         }
1076 
1077         /**
1078          * Creates a range filter for an event. The range will span the same time as the event.
1079          *
1080          * @param item
1081          *            the event for which to create the range filter
1082          * @return a filter for the time range of the event.
1083          */
1084         public static IItemFilter createRangeFilter(IItem item) {
1085                 return new TimeRangeFilter(new Range(getStartTime(item), getEndTime(item)));
1086         }
1087 
1088         /**
1089          * Convenience method for getting the start time value from a specific event.
1090          *
1091          * @param item
1092          *            the event to get the start time from
1093          * @return the start time of the provided event
1094          */
1095         public static IQuantity getStartTime(IItem item) {
1096                 return RulesToolkit.getValue(item, JfrAttributes.START_TIME);
1097         }
1098 
1099         /**
1100          * Convenience method to get the end time value from a specific event.
1101          *
1102          * @param item
1103          *            the event to get the end time from
1104          * @return the end time of the provided event
1105          */
1106         public static IQuantity getEndTime(IItem item) {
1107                 return RulesToolkit.getValue(item, JfrAttributes.END_TIME);
1108         }
1109 
1110         /**
1111          * Convenience method to get the duration value from a specific event.
1112          *
1113          * @param item
1114          *            the event to get the duration from
1115          * @return the duration of the provided event
1116          */
1117         public static IQuantity getDuration(IItem item) {
1118                 return RulesToolkit.getValue(item, JfrAttributes.DURATION);
1119         }
1120 
1121         /**
1122          * Convenience method to get the event thread value from a specific event.
1123          *
1124          * @param item
1125          *            the event to get the thread value from
1126          * @return the thread the provided event occurred in
1127          */
1128         public static IMCThread getThread(IItem item) {
1129                 return getOptionalValue(item, JfrAttributes.EVENT_THREAD);
1130         }
1131 
1132         /**
1133          * Returns the value, or null if no accessor is available.
1134          */
1135         private static <T> T getOptionalValue(IItem item, IAccessorFactory<T> attribute) {
1136                 IType<IItem> itemType = ItemToolkit.getItemType(item);
1137                 IMemberAccessor<? extends T, IItem> accessor = attribute.getAccessor(itemType);
1138                 if (accessor == null) {
1139                         return null;
1140                 }
1141                 return accessor.getMember(item);
1142         }
1143 
1144         /**
1145          * Calculates the ratio between two {@link IQuantity} values of compatible, linear kind and
1146          * returns it represented as a percentage.
1147          *
1148          * @param antecedent
1149          *            the antecedent (numerator) value
1150          * @param consequent
1151          *            the consequent (denominator) value
1152          * @return the ratio between the two values as a percentage
1153          */
1154         public static IQuantity toRatioPercent(IQuantity antecedent, IQuantity consequent) {
1155                 return UnitLookup.PERCENT.quantity(antecedent.ratioTo(consequent) * 100f);
1156         }
1157 
1158         /**
1159          * Same calculation as {@link RulesToolkit#toRatioPercent(IQuantity, IQuantity)} but it returns
1160          * the percentage as a string instead.
1161          *
1162          * @param antecedent
1163          *            the antecedent (numerator) value
1164          * @param consequent
1165          *            the consequent (denominator) value
1166          * @return the ratio between the two values as a percentage, as a string
1167          */
1168         public static String toRatioPercentString(IQuantity antecedent, IQuantity consequent) {
1169                 return toRatioPercent(antecedent, consequent).displayUsing(IDisplayable.AUTO);
1170         }
1171 
1172         /**
1173          * Retrieves all topics that have rules associated with them.
1174          *
1175          * @return all topics associated with any rule
1176          */
1177         public static Collection<String> getAllTopics() {
1178                 Set<String> topics = new HashSet<>();
1179                 for (IRule r : RuleRegistry.getRules()) {
1180                         topics.add(r.getTopic());
1181                 }
1182                 return topics;
1183         }
1184 
1185         /**
1186          * Evaluates a collection of rules in parallel threads. The method returns a map of rules and
1187          * {@link Future future} results that are scheduled to run using the specified number of
1188          * threads.
1189          * <p>
1190          * You can use a single threaded loop over the returned futures to {@link Future#get() get} the
1191          * results.
1192          * <p>
1193          * If evaluation of a rule fails, then the get method of the corresponding future will throw an
1194          * {@link ExecutionException}.
1195          *
1196          * @param rules
1197          *            rules to run
1198          * @param items
1199          *            items to evaluate
1200          * @param preferences
1201          *            See {@link IRule#evaluate(IItemCollection, IPreferenceValueProvider)}. If
1202          *            {@code null}, then default values will be used.
1203          * @param nThreads
1204          *            The number or parallel threads to use when evaluating. If 0, then the number of
1205          *            available processors will be used.
1206          * @return a map from rules to result futures
1207          */
1208         public static Map<IRule, Future<Result>> evaluateParallel(
1209                 Collection<IRule> rules, IItemCollection items, IPreferenceValueProvider preferences, int nThreads) {
1210                 if (preferences == null) {
1211                         preferences = IPreferenceValueProvider.DEFAULT_VALUES;
1212                 }
1213                 if (nThreads < 1) {
1214                         nThreads = Runtime.getRuntime().availableProcessors();
1215                 }
1216                 Map<IRule, Future<Result>> resultFutures = new HashMap<>();
1217                 Queue<RunnableFuture<Result>> futureQueue = new ConcurrentLinkedQueue<>();
1218                 for (IRule rule : rules) {
1219                         RunnableFuture<Result> resultFuture = rule.evaluate(items, preferences);
1220                         resultFutures.put(rule, resultFuture);
1221                         futureQueue.add(resultFuture);
1222                 }
1223                 for (int i = 0; i < nThreads; i++) {
1224                         RuleEvaluator re = new RuleEvaluator(futureQueue);
1225                         Thread t = new Thread(re);
1226                         t.start();
1227                 }
1228                 return resultFutures;
1229         }
1230 
1231         private static class RuleEvaluator implements Runnable {
1232                 private Queue<RunnableFuture<Result>> futureQueue;
1233 
1234                 public RuleEvaluator(Queue<RunnableFuture<Result>> futureQueue) {
1235                         this.futureQueue = futureQueue;
1236                 }
1237 
1238                 @Override
1239                 public void run() {
1240                         RunnableFuture<Result> resultFuture;
1241                         while ((resultFuture = futureQueue.poll()) != null) {
1242                                 resultFuture.run();
1243                         }
1244                 }
1245         }
1246 
1247         /**
1248          * Gets the second frame in the most common stack trace. Useful when showing what called a
1249          * interesting method, like for example java.lang.Integer.valueOf (aka autoboxing)
1250          *
1251          * @param items
1252          *            the item collection to build the aggregated stack trace on
1253          * @return a stack trace frame
1254          */
1255         // FIXME: Generalize this a bit, get the top N frames
1256         public static String getSecondFrameInMostCommonTrace(IItemCollection items) {
1257                 FrameSeparator sep = new FrameSeparator(FrameSeparator.FrameCategorization.LINE, false);
1258                 StacktraceModel stacktraceModel = new StacktraceModel(false, sep, items);
1259                 Branch firstBranch = stacktraceModel.getRootFork().getBranch(0);
1260                 StacktraceFrame secondFrame = null;
1261                 if (firstBranch.getTailFrames().length > 0) {
1262                         secondFrame = firstBranch.getTailFrames()[0];
1263                 } else if (firstBranch.getEndFork().getBranchCount() > 0) {
1264                         secondFrame = firstBranch.getEndFork().getBranch(0).getFirstFrame();
1265                 } else {
1266                         return null;
1267                 }
1268                 /*
1269                  * FIXME: Consider defining the method formatting based on preferences.
1270                  *
1271                  * Currently it's a compromise between keeping the length short, but still being able to
1272                  * identify the actual method, even if the line number is a bit incorrect.
1273                  */
1274                 return StacktraceFormatToolkit.formatFrame(secondFrame.getFrame(), sep, false, false, true, true, true, false);
1275         }
1276 
1277         /**
1278          * Convenience method for parsing the -XX:FlightRecorderOptions JVM flag. Since this is one flag
1279          * that contains all flight recorder settings in a comma separated list as a single string it is
1280          * useful to have one place to get the actual setting/value pairs from an
1281          * {@link IItemCollection}.
1282          *
1283          * @param items
1284          *            an item collection containing at least one {@link JdkTypeIDs#STRING_FLAG} event
1285          *            with the value "FlightRecorderOptions"
1286          * @return a setting/value map for all FlightRecorderOptions
1287          */
1288         public static Map<String, String> getFlightRecorderOptions(IItemCollection items) {
1289                 Map<String, String> options = new HashMap<>();
1290                 IItemFilter stringFlagsFilter = ItemFilters.type(JdkTypeIDs.STRING_FLAG);
1291                 IItemFilter optionsFilter = ItemFilters.matches(JdkAttributes.FLAG_NAME, "FlightRecorderOptions"); //$NON-NLS-1$
1292                 IItemCollection optionsFlag = items.apply(ItemFilters.and(stringFlagsFilter, optionsFilter));
1293                 Set<String> optionsValues = optionsFlag.getAggregate(Aggregators.distinct(JdkAttributes.FLAG_VALUE_TEXT));
1294                 if (optionsValues != null && optionsValues.size() > 0) {
1295                         String optionsValue = optionsValues.iterator().next();
1296                         String[] allOptions = optionsValue.split(","); //$NON-NLS-1$
1297                         for (String optionAndValue : allOptions) {
1298                                 String[] optionAndValueSplit = optionAndValue.split("="); //$NON-NLS-1$
1299                                 if (optionAndValueSplit.length >= 2) {
1300                                         options.put(optionAndValueSplit[0], optionAndValueSplit[1]);
1301                                 } else {
1302                                         options.put(optionAndValue, ""); //$NON-NLS-1$
1303                                 }
1304                         }
1305                 }
1306                 return options;
1307         }
1308 
1309         /**
1310          * Checks if the timerange spanned by the items is shorter than the limit, and returns a
1311          * informative text message if that is the case.
1312          *
1313          * @param items
1314          *            the item collection to get recording range from
1315          * @param shortRecordingLimit
1316          *            limit for a short recording
1317          * @return a text message informing that this is a short recording, or null if recording is not
1318          *         short
1319          */
1320         public static String getShortRecordingInfo(IItemCollection items, IQuantity shortRecordingLimit) {
1321                 IQuantity recordingDuration = getItemRange(items);
1322                 boolean shortRecording = recordingDuration.compareTo(shortRecordingLimit) < 0;
1323                 if (shortRecording) {
1324                         return MessageFormat.format(Messages.getString(Messages.Result_SHORT_RECORDING),
1325                                         recordingDuration.displayUsing(IDisplayable.AUTO),
1326                                         shortRecordingLimit.displayUsing(IDisplayable.AUTO));
1327                 }
1328                 return null;
1329         }
1330 
1331         private static IQuantity getItemRange(IItemCollection items) {
1332                 IQuantity first = items.getAggregate(JdkAggregators.FIRST_ITEM_START);
1333                 IQuantity last = items.getAggregate(JdkAggregators.LAST_ITEM_END);
1334 
1335                 return last.subtract(first);
1336         }
1337 
1338         /**
1339          * Sorts map according to values.
1340          *
1341          * @param map
1342          *            the map to sort
1343          * @param sortAscending
1344          *            true if the sorting should be from lower to higher, false for higher to lower
1345          * @return sorted map
1346          */
1347         public static Map<String, Integer> sortMap(final Map<String, Integer> map, final boolean sortAscending) {
1348                 List<Map.Entry<String, Integer>> entries = new ArrayList<>(map.entrySet());
1349                 Collections.sort(entries, new Comparator<Map.Entry<String, Integer>>() {
1350                         @Override
1351                         public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
1352                                 if (sortAscending) {
1353                                         return o1.getValue().compareTo(o2.getValue());
1354                                 } else {
1355                                         return o2.getValue().compareTo(o1.getValue());
1356                                 }
1357                         }
1358                 });
1359                 final Map<String, Integer> sortedMap = new LinkedHashMap<>();
1360                 for (Map.Entry<String, Integer> entry : entries) {
1361                         sortedMap.put(entry.getKey(), entry.getValue());
1362                 }
1363                 return sortedMap;
1364         }
1365 }