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 }