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.ui.pages;
  34 
  35 import static org.openjdk.jmc.common.item.Aggregators.max;
  36 import static org.openjdk.jmc.common.item.Aggregators.min;
  37 
  38 import java.util.ArrayList;
  39 import java.util.Arrays;
  40 import java.util.List;
  41 import java.util.stream.Collectors;
  42 import java.util.stream.Stream;
  43 
  44 import org.eclipse.jface.action.IAction;
  45 import org.eclipse.jface.action.Separator;
  46 import org.eclipse.jface.resource.ImageDescriptor;
  47 import org.eclipse.swt.SWT;
  48 import org.eclipse.swt.widgets.Composite;
  49 import org.eclipse.ui.forms.widgets.FormToolkit;
  50 import org.openjdk.jmc.common.IMCThread;
  51 import org.openjdk.jmc.common.IState;
  52 import org.openjdk.jmc.common.IWritableState;
  53 import org.openjdk.jmc.common.item.Aggregators;
  54 import org.openjdk.jmc.common.item.IAggregator;
  55 import org.openjdk.jmc.common.item.IItemCollection;
  56 import org.openjdk.jmc.common.item.IItemFilter;
  57 import org.openjdk.jmc.common.item.ItemFilters;
  58 import org.openjdk.jmc.common.unit.IQuantity;
  59 import org.openjdk.jmc.common.unit.IRange;
  60 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  61 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  62 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  63 import org.openjdk.jmc.flightrecorder.rules.util.JfrRuleTopics;
  64 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
  65 import org.openjdk.jmc.flightrecorder.ui.IDataPageFactory;
  66 import org.openjdk.jmc.flightrecorder.ui.IDisplayablePage;
  67 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
  68 import org.openjdk.jmc.flightrecorder.ui.IPageDefinition;
  69 import org.openjdk.jmc.flightrecorder.ui.IPageUI;
  70 import org.openjdk.jmc.flightrecorder.ui.StreamModel;
  71 import org.openjdk.jmc.flightrecorder.ui.common.AbstractDataPage;
  72 import org.openjdk.jmc.flightrecorder.ui.common.FlavorSelector.FlavorSelectorState;
  73 import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants;
  74 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram;
  75 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.HistogramSelection;
  76 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.ItemHistogramBuilder;
  77 import org.openjdk.jmc.flightrecorder.ui.common.ItemRow;
  78 import org.openjdk.jmc.flightrecorder.ui.common.ThreadGraphLanes;
  79 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  80 import org.openjdk.jmc.ui.UIPlugin;
  81 import org.openjdk.jmc.ui.charts.IXDataRenderer;
  82 import org.openjdk.jmc.ui.charts.QuantitySpanRenderer;
  83 import org.openjdk.jmc.ui.charts.RendererToolkit;
  84 import org.openjdk.jmc.ui.column.ColumnManager.SelectionState;
  85 import org.openjdk.jmc.ui.column.TableSettings;
  86 import org.openjdk.jmc.ui.handlers.ActionToolkit;
  87 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  88 
  89 public class ThreadsPage extends AbstractDataPage {
  90 
  91         public static class ThreadsPageFactory implements IDataPageFactory {
  92 
  93                 @Override
  94                 public String getName(IState state) {
  95                         return Messages.ThreadsPage_NAME;
  96                 }
  97 
  98                 @Override
  99                 public String[] getTopics(IState state) {
 100                         return new String[] {JfrRuleTopics.THREADS_TOPIC};
 101                 }
 102 
 103                 @Override
 104                 public ImageDescriptor getImageDescriptor(IState state) {
 105                         return FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.PAGE_THREADS);
 106                 }
 107 
 108                 @Override
 109                 public IDisplayablePage createPage(IPageDefinition definition, StreamModel items, IPageContainer editor) {
 110                         return new ThreadsPage(definition, items, editor);
 111                 }
 112 
 113         }
 114 
 115         private static final String THREAD_START_COL = "threadStart"; //$NON-NLS-1$
 116         private static final String THREAD_END_COL = "threadEnd"; //$NON-NLS-1$
 117         private static final String THREAD_DURATION_COL = "threadDuration"; //$NON-NLS-1$
 118         private static final String THREAD_LANE = "threadLane"; //$NON-NLS-1$
 119 
 120         private static final IItemFilter pageFilter = ItemFilters.hasAttribute(JfrAttributes.EVENT_THREAD);
 121         private static final ItemHistogramBuilder HISTOGRAM = new ItemHistogramBuilder();
 122 
 123         static {
 124                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_GROUP_NAME);
 125                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_ID);
 126                 HISTOGRAM.addColumn(THREAD_START_COL,
 127                                 min(Messages.JavaApplicationPage_COLUMN_THREAD_START,
 128                                                 Messages.JavaApplicationPage_COLUMN_THREAD_START_DESC, JdkTypeIDs.JAVA_THREAD_START,
 129                                                 JfrAttributes.EVENT_TIMESTAMP));
 130                 /*
 131                  * Will order empty cells before first end time.
 132                  * 
 133                  * It should be noted that no event (empty column cell) is considered less than all values
 134                  * (this is common for all columns), which causes the column to sort threads without end
 135                  * time (indicating that the thread ended after the end of the recording) is ordered before
 136                  * the thread that ended first. While this is not optimal, we decided to accept it as it's
 137                  * not obviously better to have this particular column ordering empty cells last in contrast
 138                  * to all other columns.
 139                  */
 140                 HISTOGRAM.addColumn(THREAD_END_COL,
 141                                 max(Messages.JavaApplicationPage_COLUMN_THREAD_END, Messages.JavaApplicationPage_COLUMN_THREAD_END_DESC,
 142                                                 JdkTypeIDs.JAVA_THREAD_END, JfrAttributes.EVENT_TIMESTAMP));
 143                 HISTOGRAM.addColumn(THREAD_DURATION_COL, ic -> {
 144                         IQuantity threadStart = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_START))
 145                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.min(JfrAttributes.EVENT_TIMESTAMP));
 146                         IQuantity threadEnd = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_END))
 147                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.max(JfrAttributes.EVENT_TIMESTAMP));
 148                         if (threadStart != null && threadEnd != null) {
 149                                 return threadEnd.subtract(threadStart);
 150                         }
 151                         return null;
 152                 }, Messages.JavaApplicationPage_COLUMN_THREAD_DURATION,
 153                                 Messages.JavaApplicationPage_COLUMN_THREAD_DURATION_DESC);
 154         }
 155 
 156         private class ThreadsPageUi extends ChartAndTableUI {
 157                 private static final String THREADS_TABLE_FILTER = "threadsTableFilter"; //$NON-NLS-1$
 158                 private static final String HIDE_THREAD = "hideThread"; //$NON-NLS-1$
 159                 private static final String RESET_CHART = "resetChart"; //$NON-NLS-1$
 160                 private Boolean isChartMenuActionsInit;
 161                 private Boolean isChartModified;
 162                 private Boolean reloadThreads;
 163                 private IAction hideThreadAction;
 164                 private IAction resetChartAction;
 165                 private List<IXDataRenderer> threadRows;
 166                 private MCContextMenuManager mm;
 167                 private ThreadGraphLanes lanes;
 168 
 169                 ThreadsPageUi(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 170                         super(pageFilter, getDataSource(), parent, toolkit, editor, state, getName(), pageFilter, getIcon(),
 171                                         flavorSelectorState);
 172                         mm = (MCContextMenuManager) chartCanvas.getContextMenu();
 173                         sash.setOrientation(SWT.HORIZONTAL);
 174                         addActionsToContextMenu(mm);
 175                         // FIXME: The lanes field is initialized by initializeChartConfiguration which is called by the super constructor. This is too indirect for SpotBugs to resolve and should be simplified.
 176                         lanes.updateContextMenu(mm, false);
 177 
 178                         form.getToolBarManager()
 179                                         .add(ActionToolkit.action(() -> lanes.openEditLanesDialog(mm, false), Messages.ThreadsPage_EDIT_LANES,
 180                                                         FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_LANES_EDIT)));
 181                         form.getToolBarManager().update(true);
 182                         chartLegend.getControl().dispose();
 183                         buildChart();
 184                         table.getManager().setSelectionState(histogramSelectionState);
 185                         tableFilterComponent.loadState(state.getChild(THREADS_TABLE_FILTER));
 186                         chart.setVisibleRange(visibleRange.getStart(), visibleRange.getEnd());
 187                         onFilterChange(tableFilter);
 188                 }
 189 
 190                 /**
 191                  * Hides a thread from the chart and rebuilds the chart
 192                  */
 193                 private void hideThread(Object thread) {
 194                         if (this.threadRows != null && this.threadRows.size() > 0 && thread instanceof IMCThread) {
 195                                 int index = indexOfThread(thread);
 196                                 if (index != -1) {
 197                                         this.threadRows.remove(index);
 198                                         this.reloadThreads = false;
 199                                         buildChart();
 200                                         if (!this.isChartModified) {
 201                                                 this.isChartModified = true;
 202                                                 setResetChartActionEnablement(true);
 203                                         }
 204                                 }
 205                                 if (this.threadRows.size() == 0) {
 206                                         setHideThreadActionEnablement(false);
 207                                 }
 208                         }
 209                 }
 210 
 211                 /**
 212                  * Locates the index of the target Thread in the current selection list
 213                  *
 214                  * @param thread
 215                  *            the thread of interest
 216                  * @return the index of the thread in the current selection, or -1 if not found
 217                  */
 218                 private int indexOfThread(Object thread) {
 219                         for (int i = 0; i < this.threadRows.size() && thread != null; i++) {
 220                                 if (this.threadRows.get(i) instanceof QuantitySpanRenderer) {
 221                                         if (thread.equals(((QuantitySpanRenderer) this.threadRows.get(i)).getData())) {
 222                                                 return i;
 223                                         }
 224                                 }
 225                         }
 226                         return -1;
 227                 }
 228 
 229                 /**
 230                  * Update the context menu to include actions to hide threads and reset the chart
 231                  */
 232                 private void addActionsToContextMenu(MCContextMenuManager mm) {
 233                         mm.add(new Separator());
 234 
 235                         IAction hideThreadAction = ActionToolkit.action(() -> this.hideThread(chartCanvas.getHoveredItemData()),
 236                                         Messages.ThreadsPage_HIDE_THREAD_ACTION,
 237                                         UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_DELETE));
 238                         hideThreadAction.setId(HIDE_THREAD);
 239                         this.hideThreadAction = hideThreadAction;
 240                         mm.add(hideThreadAction);
 241 
 242                         IAction resetChartAction = ActionToolkit.action(() -> this.resetChartToSelection(),
 243                                         Messages.ThreadsPage_RESET_CHART_TO_SELECTION_ACTION,
 244                                         UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_REFRESH));
 245                         resetChartAction.setId(RESET_CHART);
 246                         resetChartAction.setEnabled(this.isChartModified);
 247                         this.resetChartAction = resetChartAction;
 248                         mm.add(resetChartAction);
 249 
 250                         this.isChartMenuActionsInit = true;
 251                 }
 252 
 253                 /**
 254                  * Redraws the chart, and disables the reset chart menu action
 255                  */
 256                 private void resetChartToSelection() {
 257                         buildChart();
 258                         this.isChartModified = false;
 259                         setResetChartActionEnablement(false);
 260                         setHideThreadActionEnablement(true);
 261                 }
 262 
 263                 private void setHideThreadActionEnablement(Boolean enabled) {
 264                         this.hideThreadAction.setEnabled(enabled);
 265                 }
 266                 private void setResetChartActionEnablement(Boolean enabled) {
 267                         this.resetChartAction.setEnabled(enabled);
 268                 }
 269 
 270                 @Override
 271                 protected ItemHistogram buildHistogram(Composite parent, IState state) {
 272                         ItemHistogram build = HISTOGRAM.buildWithoutBorder(parent, JfrAttributes.EVENT_THREAD,
 273                                         TableSettings.forState(state));
 274                         return build;
 275                 }
 276 
 277                 @Override
 278                 protected IXDataRenderer getChartRenderer(IItemCollection itemsInTable, HistogramSelection tableSelection) {
 279                         List<IXDataRenderer> rows = new ArrayList<>();
 280 
 281                         IItemCollection selectedItems;
 282                         HistogramSelection selection;
 283                         if (tableSelection.getRowCount() == 0) {
 284                                 selectedItems = itemsInTable;
 285                                 selection = table.getAllRows();
 286                         } else {
 287                                 selectedItems = tableSelection.getItems();
 288                                 selection = tableSelection;
 289                         }
 290                         boolean useDefaultSelection = rows.size() > 1;
 291                         if (lanes.getLaneDefinitions().stream().anyMatch(a -> a.isEnabled()) && selection.getRowCount() > 0) {
 292                                 if (this.reloadThreads) {
 293                                         this.threadRows = selection
 294                                                         .getSelectedRows((object, items) -> lanes.buildThreadRenderer(object, items))
 295                                                         .collect(Collectors.toList());
 296                                         this.isChartModified = false;
 297                                         if (this.isChartMenuActionsInit) {
 298                                                 setResetChartActionEnablement(false);
 299                                                 setHideThreadActionEnablement(true);
 300                                         }
 301                                 } else {
 302                                         this.reloadThreads = true;
 303                                 }
 304 
 305                                 double threadsWeight = Math.sqrt(threadRows.size()) * 0.15;
 306                                 double otherRowWeight = Math.max(threadsWeight * 0.1, (1 - threadsWeight) / rows.size());
 307                                 List<Double> weights = Stream
 308                                                 .concat(Stream.generate(() -> otherRowWeight).limit(rows.size()), Stream.of(threadsWeight))
 309                                                 .collect(Collectors.toList());
 310                                 rows.add(RendererToolkit.uniformRows(this.threadRows));
 311                                 useDefaultSelection = true;
 312                                 rows = Arrays.asList(RendererToolkit.weightedRows(rows, weights));
 313                         }
 314                         IXDataRenderer root = rows.size() == 1 ? rows.get(0) : RendererToolkit.uniformRows(rows);
 315                         // We don't use the default selection when there is only one row. This is to get the correct payload.
 316                         return useDefaultSelection ? new ItemRow(root, selectedItems.apply(lanes.getEnabledLanesFilter())) : root;
 317                 }
 318 
 319                 @Override
 320                 protected void onFilterChange(IItemFilter filter) {
 321                         super.onFilterChange(filter);
 322                         tableFilter = filter;
 323                 }
 324 
 325                 @Override
 326                 public void saveTo(IWritableState state) {
 327                         super.saveTo(state);
 328                         tableFilterComponent.saveState(state.createChild(THREADS_TABLE_FILTER));
 329                         saveToLocal();
 330                 }
 331 
 332                 private void saveToLocal() {
 333                         flavorSelectorState = flavorSelector.getFlavorSelectorState();
 334                         histogramSelectionState = table.getManager().getSelectionState();
 335                         visibleRange = chart.getVisibleRange();
 336                 }
 337 
 338                 @Override
 339                 protected List<IAction> initializeChartConfiguration(IState state) {
 340                         this.isChartMenuActionsInit = false;
 341                         this.isChartModified = false;
 342                         this.reloadThreads = true;
 343                         lanes = new ThreadGraphLanes(() -> getDataSource(), () -> buildChart());
 344                         return lanes.initializeChartConfiguration(Stream.of(state.getChildren(THREAD_LANE)));
 345                 }
 346         }
 347 
 348         private FlavorSelectorState flavorSelectorState;
 349         private SelectionState histogramSelectionState;
 350         private IItemFilter tableFilter;
 351         private IRange<IQuantity> visibleRange;
 352 
 353         public ThreadsPage(IPageDefinition definition, StreamModel model, IPageContainer editor) {
 354                 super(definition, model, editor);
 355                 visibleRange = editor.getRecordingRange();
 356         }
 357 
 358         @Override
 359         public IPageUI display(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 360                 return new ThreadsPageUi(parent, toolkit, editor, state);
 361         }
 362 
 363 }