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 }