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.IState;
  51 import org.openjdk.jmc.common.IWritableState;
  52 import org.openjdk.jmc.common.item.Aggregators;
  53 import org.openjdk.jmc.common.item.IAggregator;
  54 import org.openjdk.jmc.common.item.IItemCollection;
  55 import org.openjdk.jmc.common.item.IItemFilter;
  56 import org.openjdk.jmc.common.item.ItemFilters;
  57 import org.openjdk.jmc.common.unit.IQuantity;
  58 import org.openjdk.jmc.common.unit.IRange;
  59 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  60 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  61 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  62 import org.openjdk.jmc.flightrecorder.rules.util.JfrRuleTopics;
  63 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
  64 import org.openjdk.jmc.flightrecorder.ui.IDataPageFactory;
  65 import org.openjdk.jmc.flightrecorder.ui.IDisplayablePage;
  66 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
  67 import org.openjdk.jmc.flightrecorder.ui.IPageDefinition;
  68 import org.openjdk.jmc.flightrecorder.ui.IPageUI;
  69 import org.openjdk.jmc.flightrecorder.ui.StreamModel;
  70 import org.openjdk.jmc.flightrecorder.ui.common.AbstractDataPage;
  71 import org.openjdk.jmc.flightrecorder.ui.common.FlavorSelector.FlavorSelectorState;
  72 import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants;
  73 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram;
  74 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.HistogramSelection;
  75 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.ItemHistogramBuilder;
  76 import org.openjdk.jmc.flightrecorder.ui.common.ItemRow;
  77 import org.openjdk.jmc.flightrecorder.ui.common.ThreadGraphLanes;
  78 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  79 import org.openjdk.jmc.ui.charts.IXDataRenderer;
  80 import org.openjdk.jmc.ui.charts.RendererToolkit;
  81 import org.openjdk.jmc.ui.column.ColumnManager.SelectionState;
  82 import org.openjdk.jmc.ui.column.TableSettings;
  83 import org.openjdk.jmc.ui.handlers.ActionToolkit;
  84 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  85 
  86 public class ThreadsPage extends AbstractDataPage {
  87 
  88         public static class ThreadsPageFactory implements IDataPageFactory {
  89 
  90                 @Override
  91                 public String getName(IState state) {
  92                         return Messages.ThreadsPage_NAME;
  93                 }
  94 
  95                 @Override
  96                 public String[] getTopics(IState state) {
  97                         return new String[] {JfrRuleTopics.THREADS_TOPIC};
  98                 }
  99 
 100                 @Override
 101                 public ImageDescriptor getImageDescriptor(IState state) {
 102                         return FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.PAGE_THREADS);
 103                 }
 104 
 105                 @Override
 106                 public IDisplayablePage createPage(IPageDefinition definition, StreamModel items, IPageContainer editor) {
 107                         return new ThreadsPage(definition, items, editor);
 108                 }
 109 
 110         }
 111 
 112         private static final String THREAD_START_COL = "threadStart"; //$NON-NLS-1$
 113         private static final String THREAD_END_COL = "threadEnd"; //$NON-NLS-1$
 114         private static final String THREAD_DURATION_COL = "threadDuration"; //$NON-NLS-1$
 115         private static final String THREAD_LANE = "threadLane"; //$NON-NLS-1$
 116 
 117         private static final IItemFilter pageFilter = ItemFilters.hasAttribute(JfrAttributes.EVENT_THREAD);
 118         private static final ItemHistogramBuilder HISTOGRAM = new ItemHistogramBuilder();
 119 
 120         static {
 121                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_GROUP_NAME);
 122                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_ID);
 123                 HISTOGRAM.addColumn(THREAD_START_COL,
 124                                 min(Messages.JavaApplicationPage_COLUMN_THREAD_START,
 125                                                 Messages.JavaApplicationPage_COLUMN_THREAD_START_DESC, JdkTypeIDs.JAVA_THREAD_START,
 126                                                 JfrAttributes.EVENT_TIMESTAMP));
 127                 /*
 128                  * Will order empty cells before first end time.
 129                  * 
 130                  * It should be noted that no event (empty column cell) is considered less than all values
 131                  * (this is common for all columns), which causes the column to sort threads without end
 132                  * time (indicating that the thread ended after the end of the recording) is ordered before
 133                  * the thread that ended first. While this is not optimal, we decided to accept it as it's
 134                  * not obviously better to have this particular column ordering empty cells last in contrast
 135                  * to all other columns.
 136                  */
 137                 HISTOGRAM.addColumn(THREAD_END_COL,
 138                                 max(Messages.JavaApplicationPage_COLUMN_THREAD_END, Messages.JavaApplicationPage_COLUMN_THREAD_END_DESC,
 139                                                 JdkTypeIDs.JAVA_THREAD_END, JfrAttributes.EVENT_TIMESTAMP));
 140                 HISTOGRAM.addColumn(THREAD_DURATION_COL, ic -> {
 141                         IQuantity threadStart = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_START))
 142                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.min(JfrAttributes.EVENT_TIMESTAMP));
 143                         IQuantity threadEnd = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_END))
 144                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.max(JfrAttributes.EVENT_TIMESTAMP));
 145                         if (threadStart != null && threadEnd != null) {
 146                                 return threadEnd.subtract(threadStart);
 147                         }
 148                         return null;
 149                 }, Messages.JavaApplicationPage_COLUMN_THREAD_DURATION,
 150                                 Messages.JavaApplicationPage_COLUMN_THREAD_DURATION_DESC);
 151         }
 152 
 153         private class ThreadsPageUi extends ChartAndTableUI {
 154                 private static final String THREADS_TABLE_FILTER = "threadsTableFilter"; //$NON-NLS-1$
 155                 private ThreadGraphLanes lanes;
 156                 private MCContextMenuManager mm;
 157 
 158                 ThreadsPageUi(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 159                         super(pageFilter, getDataSource(), parent, toolkit, editor, state, getName(), pageFilter, getIcon(),
 160                                         flavorSelectorState);
 161                         mm = (MCContextMenuManager) chartCanvas.getContextMenu();
 162                         sash.setOrientation(SWT.HORIZONTAL);
 163                         mm.add(new Separator());
 164                         // 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.
 165                         lanes.updateContextMenu(mm, false);
 166 
 167                         form.getToolBarManager()
 168                                         .add(ActionToolkit.action(() -> lanes.openEditLanesDialog(mm, false), Messages.ThreadsPage_EDIT_LANES,
 169                                                         FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_LANES_EDIT)));
 170                         form.getToolBarManager().update(true);
 171                         chartLegend.getControl().dispose();
 172                         buildChart();
 173                         table.getManager().setSelectionState(histogramSelectionState);
 174                         tableFilterComponent.loadState(state.getChild(THREADS_TABLE_FILTER));
 175                         chart.setVisibleRange(visibleRange.getStart(), visibleRange.getEnd());
 176                         onFilterChange(tableFilter);
 177                 }
 178 
 179                 @Override
 180                 protected ItemHistogram buildHistogram(Composite parent, IState state) {
 181                         ItemHistogram build = HISTOGRAM.buildWithoutBorder(parent, JfrAttributes.EVENT_THREAD,
 182                                         TableSettings.forState(state));
 183                         return build;
 184                 }
 185 
 186                 @Override
 187                 protected IXDataRenderer getChartRenderer(IItemCollection itemsInTable, HistogramSelection tableSelection) {
 188                         List<IXDataRenderer> rows = new ArrayList<>();
 189 
 190                         IItemCollection selectedItems;
 191                         HistogramSelection selection;
 192                         if (tableSelection.getRowCount() == 0) {
 193                                 selectedItems = itemsInTable;
 194                                 selection = table.getAllRows();
 195                         } else {
 196                                 selectedItems = tableSelection.getItems();
 197                                 selection = tableSelection;
 198                         }
 199                         boolean useDefaultSelection = rows.size() > 1;
 200                         if (lanes.getLaneDefinitions().stream().anyMatch(a -> a.isEnabled()) && selection.getRowCount() > 0) {
 201                                 List<IXDataRenderer> threadRows = selection
 202                                                 .getSelectedRows((object, items) -> lanes.buildThreadRenderer(object, items))
 203                                                 .collect(Collectors.toList());
 204                                 double threadsWeight = Math.sqrt(threadRows.size()) * 0.15;
 205                                 double otherRowWeight = Math.max(threadsWeight * 0.1, (1 - threadsWeight) / rows.size());
 206                                 List<Double> weights = Stream
 207                                                 .concat(Stream.generate(() -> otherRowWeight).limit(rows.size()), Stream.of(threadsWeight))
 208                                                 .collect(Collectors.toList());
 209                                 rows.add(RendererToolkit.uniformRows(threadRows));
 210                                 useDefaultSelection = true;
 211                                 rows = Arrays.asList(RendererToolkit.weightedRows(rows, weights));
 212                         }
 213                         IXDataRenderer root = rows.size() == 1 ? rows.get(0) : RendererToolkit.uniformRows(rows);
 214                         // We don't use the default selection when there is only one row. This is to get the correct payload.
 215                         return useDefaultSelection ? new ItemRow(root, selectedItems.apply(lanes.getEnabledLanesFilter())) : root;
 216                 }
 217 
 218                 @Override
 219                 protected void onFilterChange(IItemFilter filter) {
 220                         super.onFilterChange(filter);
 221                         tableFilter = filter;
 222                 }
 223 
 224                 @Override
 225                 public void saveTo(IWritableState state) {
 226                         super.saveTo(state);
 227                         tableFilterComponent.saveState(state.createChild(THREADS_TABLE_FILTER));
 228                         saveToLocal();
 229                 }
 230 
 231                 private void saveToLocal() {
 232                         flavorSelectorState = flavorSelector.getFlavorSelectorState();
 233                         histogramSelectionState = table.getManager().getSelectionState();
 234                         visibleRange = chart.getVisibleRange();
 235                 }
 236 
 237                 @Override
 238                 protected List<IAction> initializeChartConfiguration(IState state) {
 239                         lanes = new ThreadGraphLanes(() -> getDataSource(), () -> buildChart());
 240                         return lanes.initializeChartConfiguration(Stream.of(state.getChildren(THREAD_LANE)));
 241                 }
 242         }
 243 
 244         private FlavorSelectorState flavorSelectorState;
 245         private SelectionState histogramSelectionState;
 246         private IItemFilter tableFilter;
 247         private IRange<IQuantity> visibleRange;
 248 
 249         public ThreadsPage(IPageDefinition definition, StreamModel model, IPageContainer editor) {
 250                 super(definition, model, editor);
 251                 visibleRange = editor.getRecordingRange();
 252         }
 253 
 254         @Override
 255         public IPageUI display(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 256                 return new ThreadsPageUi(parent, toolkit, editor, state);
 257         }
 258 
 259 }