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