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