sysperfana/perfinvestigator/com.nokia.carbide.cpp.pi.peccommon/src/com/nokia/carbide/cpp/pi/peccommon/PecCommonTraceGraph.java
author Toni Pulkkinen <ext-toni.p.pulkkinen@nokia.com>
Wed, 21 Apr 2010 15:14:16 +0300
changeset 5 844b047e260d
child 12 ae255c9aa552
permissions -rw-r--r--
Performance Investigator Carbide extension 2.3.0

/*
 * Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). 
 * All rights reserved.
 * This component and the accompanying materials are made available
 * under the terms of the License "Eclipse Public License v1.0"
 * which accompanies this distribution, and is available
 * at the URL "http://www.eclipse.org/legal/epl-v10.html".
 *
 * Initial Contributors:
 * Nokia Corporation - initial contribution.
 *
 * Contributors:
 *
 * Description: 
 *
 */

package com.nokia.carbide.cpp.pi.peccommon;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.MouseEvent;
import org.eclipse.draw2d.MouseMotionListener;
import org.eclipse.draw2d.Panel;
import org.eclipse.draw2d.Viewport;
import org.eclipse.jface.action.Action;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;

import com.nokia.carbide.cpp.internal.pi.analyser.NpiInstanceRepository;
import com.nokia.carbide.cpp.internal.pi.analyser.ProfileVisualiser;
import com.nokia.carbide.cpp.internal.pi.model.GenericSample;
import com.nokia.carbide.cpp.internal.pi.plugin.model.ITitleBarMenu;
import com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph;
import com.nokia.carbide.cpp.internal.pi.visual.GraphComposite;
import com.nokia.carbide.cpp.internal.pi.visual.PIEvent;
import com.nokia.carbide.cpp.internal.pi.visual.PIEventListener;
import com.nokia.carbide.cpp.pi.editors.PIPageEditor;
import com.nokia.carbide.cpp.pi.util.ColorPalette;
import com.nokia.carbide.cpp.pi.visual.IGenericTraceGraph;

/**
 * The performance counter graph class, responsible for drawing the several graphs
 * based on performance counters as well as managing the legend views to some extend
 */
public class PecCommonTraceGraph extends GenericTraceGraph implements 
		MouseMotionListener, PIEventListener,  ITitleBarMenu  {

	private static int X_LEGEND_HEIGHT = 20;

	/** for tooltip: margin for search area for finding a domain object */
	private static final int MARGIN_IN_PIXELS = 5;
	
	/** the graph index; there is one graph per editor page (with identical content)*/
	//private int graphIndex;
	protected PecCommonTrace trace;
	private String graphTitle;
	private String helpContextId;

	/** number of series to draw; each in a separate graph section */
	private int typeAmount = 0;
	protected List<Integer> drawSeries;
	
	private int[][] origYCoords;
	private int[][] points;

	private int[] exact_mins;
	private int[] exact_maxs;

	private int[] mins;
	private int[] maxs;
	
	private double currentlyCalcFinalScale = -1;
	private int currentlyCalcFinalHeight = -1;

	protected FigureCanvas leftFigureCanvas;

	/** marks graph as dirty so it gets re-drawn */
	protected boolean dirty;

	private PecCommonLegend legend;
	
	/** GUI manager has knowledge off all IpcTraceGraphs; can broadcast some of the events */
	private PecCommonGuiManager guiManager;
	
	/**
	 * Constructor
	 * @param graphIndex the index of the graph (corresponds to the page in the editor)
	 * @param pecTrace the trace class
	 * @param uid the uid to identify the current editor
	 * @param guiManager IpcGuiManager which manages all graphs
	 * @param title The title of the graph
	 * @param helpContextIdMainPage 
	 */
	public PecCommonTraceGraph(int graphIndex, PecCommonTrace pecTrace, int uid, PecCommonGuiManager guiManager, String title, String helpContextIdMainPage) {
		super(pecTrace);

//		this.graphIndex = graphIndex;
		this.trace = pecTrace;
		this.guiManager = guiManager;
		this.graphTitle = title;
		this.helpContextId = helpContextIdMainPage;
		
		typeAmount = pecTrace.getValueTypes().length;

		int sampleCount = pecTrace.getSampleAmount();
		origYCoords = new int[typeAmount][sampleCount];
		points = new int[typeAmount][sampleCount*2];
		exact_mins = new int[typeAmount];
		exact_maxs = new int[typeAmount];
		mins = new int[typeAmount];
		maxs = new int[typeAmount];
		
		drawSeries = new ArrayList<Integer>();
		//by default, draw all graphs on screen
		showAll();
		
		this.initialiseData();
		
		ProfileVisualiser pV = NpiInstanceRepository.getInstance().getProfilePage(uid, graphIndex);
		legend = createLegend(pV.getBottomComposite());
	}
	
	/**
	 * Creates the legend
	 * @param bottomComposite The composite to create the legend in
	 */
	protected PecCommonLegend createLegend(Composite bottomComposite) {		
		return new PecCommonLegend(this, bottomComposite, getTitle(), trace);
	}

	/**
	 * Fill mins, maxs, and origYCoords arrays
	 */
	private void initialiseData() {
		Vector<GenericSample> sv = trace.samples;

		Arrays.fill(exact_mins, Integer.MAX_VALUE);
		Arrays.fill(exact_maxs, Integer.MIN_VALUE);

		for (int x = 0; x < sv.size(); x++) {
			PecCommonSample s = (PecCommonSample) sv.get(x);

			for (int i = 0; i < typeAmount; i++) {
				int value = s.values[i];

				if (value < exact_mins[i]){
					exact_mins[i] = value;					
				} 
				if (value > exact_maxs[i]){
					exact_maxs[i] = value;					
				}

				origYCoords[i][x] = value;
			}
		}
		
		//let the graph draw in area [0, prettyMaxValue]
		for (int i = 0; i < typeAmount; i++) {
			mins[i] = 0;
			maxs[i] = prettyMaxValue(exact_maxs[i]);
		}
	}
	
	private void showAll(){
		drawSeries.clear();
		for (int i = 0; i < typeAmount; i++) {
			drawSeries.add(i);
		}
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#repaint()
	 */
	@Override
	public void repaint() {
		this.parentComponent.repaintComponent();
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#paint(org.eclipse.draw2d.Panel, org.eclipse.draw2d.Graphics)
	 */
	@Override
	public void paint(org.eclipse.draw2d.Panel panel,
			org.eclipse.draw2d.Graphics graphics) {
		this.setSize(this.getSize().width, getVisualSize().height);
		this.drawDottedLineBackground(graphics, X_LEGEND_HEIGHT);
		
		drawGraphs(panel, graphics);
		drawLabelsInGraph(panel, graphics);

		this.drawSelectionSection(graphics, X_LEGEND_HEIGHT);
	}

	private void drawLabelsInGraph(Panel panel, Graphics graphics) {

		float sectionHeight = getSectionHeight();
		
		if (sectionHeight > 60f){ //only draw the label if the section has a decent height
			
			int edgeX = ((Viewport) panel.getParent()).getViewLocation().x;

			for (int i = 0; i < drawSeries.size(); i++) {
				int seriesIdx = drawSeries.get(i);
				int y = (int) sectionHeight * i;

				graphics.setForegroundColor(ColorConstants.black);
				graphics.drawString(trace.getValueTypes()[seriesIdx],
						edgeX + 10, y + 6);
				if (sectionHeight > 80f){
					graphics.drawString(String.format(Messages.PecCommonTraceGraph_0,
							this.exact_mins[seriesIdx], this.exact_maxs[seriesIdx]),
							edgeX + 10, y + 18);					
				}
			}
		}
	}

	private void drawGraphs(Panel panel, Graphics graphics) {
		int visY = this.getVisualSize().height;
		double scale = getScale();

		if (dirty || this.currentlyCalcFinalHeight != visY
				|| this.currentlyCalcFinalScale != scale) {

			Vector<GenericSample> sv = trace.samples;
			float sectionHeight = getSectionHeight();
			
			for (int sampleIdx = 0; sampleIdx < sv.size(); sampleIdx++) {
				// calculate x-coordinate per sample
				int xCoord = (int) (sampleIdx / scale);

				for (int seriesIdx : drawSeries) {
					points[seriesIdx][sampleIdx * 2] = xCoord;
					points[seriesIdx][sampleIdx * 2 + 1] = convertValueToYCoordinate(origYCoords[seriesIdx][sampleIdx], mins[seriesIdx], maxs[seriesIdx], drawSeries.indexOf(seriesIdx), sectionHeight);
				}
			}
			
			dirty = false;
			this.currentlyCalcFinalHeight = visY;
			this.currentlyCalcFinalScale = scale;
		}

		graphics.setForegroundColor(ColorConstants.blue);

		for (int seriesIdx : drawSeries) {
			graphics.drawPolyline(points[seriesIdx]);
		}
	}
	
	/**
	 * Converts a y coordinate into the actual value.
	 * @param yCoordinate the y coordinate to convert
	 * @param minValue the lowest value in the series
	 * @param maxValue the highest value in the series
	 * @param section the number of the section (for 3 sections this would be 0, 1, or 2)
	 * @param sectionHeight the height of the section in pixels. All sections are of equal height
	 * @return the converted value
	 */
	static int convertYCoordinateToValue(int yCoordinate, int minValue, int maxValue, int section, float sectionHeight){
		float offset = sectionHeight * section;
		
		float location = sectionHeight - (yCoordinate - offset) ;
		return (int)(location * (maxValue - minValue) / sectionHeight ) + minValue;
	} 
	
	/**
	 * Converts a value into a y coordinate; the opposite of convertYCoordinateToValue()
	 * @param value the value to convert
	 * @param minValue the lowest value in the series
	 * @param maxValue the highest value in the series
	 * @param section the number of the section (for 3 sections this would be 0, 1, or 2)
	 * @param sectionHeight the height of the section in points. All sections are of equal height
	 * @return the converted value
	 */
	static int convertValueToYCoordinate(int value, int minValue, int maxValue, int section, float sectionHeight){
		float offset = sectionHeight * section;
		
		float location = (value - minValue) * sectionHeight / (maxValue - minValue); //lowest shown value is min (not 0)
		return(int) ((sectionHeight - location) + offset);
	}
	
	/**
	 * Returns the height of a section in pixels. All sections are of equal height.
	 * @return
	 */
	private float getSectionHeight(){
		int visualHeight = getVisualSize().height - X_LEGEND_HEIGHT;
		return drawSeries.size() == 0 ? visualHeight :  visualHeight / drawSeries.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#paintLeftLegend
	 * (org.eclipse.draw2d.FigureCanvas, org.eclipse.swt.graphics.GC)
	 */
	@Override
	public void paintLeftLegend(FigureCanvas figureCanvas, GC gc) {
		GC localGC = gc;

		if (gc == null)
			gc = new GC(PIPageEditor.currentPageEditor().getSite().getShell());

		Rectangle rect = ((GraphComposite) figureCanvas.getParent()).figureCanvas
				.getClientArea();

		int visY = rect.height;

		float visYfloat = visY - X_LEGEND_HEIGHT;

		if (visYfloat < 0f)
			visYfloat = 0f;

		gc.setForeground(ColorPalette.getColor(new RGB(100, 100, 100)));
		gc.setBackground(ColorPalette.getColor(new RGB(255, 255, 255)));
		
		int legendsToDraw = drawSeries.size();
		float legendHeight = visYfloat / legendsToDraw;
		double yIncrement = legendHeight / 10;
		int previousBottom = 0;
		
		for (int section = 0; section < legendsToDraw; section++) {
			int seriesIdx = drawSeries.get(section);
			int maxValue = maxs[seriesIdx];

			float valuePerPixel = maxValue / legendHeight;

			for (int k = 10; k >= 0; k--) {
				// location for the value indicator is k * 1/10 the height of
				// the
				// height of the section
				int y = (int) (legendHeight * (section+1) - (yIncrement * k));
				int value = (int) ((((legendHeight * valuePerPixel) / 10.0) * k));

				// construct the text for each scale
				//use grouping for small numbers, then without grouping to fit the text
				String sValue = String.format(value < 1000000 ? Messages.PecCommonTraceGraph_1 : Messages.PecCommonTraceGraph_2, value);

				Point extent = gc.stringExtent(sValue);

				gc.drawLine(IGenericTraceGraph.Y_LEGEND_WIDTH - 3,  y + 1,
						IGenericTraceGraph.Y_LEGEND_WIDTH, y + 1);

				if (y >= previousBottom) {
					gc.drawString(sValue, IGenericTraceGraph.Y_LEGEND_WIDTH
							- extent.x - 4, y);
					previousBottom = y + extent.y;
				}
			}
		}

		if (localGC == null) {
			gc.dispose();
			figureCanvas.redraw();
		}
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#getTitle()
	 */
	@Override
	public String getTitle() {
		return this.graphTitle;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.MouseMotionListener#mouseDragged(org.eclipse.draw2d.MouseEvent)
	 */
	public void mouseDragged(MouseEvent arg0) {
		// no-op		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.MouseMotionListener#mouseEntered(org.eclipse.draw2d.MouseEvent)
	 */
	public void mouseEntered(MouseEvent arg0) {
		// no-op		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.MouseMotionListener#mouseExited(org.eclipse.draw2d.MouseEvent)
	 */
	public void mouseExited(MouseEvent arg0) {
		// no-op		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.MouseMotionListener#mouseHover(org.eclipse.draw2d.MouseEvent)
	 */
	public void mouseHover(MouseEvent arg0) {
		// no-op		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.MouseMotionListener#mouseMoved(org.eclipse.draw2d.MouseEvent)
	 */
	public void mouseMoved(MouseEvent me) {
		//setting the tooltip
		float sectionHeight = getSectionHeight();
		int section = (int) (me.y / sectionHeight);
		if (section < 0 || section >= drawSeries.size()){
			this.setToolTipText(null);
			return;
		}
		int seriesIdx = drawSeries.get(section);
		
		PecCommonSample sample = getSampleUnderMouse(me);

		if (sample != null){
			setToolTipText(String.format(Messages.PecCommonTraceGraph_3, trace.getValueTypes()[seriesIdx], sample.sampleSynchTime /1000f, sample.values[seriesIdx]));			
		} else {
			String tooltip = null;

			//display default values
			
			if (section < drawSeries.size()) {
				double time = me.x * this.getScale();
				if (time >= 0 && time <= trace.getLastSampleTime()){
					int value = convertYCoordinateToValue(me.y, mins[seriesIdx], maxs[seriesIdx], section, sectionHeight);
					tooltip = String.format(Messages.PecCommonTraceGraph_4, time / 1000f, value);
				}
			}
			
			this.setToolTipText(tooltip);			
		}
	}

	/**
	 * Returns the sample for the given mouse location, or null
	 * @param me mouse event for the mouse location
	 * @return the sample if found, or null
	 */
	private PecCommonSample getSampleUnderMouse(MouseEvent me) {
		PecCommonSample ret = null;
		
		if (drawSeries.size() == 0){
			return ret; //no graphs are being drawn so no samples can be found
		}

		int mex = me.x;
		int mey = me.y;
		float sectionHeight = getSectionHeight();
		int section = (int) (mey / sectionHeight);
		
		if (section >= 0 && section < drawSeries.size()) {
			int seriesIdx = drawSeries.get(section);
			double idealTime = mex * this.getScale();
			if (idealTime < 0 || idealTime > trace.getLastSampleTime()){
				return null; 
			}
			int idealValue = convertYCoordinateToValue(mey, mins[seriesIdx], maxs[seriesIdx], section, sectionHeight);

			//calculate the boundary of the area in which to look for a sample
			double leftBoudaryTime =  ((mex - MARGIN_IN_PIXELS) * getScale());
			if (leftBoudaryTime < 0){
				leftBoudaryTime = 0;
			}
			double rightBoundaryTime = ((mex + MARGIN_IN_PIXELS) * getScale());
			if (rightBoundaryTime < 0){
				return null; 
			} else if (rightBoundaryTime > trace.getLastSampleTime()){
				rightBoundaryTime = trace.getLastSampleTime();
			}
			 
			int topBoundaryValue = convertYCoordinateToValue(Math.max((int)(section * sectionHeight), mey - MARGIN_IN_PIXELS), mins[seriesIdx], maxs[seriesIdx], section, sectionHeight);
			int bottomBoundaryValue = convertYCoordinateToValue(Math.min((int)((section +1) * sectionHeight), mey + MARGIN_IN_PIXELS), mins[seriesIdx], maxs[seriesIdx], section, sectionHeight);
			
			int leftSample = (int)(leftBoudaryTime / trace.getSamplingInterval());
			int rightSample = (int)(rightBoundaryTime+.5 / trace.getSamplingInterval());
			
			for (int i = leftSample; i <= rightSample; i++) {
				PecCommonSample sample = (PecCommonSample)trace.getSample(i);
				if (sample.values[seriesIdx] < topBoundaryValue && sample.values[seriesIdx] > bottomBoundaryValue){
					//System.out.println("Cur: "+ sample.values[seriesIdx]+" Range: "+topBoundaryValue+", "+bottomBoundaryValue); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					//check whether we have found a closer match to mid-point
					if (ret == null 
							|| (Math.abs(sample.sampleSynchTime - idealTime) < Math.abs(ret.sampleSynchTime - idealTime))
							|| (ret.sampleSynchTime == sample.sampleSynchTime 
									&& Math.abs(sample.values[seriesIdx] - idealValue) < Math.abs(ret.values[seriesIdx]	- idealValue))){
						ret = sample;
					}
				}
			} 
		}
		
		return ret;
	}

	/**
	 * Converts the passed X-coordiate into a time value (in milliseconds) using
	 * the scale provided. Makes sure the return value is non-negative.
	 * 
	 * @param x
	 *            the x coordinate to use
	 * @param scale
	 *            the scale to use
	 * @return time in milliseconds
	 */
	protected static double getTimeForXCoordinate(int x, double scale) {
		double time = x * scale;
		// mouse event may return out of range X, that may
		// crash when we use it to index data array
		time = time >= 0 ? time : 0;
		return time;
	}
	
	/**
	 * Make the input value a pretty value to display, such as 423,345 => 500,000
	 * @param value the value to convert
	 * @return the prettier value
	 */
	static int prettyMaxValue(final int value) {
		double prettyVal = value;
		int len = 0;

		while (prettyVal >= 10) {
			prettyVal/= 10;
			len++;
		}
		
		prettyVal = Math.ceil(prettyVal);
		
		if (prettyVal <= 1){
			prettyVal = 1;
		} else if (prettyVal <= 2){
			prettyVal = 2;
		} else if (prettyVal <= 3){
			prettyVal = 3;
		} else if (prettyVal <= 5){
			prettyVal = 5;
		} else {
			prettyVal = 10;
		}
		
		return (int)(prettyVal * Math.pow(10, len));
	}

	/**
	 * Callback for PIEvent.SELECTION_AREA_CHANGED
	 * @param newStart new selection start
	 * @param newEnd new selection end
	 */
	void selectionAreaChanged(double newStart, double newEnd) {
		this.setSelectionStart(newStart);
		this.setSelectionEnd(newEnd);
		
		trace.selectionAreaChanged(newStart, newEnd);
		legend.refreshLegend();
		
		this.repaint();				
	}
	
	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.PIEventListener#piEventReceived(com.nokia.carbide.cpp.internal.pi.visual.PIEvent)
	 */
	public void piEventReceived(PIEvent be) {
		switch (be.getType()) {
		
		case PIEvent.SELECTION_AREA_CHANGED:
			double[] values = (double[]) be.getValueObject();
			//broadcast to all IPC graphs
			guiManager.selectionAreaChanged(values[0], values[1]);				
			break;

		case PIEvent.SCROLLED:
			Event event = ((Event) be.getValueObject());
			//this broadcasts to all graphs on this PICompositePanel
			this.parentComponent.setScrolledOrigin(event.x, event.y, (FigureCanvas)event.data);
			this.repaint();
			break;

		default:
			break;
		}
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#graphMaximized(boolean)
	 */
	@Override
	public void graphMaximized(boolean value) {
		legend.setLegendMaximised(value);
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.visual.GenericTraceGraph#graphVisibilityChanged(boolean)
	 */
	@Override
	public void graphVisibilityChanged(boolean value) {
		legend.setLegendVisible(value);
	}

	@Override
	public void action(String action) {
		// TODO Auto-generated method stub
		
	}
	
	/**
	 * Adds or removes a series to be drawn
	 * @param seriesId series id of the series to add or remove
	 * @param add true if series is to be added, false for remove
	 */
	public void addOrRemoveSeries(int seriesId, boolean add){
		if (add){
			if (!drawSeries.contains(seriesId)){
				drawSeries.add(seriesId);
				Collections.sort(drawSeries);
			}
		} else {
			drawSeries.remove(Integer.valueOf(seriesId));
		}
		
		//repaint graphs
		redrawGraphArea();
	}
	
	private void redrawGraphArea() {
		dirty = true;
		setGraphImageChanged(true);
		repaint();		
	}

	/**
	 * Removes all series from display
	 */
	public void removeAllSeries(){
		drawSeries.clear();
		redrawGraphArea();
		
	}

	/**
	 * Shows all series in graph area
	 */
	public void showAllSeries(){
		showAll();
		redrawGraphArea();
	}

	
	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.plugin.model.ITitleBarMenu#addTitleBarMenuItems()
	 */
	public Action[] addTitleBarMenuItems() {
		return null;
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.cpp.internal.pi.plugin.model.ITitleBarMenu#getContextHelpId()
	 */
	public String getContextHelpId() {
		return this.helpContextId;
	}

}