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 }