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.common; 34 35 import static org.openjdk.jmc.ui.charts.QuantitySpanRenderer.MISSING_END; 36 import static org.openjdk.jmc.ui.charts.QuantitySpanRenderer.MISSING_START; 37 38 import java.awt.Color; 39 import java.text.MessageFormat; 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.Iterator; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.function.Predicate; 46 import java.util.function.Supplier; 47 import java.util.stream.Collectors; 48 import java.util.stream.Stream; 49 50 import org.eclipse.jface.action.Action; 51 import org.eclipse.jface.action.IAction; 52 import org.eclipse.jface.action.Separator; 53 import org.eclipse.osgi.util.NLS; 54 55 import org.openjdk.jmc.common.IDisplayable; 56 import org.openjdk.jmc.common.IMCThread; 57 import org.openjdk.jmc.common.IState; 58 import org.openjdk.jmc.common.IWritableState; 59 import org.openjdk.jmc.common.item.IItem; 60 import org.openjdk.jmc.common.item.IItemCollection; 61 import org.openjdk.jmc.common.item.IItemFilter; 62 import org.openjdk.jmc.common.item.IItemIterable; 63 import org.openjdk.jmc.common.item.ItemFilters; 64 import org.openjdk.jmc.common.unit.IQuantity; 65 import org.openjdk.jmc.flightrecorder.JfrAttributes; 66 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters; 67 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs; 68 import org.openjdk.jmc.flightrecorder.ui.EventTypeFolderNode; 69 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI; 70 import org.openjdk.jmc.flightrecorder.ui.ItemCollectionToolkit; 71 import org.openjdk.jmc.flightrecorder.ui.StreamModel; 72 import org.openjdk.jmc.flightrecorder.ui.common.LaneEditor.LaneDefinition; 73 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages; 74 import org.openjdk.jmc.ui.charts.IQuantitySeries; 75 import org.openjdk.jmc.ui.charts.ISpanSeries; 76 import org.openjdk.jmc.ui.charts.IXDataRenderer; 77 import org.openjdk.jmc.ui.charts.QuantitySeries; 78 import org.openjdk.jmc.ui.charts.QuantitySpanRenderer; 79 import org.openjdk.jmc.ui.charts.RendererToolkit; 80 import org.openjdk.jmc.ui.charts.SpanRenderer; 81 import org.openjdk.jmc.ui.handlers.ActionToolkit; 82 import org.openjdk.jmc.ui.handlers.MCContextMenuManager; 83 84 public class ThreadGraphLanes { 85 86 private static final String EDIT_LANES = "editLanes"; //$NON-NLS-1$ 87 private static final Color THREAD_BG_COLOR = new Color( 88 Color.HSBtoRGB(Color.RGBtoHSB(200, 255, 200, null)[0], 0.6f, 0.5f)); 89 private static final String THREAD_LANE = "threadLane"; //$NON-NLS-1$ 90 91 private List<LaneDefinition> laneDefs; 92 private List<LaneDefinition> naLanes; 93 private Supplier<StreamModel> dataSourceSupplier; 94 private Runnable buildChart; 95 private List<IAction> actions; 96 private String tooltipTitle; 97 98 public ThreadGraphLanes(Supplier<StreamModel> dataSourceSupplier, Runnable buildChart) { 99 this.dataSourceSupplier = dataSourceSupplier; 100 this.buildChart = buildChart; 101 this.actions = new ArrayList<>(); 102 } 103 104 public void openEditLanesDialog(MCContextMenuManager mm, boolean isLegendMenu) { 105 // FIXME: Might there be other interesting events that don't really have duration? 106 EventTypeFolderNode typeTree = dataSourceSupplier.get().getTypeTree(ItemCollectionToolkit 107 .stream(dataSourceSupplier.get().getItems()).filter(this::typeWithThreadAndDuration)); 108 laneDefs = LaneEditor.openDialog(typeTree, laneDefs.stream().collect(Collectors.toList()), 109 Messages.JavaApplicationPage_EDIT_THREAD_LANES_DIALOG_TITLE, 110 Messages.JavaApplicationPage_EDIT_THREAD_LANES_DIALOG_MESSAGE); 111 updateContextMenu(mm, isLegendMenu); 112 buildChart.run(); 113 } 114 115 public List<LaneDefinition> getLaneDefinitions() { 116 return laneDefs; 117 } 118 119 private Boolean typeWithThreadAndDuration(IItemIterable itemStream) { 120 return DataPageToolkit.isTypeWithThreadAndDuration(itemStream.getType()); 121 } 122 123 public IItemFilter getEnabledLanesFilter() { 124 List<IItemFilter> laneFilters = laneDefs.stream() 125 .filter((Predicate<? super LaneDefinition>) LaneDefinition::isEnabled).map(ld -> ld.getFilter()) 126 .collect(Collectors.toList()); 127 return ItemFilters.or(laneFilters.toArray(new IItemFilter[laneFilters.size()])); 128 } 129 130 private void setTooltipTitle(String description) { 131 this.tooltipTitle = description; 132 } 133 134 private String getTooltipTitle() { 135 return this.tooltipTitle; 136 } 137 138 private void resetTooltipTitle() { 139 this.tooltipTitle = null; 140 } 141 142 public IXDataRenderer buildThreadRenderer(Object thread, IItemCollection items) { 143 this.resetTooltipTitle(); 144 String threadName = thread == null ? "" : ((IMCThread) thread).getThreadName(); //$NON-NLS-1$ 145 // FIXME: Workaround since this method can be called from super class constructor. Refactor to avoid this. 146 List<LaneDefinition> laneFilters = this.laneDefs == null ? Collections.emptyList() : this.laneDefs; 147 List<IXDataRenderer> lanes = new ArrayList<>(laneFilters.size()); 148 laneFilters.stream().filter(ld -> ld.isEnabled()).forEach(lane -> { 149 IItemCollection laneItems = items.apply(lane.getFilter()); 150 if (laneItems.iterator().hasNext()) { 151 ISpanSeries<IItem> laneSeries = QuantitySeries.max(laneItems, JfrAttributes.START_TIME, 152 JfrAttributes.END_TIME); 153 this.setTooltipTitle(MessageFormat.format(Messages.ThreadsPage_LANE_TOOLTIP_TITLE, threadName, lane.getName())); 154 lanes.add(new ItemRow(SpanRenderer.withBoundaries(laneSeries, DataPageToolkit.ITEM_COLOR, this.getTooltipTitle()), laneItems)); 155 } 156 }); 157 IXDataRenderer renderer = !lanes.isEmpty() ? RendererToolkit.uniformRows(lanes) 158 : new ItemRow(RendererToolkit.empty(), ItemCollectionToolkit.EMPTY); 159 IItemCollection itemsAndThreadLifespan = addThreadLifeSpanEvents(thread, items); 160 // If the lane doesn't match a filter, display the Thread name as the tooltip title 161 if (this.getTooltipTitle() == null) { 162 this.setTooltipTitle(threadName); 163 } else { 164 this.resetTooltipTitle(); 165 } 166 return new QuantitySpanRenderer(threadRanges(threadName, itemsAndThreadLifespan), renderer, THREAD_BG_COLOR, 10, 167 threadName, this.getTooltipTitle(), thread); 168 } 169 170 private IItemCollection addThreadLifeSpanEvents(Object thread, final IItemCollection items) { 171 IItemCollection threadLifeSpan = dataSourceSupplier.get().getItems() 172 .apply(ItemFilters.and(ItemFilters.equals(JfrAttributes.EVENT_THREAD, (IMCThread) thread), 173 ItemFilters.type(JdkTypeIDs.JAVA_THREAD_START, JdkTypeIDs.JAVA_THREAD_END))); 174 IItemCollection itemsAndThreadLifespan = ItemCollectionToolkit.merge(() -> Stream.of(items, threadLifeSpan)); 175 return itemsAndThreadLifespan; 176 } 177 178 private IQuantitySeries<?> threadRanges(String threadName, IItemCollection items) { 179 IItemCollection startEvents = items.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_START)); 180 IItemCollection endEvents = items.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_END)); 181 Iterator<IQuantity> start = ItemCollectionToolkit.values(startEvents, JfrAttributes.START_TIME).get().sorted() 182 .iterator(); 183 Iterator<IQuantity> end = ItemCollectionToolkit.values(endEvents, JfrAttributes.END_TIME).get().sorted() 184 .iterator(); 185 186 ArrayList<IQuantity> startList = new ArrayList<>(); 187 ArrayList<IQuantity> endList = new ArrayList<>(); 188 IQuantity sq = start.hasNext() ? start.next() : MISSING_START; 189 IQuantity eq = end.hasNext() ? end.next() : MISSING_END; 190 if (sq.compareTo(eq) >= 0) { 191 startList.add(MISSING_START); 192 endList.add(eq); 193 eq = end.hasNext() ? end.next() : MISSING_END; 194 } 195 196 while (start.hasNext()) { 197 startList.add(sq); 198 endList.add(eq); 199 sq = start.hasNext() ? start.next() : MISSING_START; 200 eq = end.hasNext() ? end.next() : MISSING_END; 201 } 202 startList.add(sq); 203 endList.add(eq); 204 205 String text = NLS.bind(Messages.JavaApplicationPage_THREAD_LIFESPAN, threadName); 206 return QuantitySeries.all(startList, endList, new IDisplayable() { 207 208 @Override 209 public String displayUsing(String formatHint) { 210 return text; 211 } 212 213 }); 214 } 215 216 public void saveTo(IWritableState writableState) { 217 laneDefs.stream().forEach(f -> f.saveTo(writableState.createChild(THREAD_LANE))); 218 // FIXME: This will change the order from the original lane order, putting all the non applicable lanes last, can we live with that? 219 naLanes.stream().forEach(f -> f.saveTo(writableState.createChild(THREAD_LANE))); 220 } 221 222 public List<IAction> initializeChartConfiguration(Stream<IState> laneStates) { 223 laneDefs = new ArrayList<>(); 224 laneStates.map(LaneDefinition::readFrom).forEach(laneDefs::add); 225 if (laneDefs.isEmpty()) { 226 laneDefs.add(new LaneDefinition(Messages.JavaApplicationPage_THREAD_LANE_JAVA_LATENCIES, true, 227 JdkFilters.THREAD_LATENCIES, false)); 228 } 229 // FIXME: Might be nice to make some sort of model for the whole lane set 230 LaneEditor.ensureRestLane(laneDefs); 231 Map<Boolean, List<LaneDefinition>> lanesByApplicability = laneDefs.stream() 232 .collect(Collectors.partitioningBy(ld -> ld.isRestLane() 233 || dataSourceSupplier.get().getItems().apply(ld.getFilter()).iterator().hasNext())); 234 laneDefs = lanesByApplicability.get(true); 235 naLanes = lanesByApplicability.get(false); 236 return Collections.emptyList(); 237 } 238 239 //create two action identifiers to handle the chart context menu and the legend context menu 240 private List<String> chartActionIdentifiers = new ArrayList<>(); 241 private List<String> legendActionIdentifiers = new ArrayList<>(); 242 243 public void updateContextMenu(MCContextMenuManager mm, boolean isLegendMenu) { 244 245 if(isLegendMenu) { 246 for (String id : legendActionIdentifiers) { 247 mm.remove(id); 248 } 249 legendActionIdentifiers.clear(); 250 } else { 251 for (String id : chartActionIdentifiers) { 252 mm.remove(id); 253 } 254 chartActionIdentifiers.clear(); 255 } 256 if (mm.indexOf(EDIT_LANES) == -1) { 257 IAction action = ActionToolkit.action(() -> this.openEditLanesDialog(mm, isLegendMenu), 258 Messages.JavaApplicationPage_EDIT_THREAD_LANES_ACTION, 259 FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_LANES_EDIT)); 260 action.setId(EDIT_LANES); 261 mm.add(action); 262 actions.add(action); 263 mm.add(new Separator()); 264 } 265 laneDefs.stream().forEach(ld -> { 266 Action checkAction = new Action(ld.getName(), IAction.AS_CHECK_BOX) { 267 int laneIndex = laneDefs.indexOf(ld); 268 269 @Override 270 public void run() { 271 LaneDefinition newLd = new LaneDefinition(ld.getName(), isChecked(), ld.getFilter(), 272 ld.isRestLane()); 273 laneDefs.set(laneIndex, newLd); 274 buildChart.run(); 275 } 276 }; 277 String identifier = ld.getName() + checkAction.hashCode(); 278 checkAction.setId(identifier); 279 if(isLegendMenu) { 280 legendActionIdentifiers.add(identifier); 281 } else { 282 chartActionIdentifiers.add(identifier); 283 } 284 checkAction.setChecked(ld.isEnabled()); 285 // FIXME: Add a tooltip here 286 mm.add(checkAction); 287 actions.add(checkAction); 288 }); 289 } 290 291 public List<IAction> getContextMenuActions() { 292 return actions; 293 } 294 295 }