1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * Copyright (c) 2019, Red Hat Inc. All rights reserved.
   4  *
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * The contents of this file are subject to the terms of either the Universal Permissive License
   8  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   9  *
  10  * or the following license:
  11  *
  12  * Redistribution and use in source and binary forms, with or without modification, are permitted
  13  * provided that the following conditions are met:
  14  *
  15  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  16  * and the following disclaimer.
  17  *
  18  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  19  * conditions and the following disclaimer in the documentation and/or other materials provided with
  20  * the distribution.
  21  *
  22  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  23  * endorse or promote products derived from this software without specific prior written permission.
  24  *
  25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  26  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  27  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  31  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  32  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33  */
  34 
  35 package org.openjdk.jmc.flightrecorder.ui.common;
  36 
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.HashMap;
  40 import java.util.Iterator;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.concurrent.ConcurrentHashMap;
  44 import java.util.function.Function;
  45 
  46 import org.eclipse.jface.viewers.ArrayContentProvider;
  47 import org.eclipse.jface.viewers.ColumnViewer;
  48 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
  49 import org.eclipse.jface.viewers.IStructuredSelection;
  50 import org.eclipse.jface.viewers.TableViewer;
  51 import org.eclipse.swt.SWT;
  52 import org.eclipse.swt.widgets.Composite;
  53 import org.openjdk.jmc.common.item.Aggregators.AggregatorBase;
  54 import org.openjdk.jmc.common.item.IItem;
  55 import org.openjdk.jmc.common.item.IItemCollection;
  56 import org.openjdk.jmc.common.item.IItemFilter;
  57 import org.openjdk.jmc.common.item.IMemberAccessor;
  58 import org.openjdk.jmc.common.item.IType;
  59 import org.openjdk.jmc.common.item.ItemFilters;
  60 import org.openjdk.jmc.common.unit.IQuantity;
  61 import org.openjdk.jmc.common.unit.UnitLookup;
  62 import org.openjdk.jmc.common.util.Pair;
  63 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  64 import org.openjdk.jmc.flightrecorder.ui.common.DurationHdrHistogram.DurationItemConsumer;
  65 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  66 import org.openjdk.jmc.ui.UIPlugin;
  67 import org.openjdk.jmc.ui.accessibility.FocusTracker;
  68 import org.openjdk.jmc.ui.column.ColumnBuilder;
  69 import org.openjdk.jmc.ui.column.ColumnManager;
  70 import org.openjdk.jmc.ui.column.IColumn;
  71 import org.openjdk.jmc.ui.column.TableSettings;
  72 import org.openjdk.jmc.ui.misc.BackgroundFractionDrawer;
  73 import org.openjdk.jmc.ui.misc.DelegatingLabelProvider;
  74 import org.openjdk.jmc.ui.misc.OptimisticComparator;
  75 
  76 /**
  77  * A table containing Flight Recorder event durations at various pre-defined percentiles.
  78  * Each row in the table contains values for a different percentile, and the columns contain
  79  * series of durations and event counts.
  80  *
  81  * @see DurationPercentileTableBuilder
  82  */
  83 public class DurationPercentileTable {
  84 
  85         public static final String TABLE_NAME = "DurationPercentileTable"; //$NON-NLS-1$
  86         private static final String COL_ID_PERCENTILE = TABLE_NAME + ".percentile"; //$NON-NLS-1$
  87 
  88         private static final IQuantity[] PERCENTILES = {
  89                         UnitLookup.NUMBER_UNITY.quantity(0.0),
  90                         UnitLookup.NUMBER_UNITY.quantity(90.0),
  91                         UnitLookup.NUMBER_UNITY.quantity(99.0),
  92                         UnitLookup.NUMBER_UNITY.quantity(99.9),
  93                         UnitLookup.NUMBER_UNITY.quantity(99.99),
  94                         UnitLookup.NUMBER_UNITY.quantity(99.999),
  95                         UnitLookup.NUMBER_UNITY.quantity(100.0),
  96         };
  97 
  98         private final DurationPercentileAggregator[] aggregators; // Correspond to column order
  99         private final ColumnManager manager;
 100 
 101         private DurationPercentileTable(ColumnManager manager, DurationPercentileAggregator[] aggregators) {
 102                 this.manager = manager;
 103                 this.aggregators = aggregators;
 104         }
 105 
 106         /**
 107          * Builder class that is the sole means of creating {@link DurationPercentileTable} instances.
 108          */
 109         public static class DurationPercentileTableBuilder {
 110 
 111                 private final List<IColumn> columns;
 112                 private final List<DurationPercentileAggregator> aggregators;
 113 
 114                 public DurationPercentileTableBuilder() {
 115                         this.columns = new ArrayList<>();
 116                         this.aggregators = new ArrayList<>();
 117                 }
 118 
 119                 /**
 120                  * Adds a data series to this table, corresponding to an event type with a duration
 121                  * associated with it. Calling this method adds two columns to the resulting table.
 122                  * The first column contains duration values for the event at different percentiles,
 123                  * and the second column contains the number of events with duration <= the duration
 124                  * at that percentile.
 125                  *
 126                  * @param durationColId - the ID to be used for the duration column of this series
 127                  * @param durationColName - the user-visible name to appear for the duration column header
 128                  * @param countColId - the ID to be used for the event count column of this series
 129                  * @param countColName - the user-visible name to appear for the event count column header
 130                  * @param typeId - the event type ID used to match events belonging to this series
 131                  */
 132                 public void addSeries(String durationColId, String durationColName,
 133                                 String countColId, String countColName, String typeId) {
 134                         IColumn column = new ColumnBuilder(durationColName, durationColId, new ValueAccessor(durationColId)).style(SWT.RIGHT).build();
 135                         columns.add(column);
 136 
 137                         Function<DurationPercentileTableRow, IQuantity> fractionFunc = row -> row.getCountFraction(countColId);
 138                         column = new ColumnBuilder(countColName, countColId, new ValueAccessor(countColId)).style(SWT.RIGHT)
 139                                         .columnDrawer(BackgroundFractionDrawer.unchecked(fractionFunc)).build();
 140                         columns.add(column);
 141 
 142                         DurationPercentileAggregator aggregator = new DurationPercentileAggregator(typeId, durationColId, countColId);
 143                         aggregators.add(aggregator);
 144                 }
 145 
 146                 /**
 147                  * Builds the {@link DurationPercentileTable} after all series have been added.
 148                  * Calling this method results in the creation of the underlying {@link TableViewer}.
 149                  * Further changes to this builder will not affect the returned table.
 150                  * @param parent - the parent SWT composite that will contain this table
 151                  * @param ts - settings to adjust various attributes of the created table
 152                  * @return a fully constructed {@link DurationPercentileTable} with no data
 153                  */
 154                 public DurationPercentileTable build(Composite parent, TableSettings ts) {
 155                         TableViewer tableViewer = new TableViewer(parent,
 156                                         SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
 157                         tableViewer.getControl().setData("name", TABLE_NAME); //$NON-NLS-1$
 158                         tableViewer.setContentProvider(ArrayContentProvider.getInstance());
 159                         ColumnViewerToolTipSupport.enableFor(tableViewer);
 160                         if (UIPlugin.getDefault().getAccessibilityMode()) {
 161                                 FocusTracker.enableFocusTracking(tableViewer.getTable());
 162                         }
 163 
 164                         List<IColumn> columns = new ArrayList<>();
 165                         ItemHistogram.KeyLabelProvider keyLP = new ItemHistogram.KeyLabelProvider(UnitLookup.NUMBER);
 166                         PercentileAccessor cellAccessor = new PercentileAccessor();
 167                         OptimisticComparator comp = new OptimisticComparator(cellAccessor, keyLP);
 168                         columns.add(new ColumnBuilder(Messages.DurationPercentileTable_PERCENTILE_COL_NAME, COL_ID_PERCENTILE,
 169                                         new DelegatingLabelProvider(keyLP, cellAccessor)).comparator(comp).build());
 170                         columns.addAll(this.columns);
 171 
 172                         ColumnManager manager = ColumnManager.build(tableViewer, columns, ts);
 173                         DurationPercentileAggregator[] aggregatorsCopy = aggregators.toArray(new DurationPercentileAggregator[aggregators.size()]);
 174                         return new DurationPercentileTable(manager, aggregatorsCopy);
 175                 }
 176         }
 177 
 178         /**
 179          * Updates the data in this table with events from the item collection.
 180          * Calling this method stores the input data into a histogram, which is then
 181          * used to generate duration values at various percentiles.
 182          *
 183          * @param itemCol - a collection of events to use as input for this table
 184          */
 185         public void update(IItemCollection itemCol) {
 186                 // Add the value of each aggregate to our data model
 187                 DurationPercentileTableModel model = new DurationPercentileTableModel(itemCol);
 188                 Arrays.stream(aggregators).parallel().forEach(model::addAggregate);
 189 
 190                 // Build rows for each percentile and set in the table
 191                 List<DurationPercentileTableRow> rows = model.buildRows();
 192                 updateColumnVisibilty(rows.get(0));
 193                 manager.getViewer().setInput(rows);
 194         }
 195 
 196         private void updateColumnVisibilty(DurationPercentileTableRow row) {
 197                 manager.getColumnStates().forEach(c -> {
 198                         String id = c.getColumn().getId();
 199                         if (!COL_ID_PERCENTILE.equals(id)) { // Percentile column always shown
 200                                 boolean shouldShow = row.hasValue(id);
 201                                 // Don't show if already shown, will duplicate column
 202                                 if (shouldShow != c.isVisible()) {
 203                                         manager.setColumnHidden(id, !shouldShow);
 204                                 }
 205                         }
 206                 });
 207         }
 208 
 209         /**
 210          * Get the {@link ColumnManager} responsible for the underlying {@link TableViewer}.
 211          * @return the manager
 212          */
 213         public ColumnManager getManager() {
 214                 return manager;
 215         }
 216 
 217         /**
 218          * Gets a collection of items whose duration is at least as long as the percentile value
 219          * in the currently selected row.
 220          * @return the collection of matching items
 221          */
 222         public IItemCollection getSelectedItems() {
 223                 IStructuredSelection selection = manager.getViewer().getStructuredSelection();
 224                 Object firstSelection = selection.getFirstElement();
 225                 if (firstSelection instanceof DurationPercentileTableRow) {
 226                         DurationPercentileTableRow row = (DurationPercentileTableRow) firstSelection;
 227                         return row.getItemsForRow(aggregators);
 228                 }
 229                 return null;
 230         }
 231 
 232         private static class PercentileAccessor implements IMemberAccessor<IQuantity, Object> {
 233 
 234                 @Override
 235                 public IQuantity getMember(Object inObject) {
 236                         if (inObject instanceof DurationPercentileTableRow) {
 237                                 return ((DurationPercentileTableRow) inObject).getPercentile();
 238                         }
 239                         return null;
 240                 }
 241 
 242         }
 243 
 244         private static class ValueAccessor implements IMemberAccessor<IQuantity, DurationPercentileTableRow> {
 245 
 246                 private final String columnId;
 247 
 248                 public ValueAccessor(String columnId) {
 249                         this.columnId = columnId;
 250                 }
 251 
 252                 @Override
 253                 public IQuantity getMember(DurationPercentileTableRow inObject) {
 254                         return inObject.getValue(columnId);
 255                 }
 256 
 257         }
 258 
 259         /**
 260          * Aggregator that inserts event durations into a histogram.
 261          */
 262         private static class DurationPercentileAggregator extends AggregatorBase<Map<IQuantity, Map<String, IQuantity>>, DurationItemConsumer> {
 263 
 264                 private final DurationHdrHistogram histogram;
 265                 private final String typeId;
 266                 private final String durationColId;
 267                 private final String countColId;
 268 
 269                 /**
 270                  * Creates a new aggregator.
 271                  * @param typeId - type ID used to match events
 272                  * @param durationColId - the column ID for the duration column of this series
 273                  * @param countColId - the column ID for the item count column of this series
 274                  */
 275                 public DurationPercentileAggregator(String typeId, String durationColId, String countColId) {
 276                         super(null, null, UnitLookup.UNKNOWN);
 277                         this.histogram = new DurationHdrHistogram();
 278                         this.typeId = typeId;
 279                         this.durationColId = durationColId;
 280                         this.countColId = countColId;
 281                 }
 282 
 283                 @Override
 284                 public boolean acceptType(IType<IItem> type) {
 285                         return typeId.equals(type.getIdentifier());
 286                 }
 287 
 288                 @Override
 289                 public DurationItemConsumer newItemConsumer(IType<IItem> itemType) {
 290                         return new DurationItemConsumer(histogram, JfrAttributes.DURATION.getAccessor(itemType));
 291                 }
 292 
 293                 @Override
 294                 public Map<IQuantity, Map<String, IQuantity>> getValue(Iterator<DurationItemConsumer> source) {
 295                         while (source.hasNext()) {
 296                                 source.next();
 297                         }
 298 
 299                         Map<IQuantity, Map<String, IQuantity>> result = new HashMap<>();
 300                         for (IQuantity percentile : PERCENTILES) {
 301                                 Map<String, IQuantity> colValues = new HashMap<>();
 302                                 // Only add columns to model if there is data to show
 303                                 if (!histogram.isEmpty()) {
 304                                         Pair<IQuantity, IQuantity> data = histogram.getDurationAndCountAtPercentile(percentile);
 305 
 306                                         colValues.put(durationColId, data.left);
 307                                         colValues.put(countColId, data.right);
 308                                 }
 309                                 result.put(percentile, colValues);
 310                         }
 311                         return result;
 312                 }
 313 
 314                 /**
 315                  * @return the number of items stored in this aggregator's histogram
 316                  */
 317                 public IQuantity getItemCount() {
 318                         long total = histogram.getTotalCount();
 319                         return UnitLookup.NUMBER_UNITY.quantity(total);
 320                 }
 321 
 322                 /**
 323                  * @return the ID for the duration column using this aggregator
 324                  */
 325                 public String getDurationColId() {
 326                         return durationColId;
 327                 }
 328 
 329                 /**
 330                  * @return the ID for the item count column using this aggregator
 331                  */
 332                 public String getCountColId() {
 333                         return countColId;
 334                 }
 335 
 336                 /**
 337                  * @return the type ID used to match items accepted by this aggregator
 338                  */
 339                 public String getTypeId() {
 340                         return typeId;
 341                 }
 342 
 343                 /**
 344                  * @param duration - a {@link UnitLookup#TIMESPAN} quantity
 345                  * @return a lower bound on values considered equivalent by this
 346                  * aggregator's underlying histogram
 347                  */
 348                 public IQuantity getLowestEquivalentDuration(IQuantity duration) {
 349                         return histogram.getLowestEquivalentDuration(duration);
 350                 }
 351 
 352                 /**
 353                  * Resets this aggregator's histogram to its initial state
 354                  */
 355                 public void resetHistogram() {
 356                         histogram.reset();
 357                 }
 358 
 359         }
 360 
 361         /**
 362          * A data model representing the content to be displayed in the {@link DurationPercentileTable}.
 363          */
 364         private static class DurationPercentileTableModel {
 365 
 366                 private final IItemCollection items;
 367                 private final Map<IQuantity, Map<String, IQuantity>> valuesByPercentile;
 368                 private final Map<String, IQuantity> itemTotals;
 369 
 370                 public DurationPercentileTableModel(IItemCollection items) {
 371                         this.items = items;
 372                         this.valuesByPercentile = new ConcurrentHashMap<>();
 373                         this.itemTotals = new ConcurrentHashMap<>();
 374                 }
 375 
 376                 /**
 377                  * Computes the aggregate of this model's items and adds the results to this model.
 378                  * @param aggregator - the aggregator to use
 379                  */
 380                 public void addAggregate(DurationPercentileAggregator aggregator) {
 381                         aggregator.resetHistogram();
 382 
 383                         Map<IQuantity, Map<String, IQuantity>> newData = items.getAggregate(aggregator);
 384                         addData(newData);
 385 
 386                         String countCol = aggregator.getCountColId();
 387                         IQuantity itemCount = aggregator.getItemCount();
 388                         itemTotals.put(countCol, itemCount);
 389                 }
 390 
 391                 private void addData(Map<IQuantity, Map<String, IQuantity>> newValues) {
 392                         newValues.forEach((key, val) -> valuesByPercentile.merge(key, val, (oldVal, newVal) -> {
 393                                 oldVal.putAll(newVal);
 394                                 return oldVal;
 395                         }));
 396                 }
 397 
 398                 /**
 399                  * Builds a list of table rows from the data in this model, suitable as input
 400                  * to the {@link DurationPercentileTable}'s {@link ColumnViewer}.
 401                  * @return the list of rows
 402                  */
 403                 public List<DurationPercentileTableRow> buildRows() {
 404                         List<DurationPercentileTableRow> rows = new ArrayList<>();
 405                         for (IQuantity percentile : PERCENTILES) {
 406                                 DurationPercentileTableRow row = new DurationPercentileTableRow(percentile,
 407                                                 valuesByPercentile.get(percentile), itemTotals, items);
 408                                 rows.add(row);
 409                         }
 410                         return rows;
 411                 }
 412 
 413         }
 414 
 415         /**
 416          * Roughly equivalent to a row in the table, containing the percentile and list of
 417          * associated quantities in column order.
 418          */
 419         private static class DurationPercentileTableRow {
 420 
 421                 private final IQuantity percentile;
 422                 private final Map<String, IQuantity> valuesByColId;
 423                 private final Map<String, IQuantity> totalsById;
 424                 private final IItemCollection items;
 425 
 426                 public DurationPercentileTableRow(IQuantity percentile, Map<String, IQuantity> values,
 427                                 Map<String, IQuantity> totals, IItemCollection items) {
 428                         this.percentile = percentile;
 429                         this.valuesByColId = values;
 430                         this.totalsById = totals;
 431                         this.items = items;
 432                 }
 433 
 434                 public IQuantity getPercentile() {
 435                         return percentile;
 436                 }
 437 
 438                 public IQuantity getValue(String columnId) {
 439                         return valuesByColId.get(columnId);
 440                 }
 441 
 442                 public boolean hasValue(String columnId) {
 443                         return valuesByColId.containsKey(columnId);
 444                 }
 445 
 446                 /**
 447                  * Calculates the fraction of items in this row, compared to the total
 448                  * number of items in the series.
 449                  * @param columnId - the ID of the item count column
 450                  * @return a fraction quantity between 0 and 1
 451                  */
 452                 public IQuantity getCountFraction(String columnId) {
 453                         IQuantity count = valuesByColId.get(columnId);
 454                         IQuantity total = totalsById.get(columnId);
 455                         double fraction = 0.0;
 456                         if (count != null && total != null && total.longValue() > 0) {
 457                                 fraction = count.doubleValue() / total.doubleValue();
 458                         }
 459                         return UnitLookup.NUMBER_UNITY.quantity(fraction);
 460                 }
 461 
 462                 /**
 463                  * Computes the collection of items that have duration at least as long as the
 464                  * corresponding values in this row.
 465                  * @param aggregators - an array of aggregators that produced the content of this row
 466                  * @return the matching items
 467                  */
 468                 public IItemCollection getItemsForRow(DurationPercentileAggregator[] aggregators) {
 469                         // Select all events with matching Type ID and duration greater or equal to the value
 470                         // for the selected percentile in the histogram, subject to the histogram's precision.
 471                         IItemFilter filter = Arrays.stream(aggregators).parallel().filter(a -> hasValue(a.getDurationColId()))
 472                                         .map(a -> ItemFilters.and(ItemFilters.type(a.getTypeId()),
 473                                                         ItemFilters.moreOrEqual(JfrAttributes.DURATION,
 474                                                                         a.getLowestEquivalentDuration(getValue(a.getDurationColId())))))
 475                                         .reduce(ItemFilters::or).orElse(ItemFilters.none());
 476                         return items.apply(filter);
 477                 }
 478 
 479         }
 480 
 481 }