| /******************************************************************************* |
| * Copyright (c) 2016 Ericsson |
| * |
| * All rights reserved. This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 which |
| * accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Author: |
| * Sonia Farrah |
| *******************************************************************************/ |
| package org.eclipse.tracecompass.internal.analysis.profiling.ui.flamegraph; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Semaphore; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.GroupMarker; |
| import org.eclipse.jface.action.IAction; |
| import org.eclipse.jface.action.IMenuManager; |
| import org.eclipse.jface.action.IToolBarManager; |
| import org.eclipse.jface.action.MenuManager; |
| import org.eclipse.jface.action.Separator; |
| import org.eclipse.jface.dialogs.IDialogSettings; |
| import org.eclipse.jface.resource.ImageDescriptor; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.IStructuredSelection; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Menu; |
| import org.eclipse.tracecompass.analysis.profiling.core.callgraph.ICallGraphProvider; |
| import org.eclipse.tracecompass.analysis.profiling.core.callstack.CallStackAnalysis; |
| import org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph.CallGraphAnalysis; |
| import org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph.ThreadNode; |
| import org.eclipse.tracecompass.internal.analysis.profiling.ui.Activator; |
| import org.eclipse.tracecompass.segmentstore.core.ISegment; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfTraceClosedSignal; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfTraceSelectedSignal; |
| import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp; |
| import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; |
| import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager; |
| import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils; |
| import org.eclipse.tracecompass.tmf.ui.symbols.TmfSymbolProviderUpdatedSignal; |
| import org.eclipse.tracecompass.tmf.ui.views.SaveImageUtil; |
| import org.eclipse.tracecompass.tmf.ui.views.TmfView; |
| import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphViewer; |
| import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphControl; |
| import org.eclipse.ui.IActionBars; |
| import org.eclipse.ui.IWorkbenchActionConstants; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| /** |
| * View to display the flame graph .This uses the flameGraphNode tree generated |
| * by CallGraphAnalysisUI. |
| * |
| * @author Sonia Farrah |
| */ |
| public class FlameGraphView extends TmfView { |
| |
| /** |
| * |
| */ |
| public static final String ID = "org.eclipse.tracecompass.internal.analysis.timing.ui.flamegraph.flamegraphView"; //$NON-NLS-1$ |
| |
| private static final String SORT_OPTION_KEY = "sort.option"; //$NON-NLS-1$ |
| private static final String CONTENT_PRESENTATION_OPTION_KEY = "presentation.option"; //$NON-NLS-1$ |
| private static final ImageDescriptor SORT_BY_NAME_ICON = Activator.getDefault().getImageDescripterFromPath("icons/etool16/sort_alpha.gif"); //$NON-NLS-1$ |
| private static final ImageDescriptor SORT_BY_NAME_REV_ICON = Activator.getDefault().getImageDescripterFromPath("icons/etool16/sort_alpha_rev.gif"); //$NON-NLS-1$ |
| private static final ImageDescriptor SORT_BY_ID_ICON = Activator.getDefault().getImageDescripterFromPath("icons/etool16/sort_num.gif"); //$NON-NLS-1$ |
| private static final ImageDescriptor SORT_BY_ID_REV_ICON = Activator.getDefault().getImageDescripterFromPath("icons/etool16/sort_num_rev.gif"); //$NON-NLS-1$ |
| |
| private final Action VIEW_BY_THREAD = new Action(Messages.FlameGraph_ShowPerThreads, IAction.AS_RADIO_BUTTON) { |
| @Override |
| public void run() { |
| if (fContentPresentation != ContentPresentation.BY_THREAD) { |
| buildFlameGraph(fFlamegraphModules); |
| fContentPresentation = ContentPresentation.BY_THREAD; |
| saveContentPresentationOption(fContentPresentation); |
| } |
| } |
| }; |
| |
| private final Action VIEW_AGGREGATE = new Action(Messages.FlameGraph_AggregateByThread, IAction.AS_RADIO_BUTTON) { |
| @Override |
| public void run() { |
| if (fContentPresentation != ContentPresentation.AGGREGATE_THREADS) { |
| buildFlameGraph(fFlamegraphModules); |
| fContentPresentation = ContentPresentation.AGGREGATE_THREADS; |
| saveContentPresentationOption(fContentPresentation); |
| } |
| } |
| }; |
| |
| private volatile ContentPresentation fContentPresentation = ContentPresentation.AGGREGATE_THREADS; |
| |
| private TimeGraphViewer fTimeGraphViewer; |
| |
| private FlameGraphContentProvider fTimeGraphContentProvider; |
| |
| private ITmfTrace fTrace; |
| |
| /** |
| * A plain old semaphore is used since different threads will be competing |
| * for the same resource. |
| */ |
| private final Semaphore fLock = new Semaphore(1); |
| |
| private Action fSortByNameAction; |
| private Action fSortByIdAction; |
| private Job fJob; |
| |
| private Iterable<CallGraphAnalysis> fFlamegraphModules = null; |
| |
| /** |
| * Constructor |
| */ |
| public FlameGraphView() { |
| super(ID); |
| } |
| |
| @Override |
| public void createPartControl(Composite parent) { |
| super.createPartControl(parent); |
| fTimeGraphViewer = new TimeGraphViewer(parent, SWT.NONE); |
| FlameGraphContentProvider timeGraphContentProvider = new FlameGraphContentProvider(); |
| fTimeGraphContentProvider = timeGraphContentProvider; |
| fTimeGraphViewer.setTimeGraphContentProvider(timeGraphContentProvider); |
| fTimeGraphViewer.setTimeGraphProvider(new FlameGraphPresentationProvider()); |
| ITmfTrace trace = TmfTraceManager.getInstance().getActiveTrace(); |
| if (trace != null) { |
| traceSelected(new TmfTraceSelectedSignal(this, trace)); |
| } |
| contributeToActionBars(); |
| loadSortOption(); |
| loadContentPresentationOption(); |
| TmfSignalManager.register(this); |
| getSite().setSelectionProvider(fTimeGraphViewer.getSelectionProvider()); |
| IMenuManager menuManager = getViewSite().getActionBars().getMenuManager(); |
| MenuManager item = new MenuManager(Messages.FlameGraphView_ContentPresentation); |
| item.setRemoveAllWhenShown(true); |
| item.addMenuListener(manager -> { |
| item.add(VIEW_BY_THREAD); |
| item.add(VIEW_AGGREGATE); |
| }); |
| menuManager.add(item); |
| createTimeEventContextMenu(); |
| fTimeGraphViewer.getTimeGraphControl().addMouseListener(new MouseAdapter() { |
| @Override |
| public void mouseDoubleClick(MouseEvent e) { |
| TimeGraphControl timeGraphControl = getTimeGraphViewer().getTimeGraphControl(); |
| ISelection selection = timeGraphControl.getSelection(); |
| if (selection instanceof IStructuredSelection) { |
| for (Object object : ((IStructuredSelection) selection).toList()) { |
| if (object instanceof FlamegraphEvent) { |
| FlamegraphEvent event = (FlamegraphEvent) object; |
| long startTime = event.getTime(); |
| long endTime = startTime + event.getDuration(); |
| getTimeGraphViewer().setStartFinishTimeNotify(startTime, endTime); |
| break; |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Get the time graph viewer |
| * |
| * @return the time graph viewer |
| */ |
| @VisibleForTesting |
| public TimeGraphViewer getTimeGraphViewer() { |
| return fTimeGraphViewer; |
| } |
| |
| /** |
| * Handler for the trace selected signal |
| * |
| * @param signal |
| * The incoming signal |
| */ |
| @TmfSignalHandler |
| public void traceSelected(final TmfTraceSelectedSignal signal) { |
| ITmfTrace trace = signal.getTrace(); |
| fTrace = trace; |
| if (trace != null) { |
| Iterable<CallStackAnalysis> csModules = TmfTraceUtils.getAnalysisModulesOfClass(trace, CallStackAnalysis.class); |
| List<CallGraphAnalysis> cgModules = new ArrayList<>(); |
| for (CallStackAnalysis csModule : csModules) { |
| csModule.schedule(); |
| ICallGraphProvider cgModule = csModule.getCallGraph(); |
| if (cgModule instanceof CallGraphAnalysis) { |
| cgModules.add((CallGraphAnalysis) cgModule); |
| } |
| } |
| fFlamegraphModules = cgModules; |
| buildFlameGraph(cgModules); |
| } |
| } |
| |
| /** |
| * Get the necessary data for the flame graph and display it |
| * |
| * @param callGraphProviders |
| * the callGraphAnalysis |
| */ |
| @VisibleForTesting |
| public void buildFlameGraph(Iterable<CallGraphAnalysis> callGraphProviders) { |
| /* |
| * Note for synchronization: |
| * |
| * Acquire the lock at entry. then we have 4 places to release it |
| * |
| * 1- if the lock failed |
| * |
| * 2- if the data is null and we have no UI to update |
| * |
| * 3- if the request is cancelled before it gets to the display |
| * |
| * 4- on a clean execution |
| */ |
| Job job = fJob; |
| if (job != null) { |
| job.cancel(); |
| } |
| try { |
| fLock.acquire(); |
| } catch (InterruptedException e) { |
| Activator.getDefault().logError(e.getMessage(), e); |
| fLock.release(); |
| Thread.currentThread().interrupt(); |
| } |
| if (!callGraphProviders.iterator().hasNext()) { |
| fTimeGraphViewer.setInput(null); |
| fLock.release(); |
| return; |
| } |
| for (CallGraphAnalysis provider : callGraphProviders) { |
| provider.schedule(); |
| } |
| job = new Job(Messages.CallGraphAnalysis_Execution) { |
| |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| try { |
| if (monitor.isCanceled()) { |
| return Status.CANCEL_STATUS; |
| } |
| List<ThreadNode> input = new ArrayList<>(); |
| for (CallGraphAnalysis callGraphAnalysis : callGraphProviders) { |
| callGraphAnalysis.waitForCompletion(monitor); |
| input.addAll(fContentPresentation == ContentPresentation.BY_THREAD ? callGraphAnalysis.getThreadNodes() : callGraphAnalysis.getFlameGraph()); |
| } |
| // compute input outside of display thread. |
| Display.getDefault().asyncExec(() -> { |
| fTimeGraphViewer.setInput(input); |
| fTimeGraphViewer.resetStartFinishTime(); |
| }); |
| return Status.OK_STATUS; |
| } finally { |
| fJob = null; |
| fLock.release(); |
| } |
| } |
| }; |
| fJob = job; |
| job.schedule(); |
| } |
| |
| /** |
| * Await the next refresh |
| * |
| * @throws InterruptedException |
| * something took too long |
| */ |
| @VisibleForTesting |
| public void waitForUpdate() throws InterruptedException { |
| /* |
| * wait for the semaphore to be available, then release it immediately |
| */ |
| fLock.acquire(); |
| fLock.release(); |
| } |
| |
| /** |
| * Trace is closed: clear the data structures and the view |
| * |
| * @param signal |
| * the signal received |
| */ |
| @TmfSignalHandler |
| public void traceClosed(final TmfTraceClosedSignal signal) { |
| if (signal.getTrace() == fTrace) { |
| fTimeGraphViewer.setInput(null); |
| } |
| } |
| |
| @Override |
| public void setFocus() { |
| fTimeGraphViewer.setFocus(); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Helper methods |
| // ------------------------------------------------------------------------ |
| |
| private void createTimeEventContextMenu() { |
| MenuManager eventMenuManager = new MenuManager(); |
| eventMenuManager.setRemoveAllWhenShown(true); |
| TimeGraphControl timeGraphControl = fTimeGraphViewer.getTimeGraphControl(); |
| final Menu timeEventMenu = eventMenuManager.createContextMenu(timeGraphControl); |
| |
| timeGraphControl.addTimeGraphEntryMenuListener(event -> { |
| /* |
| * The TimeGraphControl will call the TimeGraphEntryMenuListener |
| * before the TimeEventMenuListener. We need to clear the menu |
| * for the case the selection was done on the namespace where |
| * the time event listener below won't be called afterwards. |
| */ |
| timeGraphControl.setMenu(null); |
| event.doit = false; |
| }); |
| timeGraphControl.addTimeEventMenuListener(event -> { |
| Menu menu = timeEventMenu; |
| if (event.data instanceof FlamegraphEvent) { |
| timeGraphControl.setMenu(menu); |
| return; |
| } |
| timeGraphControl.setMenu(null); |
| event.doit = false; |
| }); |
| |
| eventMenuManager.addMenuListener(manager -> { |
| fillTimeEventContextMenu(eventMenuManager); |
| eventMenuManager.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS)); |
| }); |
| getSite().registerContextMenu(eventMenuManager, fTimeGraphViewer.getSelectionProvider()); |
| } |
| |
| /** |
| * Fill context menu |
| * |
| * @param menuManager |
| * a menuManager to fill |
| */ |
| protected void fillTimeEventContextMenu(@NonNull IMenuManager menuManager) { |
| ISelection selection = getSite().getSelectionProvider().getSelection(); |
| if (selection instanceof IStructuredSelection) { |
| for (Object object : ((IStructuredSelection) selection).toList()) { |
| if (object instanceof FlamegraphEvent) { |
| final FlamegraphEvent flamegraphEvent = (FlamegraphEvent) object; |
| menuManager.add(new Action(Messages.FlameGraphView_GotoMaxDuration) { |
| @Override |
| public void run() { |
| ISegment maxSeg = flamegraphEvent.getStatistics().getDurationStatistics().getMaxObject(); |
| if (maxSeg == null) { |
| return; |
| } |
| TmfSelectionRangeUpdatedSignal sig = new TmfSelectionRangeUpdatedSignal(this, TmfTimestamp.fromNanos(maxSeg.getStart()), TmfTimestamp.fromNanos(maxSeg.getEnd()), fTrace); |
| broadcast(sig); |
| } |
| }); |
| |
| menuManager.add(new Action(Messages.FlameGraphView_GotoMinDuration) { |
| @Override |
| public void run() { |
| ISegment minSeg = flamegraphEvent.getStatistics().getDurationStatistics().getMinObject(); |
| if (minSeg == null) { |
| return; |
| } |
| TmfSelectionRangeUpdatedSignal sig = new TmfSelectionRangeUpdatedSignal(this, TmfTimestamp.fromNanos(minSeg.getStart()), TmfTimestamp.fromNanos(minSeg.getEnd()), fTrace); |
| broadcast(sig); |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| private void contributeToActionBars() { |
| IActionBars bars = getViewSite().getActionBars(); |
| fillLocalToolBar(bars.getToolBarManager()); |
| } |
| |
| private void fillLocalToolBar(IToolBarManager manager) { |
| manager.add(getSortByNameAction()); |
| manager.add(getSortByIdAction()); |
| manager.add(new Separator()); |
| } |
| |
| private Action getSortByNameAction() { |
| if (fSortByNameAction == null) { |
| fSortByNameAction = new Action(Messages.FlameGraph_SortByThreadName, IAction.AS_CHECK_BOX) { |
| @Override |
| public void run() { |
| SortOption sortOption = fTimeGraphContentProvider.getSortOption(); |
| if (sortOption == SortOption.BY_NAME) { |
| setSortOption(SortOption.BY_NAME_REV); |
| } else { |
| setSortOption(SortOption.BY_NAME); |
| } |
| } |
| }; |
| fSortByNameAction.setToolTipText(Messages.FlameGraph_SortByThreadName); |
| fSortByNameAction.setImageDescriptor(SORT_BY_NAME_ICON); |
| } |
| return fSortByNameAction; |
| } |
| |
| private Action getSortByIdAction() { |
| if (fSortByIdAction == null) { |
| fSortByIdAction = new Action(Messages.FlameGraph_SortByThreadId, IAction.AS_CHECK_BOX) { |
| @Override |
| public void run() { |
| SortOption sortOption = fTimeGraphContentProvider.getSortOption(); |
| if (sortOption == SortOption.BY_ID) { |
| setSortOption(SortOption.BY_ID_REV); |
| } else { |
| setSortOption(SortOption.BY_ID); |
| } |
| } |
| }; |
| fSortByIdAction.setToolTipText(Messages.FlameGraph_SortByThreadId); |
| fSortByIdAction.setImageDescriptor(SORT_BY_ID_ICON); |
| } |
| return fSortByIdAction; |
| } |
| |
| private void setSortOption(SortOption sortOption) { |
| // reset defaults |
| getSortByNameAction().setChecked(false); |
| getSortByNameAction().setImageDescriptor(SORT_BY_NAME_ICON); |
| getSortByIdAction().setChecked(false); |
| getSortByIdAction().setImageDescriptor(SORT_BY_ID_ICON); |
| |
| if (sortOption.equals(SortOption.BY_NAME)) { |
| getSortByNameAction().setChecked(true); |
| } else if (sortOption.equals(SortOption.BY_NAME_REV)) { |
| getSortByNameAction().setChecked(true); |
| getSortByNameAction().setImageDescriptor(SORT_BY_NAME_REV_ICON); |
| } else if (sortOption.equals(SortOption.BY_ID)) { |
| getSortByIdAction().setChecked(true); |
| } else if (sortOption.equals(SortOption.BY_ID_REV)) { |
| getSortByIdAction().setChecked(true); |
| getSortByIdAction().setImageDescriptor(SORT_BY_ID_REV_ICON); |
| } |
| fTimeGraphContentProvider.setSortOption(sortOption); |
| saveSortOption(); |
| fTimeGraphViewer.refresh(); |
| } |
| |
| private void saveSortOption() { |
| SortOption sortOption = fTimeGraphContentProvider.getSortOption(); |
| IDialogSettings settings = Activator.getDefault().getDialogSettings(); |
| IDialogSettings section = settings.getSection(getClass().getName()); |
| if (section == null) { |
| section = settings.addNewSection(getClass().getName()); |
| } |
| section.put(SORT_OPTION_KEY, sortOption.name()); |
| } |
| |
| private void loadSortOption() { |
| IDialogSettings settings = Activator.getDefault().getDialogSettings(); |
| IDialogSettings section = settings.getSection(getClass().getName()); |
| if (section == null) { |
| return; |
| } |
| String sortOption = section.get(SORT_OPTION_KEY); |
| if (sortOption == null) { |
| return; |
| } |
| setSortOption(SortOption.fromName(sortOption)); |
| } |
| |
| private void saveContentPresentationOption(ContentPresentation contentPresentation) { |
| IDialogSettings settings = Activator.getDefault().getDialogSettings(); |
| IDialogSettings section = settings.getSection(getClass().getName()); |
| if (section == null) { |
| section = settings.addNewSection(getClass().getName()); |
| } |
| section.put(CONTENT_PRESENTATION_OPTION_KEY, contentPresentation.name()); |
| } |
| |
| private void loadContentPresentationOption() { |
| IDialogSettings settings = Activator.getDefault().getDialogSettings(); |
| IDialogSettings section = settings.getSection(getClass().getName()); |
| ContentPresentation contentPresentation = fContentPresentation; |
| if (section != null) { |
| String contentPresentationOption = section.get(CONTENT_PRESENTATION_OPTION_KEY); |
| if (contentPresentationOption != null) { |
| contentPresentation = ContentPresentation.fromName(contentPresentationOption); |
| } |
| } |
| VIEW_BY_THREAD.setChecked(contentPresentation == ContentPresentation.BY_THREAD); |
| VIEW_AGGREGATE.setChecked(contentPresentation == ContentPresentation.AGGREGATE_THREADS); |
| fContentPresentation = contentPresentation; |
| } |
| |
| /** |
| * Symbol map provider updated |
| * |
| * @param signal |
| * the signal |
| */ |
| @TmfSignalHandler |
| public void symbolMapUpdated(TmfSymbolProviderUpdatedSignal signal) { |
| if (signal.getSource() != this) { |
| fTimeGraphViewer.refresh(); |
| } |
| } |
| |
| @Override |
| protected @Nullable IAction createSaveAction() { |
| return SaveImageUtil.createSaveAction(getName(), this::getTimeGraphViewer); |
| } |
| |
| } |