| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.gltrace.editors; |
| |
| import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function; |
| import com.android.ide.eclipse.gltrace.model.GLCall; |
| import com.android.ide.eclipse.gltrace.model.GLTrace; |
| |
| import org.eclipse.jface.resource.FontRegistry; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.MouseTrackListener; |
| import org.eclipse.swt.events.PaintEvent; |
| import org.eclipse.swt.events.PaintListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.FontData; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Canvas; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class DurationMinimap extends Canvas { |
| /** Default alpha value. */ |
| private static final int DEFAULT_ALPHA = 255; |
| |
| /** Alpha value for highlighting visible calls. */ |
| private static final int VISIBLE_CALLS_HIGHLIGHT_ALPHA = 50; |
| |
| /** Clamp call durations at this value. */ |
| private static final long CALL_DURATION_CLAMP = 20000; |
| |
| private static final String FONT_KEY = "default.font"; //$NON-NLS-1$ |
| |
| /** Scale font size by this amount to get the max display length of call duration. */ |
| private static final int MAX_DURATION_LENGTH_SCALE = 6; |
| |
| /** List of GL Calls in the trace. */ |
| private List<GLCall> mCalls; |
| |
| /** Number of GL contexts in the trace. */ |
| private int mContextCount; |
| |
| /** Starting call index of currently displayed frame. */ |
| private int mStartCallIndex; |
| |
| /** Ending call index of currently displayed frame. */ |
| private int mEndCallIndex; |
| |
| /** The top index that is currently visible in the table. */ |
| private int mVisibleCallTopIndex; |
| |
| /** The bottom index that is currently visible in the table. */ |
| private int mVisibleCallBottomIndex; |
| |
| private Color mBackgroundColor; |
| private Color mDurationLineColor; |
| private Color mGlDrawColor; |
| private Color mGlErrorColor; |
| private Color mContextHeaderColor; |
| private Color mVisibleCallsHighlightColor; |
| private Color mMouseMarkerColor; |
| |
| private FontRegistry mFontRegistry; |
| private int mFontWidth; |
| private int mFontHeight; |
| |
| // back buffers used for double buffering |
| private Image mBackBufferImage; |
| private GC mBackBufferGC; |
| |
| // mouse state |
| private boolean mMouseInSelf; |
| private int mMouseY; |
| |
| // helper object used to position various items on screen |
| private final PositionHelper mPositionHelper; |
| |
| public DurationMinimap(Composite parent, GLTrace trace) { |
| super(parent, SWT.NO_BACKGROUND); |
| |
| setInput(trace); |
| |
| initializeColors(); |
| initializeFonts(); |
| |
| mPositionHelper = new PositionHelper( |
| mFontHeight, |
| mContextCount, |
| mFontWidth * MAX_DURATION_LENGTH_SCALE, /* max display length for call. */ |
| CALL_DURATION_CLAMP /* max duration */); |
| |
| addPaintListener(new PaintListener() { |
| @Override |
| public void paintControl(PaintEvent e) { |
| draw(e.display, e.gc); |
| } |
| }); |
| |
| addListener(SWT.Resize, new Listener() { |
| @Override |
| public void handleEvent(Event event) { |
| controlResized(); |
| } |
| }); |
| |
| addMouseMoveListener(new MouseMoveListener() { |
| @Override |
| public void mouseMove(MouseEvent e) { |
| mouseMoved(e); |
| } |
| }); |
| |
| addMouseListener(new MouseAdapter() { |
| @Override |
| public void mouseUp(MouseEvent e) { |
| mouseClicked(e); |
| } |
| }); |
| |
| addMouseTrackListener(new MouseTrackListener() { |
| @Override |
| public void mouseHover(MouseEvent e) { |
| } |
| |
| @Override |
| public void mouseExit(MouseEvent e) { |
| mMouseInSelf = false; |
| redraw(); |
| } |
| |
| @Override |
| public void mouseEnter(MouseEvent e) { |
| mMouseInSelf = true; |
| redraw(); |
| } |
| }); |
| } |
| |
| public void setInput(GLTrace trace) { |
| if (trace != null) { |
| mCalls = trace.getGLCalls(); |
| mContextCount = trace.getContexts().size(); |
| } else { |
| mCalls = null; |
| mContextCount = 1; |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| disposeColors(); |
| disposeBackBuffer(); |
| super.dispose(); |
| } |
| |
| private void initializeColors() { |
| mBackgroundColor = new Color(getDisplay(), 0x33, 0x33, 0x33); |
| mDurationLineColor = new Color(getDisplay(), 0x08, 0x51, 0x9c); |
| mGlDrawColor = new Color(getDisplay(), 0x6b, 0xae, 0xd6); |
| mContextHeaderColor = new Color(getDisplay(), 0xd1, 0xe5, 0xf0); |
| mVisibleCallsHighlightColor = new Color(getDisplay(), 0xcc, 0xcc, 0xcc); |
| mMouseMarkerColor = new Color(getDisplay(), 0xaa, 0xaa, 0xaa); |
| |
| mGlErrorColor = getDisplay().getSystemColor(SWT.COLOR_RED); |
| } |
| |
| private void disposeColors() { |
| mBackgroundColor.dispose(); |
| mDurationLineColor.dispose(); |
| mGlDrawColor.dispose(); |
| mContextHeaderColor.dispose(); |
| mVisibleCallsHighlightColor.dispose(); |
| mMouseMarkerColor.dispose(); |
| } |
| |
| private void initializeFonts() { |
| mFontRegistry = new FontRegistry(getDisplay()); |
| mFontRegistry.put(FONT_KEY, |
| new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ |
| |
| GC gc = new GC(getDisplay()); |
| gc.setFont(mFontRegistry.get(FONT_KEY)); |
| mFontWidth = gc.getFontMetrics().getAverageCharWidth(); |
| mFontHeight = gc.getFontMetrics().getHeight(); |
| gc.dispose(); |
| } |
| |
| private void initializeBackBuffer() { |
| Rectangle clientArea = getClientArea(); |
| |
| if (clientArea.width == 0 || clientArea.height == 0) { |
| mBackBufferImage = null; |
| mBackBufferGC = null; |
| return; |
| } |
| |
| mBackBufferImage = new Image(getDisplay(), |
| clientArea.width, |
| clientArea.height); |
| mBackBufferGC = new GC(mBackBufferImage); |
| } |
| |
| private void disposeBackBuffer() { |
| if (mBackBufferImage != null) { |
| mBackBufferImage.dispose(); |
| mBackBufferImage = null; |
| } |
| |
| if (mBackBufferGC != null) { |
| mBackBufferGC.dispose(); |
| mBackBufferGC = null; |
| } |
| } |
| |
| private void mouseMoved(MouseEvent e) { |
| mMouseY = e.y; |
| redraw(); |
| } |
| |
| private void mouseClicked(MouseEvent e) { |
| if (mMouseInSelf) { |
| int selIndex = mPositionHelper.getCallAt(mMouseY); |
| sendCallSelectedEvent(selIndex); |
| redraw(); |
| } |
| } |
| |
| private void draw(Display display, GC gc) { |
| if (mBackBufferImage == null) { |
| initializeBackBuffer(); |
| } |
| |
| if (mBackBufferImage == null) { |
| return; |
| } |
| |
| // draw contents onto the back buffer |
| drawBackground(mBackBufferGC, mBackBufferImage.getBounds()); |
| drawContextHeaders(mBackBufferGC); |
| drawCallDurations(mBackBufferGC); |
| drawVisibleCallHighlights(mBackBufferGC); |
| drawMouseMarkers(mBackBufferGC); |
| |
| // finally copy over the rendered back buffer onto screen |
| int width = getClientArea().width; |
| int height = getClientArea().height; |
| gc.drawImage(mBackBufferImage, |
| 0, 0, width, height, |
| 0, 0, width, height); |
| } |
| |
| private void drawBackground(GC gc, Rectangle bounds) { |
| gc.setBackground(mBackgroundColor); |
| gc.fillRectangle(bounds); |
| } |
| |
| private void drawContextHeaders(GC gc) { |
| if (mContextCount <= 1) { |
| return; |
| } |
| |
| gc.setForeground(mContextHeaderColor); |
| gc.setFont(mFontRegistry.get(FONT_KEY)); |
| for (int i = 0; i < mContextCount; i++) { |
| Point p = mPositionHelper.getHeaderLocation(i); |
| gc.drawText("CTX" + Integer.toString(i), p.x, p.y); |
| } |
| } |
| |
| /** Draw the call durations as a sequence of lines. |
| * |
| * Calls are arranged on the y-axis based on the sequence in which they were originally |
| * called by the application. If the display height is lesser than the number of calls, then |
| * not every call is shown - the calls are underscanned based the height of the display. |
| * |
| * The x-axis shows two pieces of information: the duration of the call, and the context |
| * in which the call was made. The duration controls how long the displayed line is, and |
| * the context controls the starting offset of the line. |
| */ |
| private void drawCallDurations(GC gc) { |
| if (mCalls == null || mCalls.size() < mEndCallIndex) { |
| return; |
| } |
| |
| gc.setBackground(mDurationLineColor); |
| |
| int callUnderScan = mPositionHelper.getCallUnderScanValue(); |
| for (int i = mStartCallIndex; i < mEndCallIndex; i += callUnderScan) { |
| boolean resetColor = false; |
| GLCall c = mCalls.get(i); |
| |
| long duration = c.getWallDuration(); |
| |
| if (c.hasErrors()) { |
| gc.setBackground(mGlErrorColor); |
| resetColor = true; |
| |
| // If the call has any errors, we want it to be visible in the minimap |
| // regardless of how long it took. |
| duration = mPositionHelper.getMaxDuration(); |
| } else if (c.getFunction() == Function.glDrawArrays |
| || c.getFunction() == Function.glDrawElements |
| || c.getFunction() == Function.eglSwapBuffers) { |
| gc.setBackground(mGlDrawColor); |
| resetColor = true; |
| |
| // render all draw calls & swap buffer at max length |
| duration = mPositionHelper.getMaxDuration(); |
| } |
| |
| Rectangle bounds = mPositionHelper.getDurationBounds( |
| i - mStartCallIndex, |
| c.getContextId(), |
| duration); |
| gc.fillRectangle(bounds); |
| |
| if (resetColor) { |
| gc.setBackground(mDurationLineColor); |
| } |
| } |
| } |
| |
| /** |
| * Draw a bounding box that highlights the currently visible range of calls in the |
| * {@link GLFunctionTraceViewer} table. |
| */ |
| private void drawVisibleCallHighlights(GC gc) { |
| gc.setAlpha(VISIBLE_CALLS_HIGHLIGHT_ALPHA); |
| gc.setBackground(mVisibleCallsHighlightColor); |
| gc.fillRectangle(mPositionHelper.getBoundsFramingCalls( |
| mVisibleCallTopIndex - mStartCallIndex, |
| mVisibleCallBottomIndex - mStartCallIndex)); |
| gc.setAlpha(DEFAULT_ALPHA); |
| } |
| |
| private void drawMouseMarkers(GC gc) { |
| if (!mMouseInSelf) { |
| return; |
| } |
| |
| if (mPositionHelper.getCallAt(mMouseY) < 0) { |
| return; |
| } |
| |
| gc.setForeground(mMouseMarkerColor); |
| gc.drawLine(0, mMouseY, getClientArea().width, mMouseY); |
| } |
| |
| private void controlResized() { |
| // regenerate back buffer on size changes |
| disposeBackBuffer(); |
| initializeBackBuffer(); |
| |
| redraw(); |
| } |
| |
| public int getMinimumWidth() { |
| return mPositionHelper.getMinimumWidth(); |
| } |
| |
| /** Set the GL Call start and end indices for currently displayed frame. */ |
| public void setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex) { |
| mStartCallIndex = startCallIndex; |
| mEndCallIndex = endCallIndex; |
| mPositionHelper.updateCallDensity(mEndCallIndex - mStartCallIndex, getClientArea().height); |
| redraw(); |
| } |
| |
| /** |
| * Set the call range that is currently visible in the {@link GLFunctionTraceViewer} table. |
| * @param visibleTopIndex index of call currently visible at the top of the table. |
| * @param visibleBottomIndex index of call currently visible at the bottom of the table. |
| */ |
| public void setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex) { |
| mVisibleCallTopIndex = visibleTopIndex; |
| mVisibleCallBottomIndex = visibleBottomIndex; |
| redraw(); |
| } |
| |
| public interface ICallSelectionListener { |
| void callSelected(int selectedCallIndex); |
| } |
| |
| private List<ICallSelectionListener> mListeners = new ArrayList<ICallSelectionListener>(); |
| |
| public void addCallSelectionListener(ICallSelectionListener l) { |
| mListeners.add(l); |
| } |
| |
| private void sendCallSelectedEvent(int selectedCall) { |
| for (ICallSelectionListener l : mListeners) { |
| l.callSelected(selectedCall); |
| } |
| } |
| |
| /** Utility class to help with the positioning and sizes of elements in the canvas. */ |
| private static class PositionHelper { |
| /** Left Margin after which duration lines are drawn. */ |
| private static final int LEFT_MARGIN = 5; |
| |
| /** Top margin after which header is drawn. */ |
| private static final int TOP_MARGIN = 5; |
| |
| /** # of pixels of padding between duration markers for different contexts. */ |
| private static final int CONTEXT_PADDING = 10; |
| |
| private final int mHeaderMargin; |
| private final int mContextCount; |
| private final int mMaxDurationLength; |
| private final long mMaxDuration; |
| private final double mScale; |
| |
| private int mCallCount; |
| private int mNumCallsPerPixel = 1; |
| |
| public PositionHelper(int fontHeight, int contextCount, |
| int maxDurationLength, long maxDuration) { |
| mContextCount = contextCount; |
| mMaxDurationLength = maxDurationLength; |
| mMaxDuration = maxDuration; |
| mScale = (double) maxDurationLength / maxDuration; |
| |
| // header region is present only there are multiple contexts |
| if (mContextCount > 1) { |
| mHeaderMargin = fontHeight * 3; |
| } else { |
| mHeaderMargin = 0; |
| } |
| } |
| |
| /** Get the minimum width of the canvas. */ |
| public int getMinimumWidth() { |
| return LEFT_MARGIN + (mMaxDurationLength + CONTEXT_PADDING) * mContextCount; |
| } |
| |
| /** Get the bounds for a call duration line. */ |
| public Rectangle getDurationBounds(int callIndex, int context, long duration) { |
| if (duration <= 0) { |
| duration = 1; |
| } else if (duration > mMaxDuration) { |
| duration = mMaxDuration; |
| } |
| |
| int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); |
| int y = (callIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; |
| int w = (int) (duration * mScale); |
| int h = 1; |
| |
| return new Rectangle(x, y, w, h); |
| } |
| |
| public long getMaxDuration() { |
| return mMaxDuration; |
| } |
| |
| /** Get the bounds for calls spanning given range. */ |
| public Rectangle getBoundsFramingCalls(int startCallIndex, int endCallIndex) { |
| if (startCallIndex >= 0 && endCallIndex >= startCallIndex |
| && endCallIndex <= mCallCount) { |
| int x = LEFT_MARGIN; |
| int y = (startCallIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; |
| int w = ((mMaxDurationLength + CONTEXT_PADDING) * mContextCount); |
| int h = (endCallIndex - startCallIndex)/mNumCallsPerPixel; |
| return new Rectangle(x, y, w, h); |
| } else { |
| return new Rectangle(0, 0, 0, 0); |
| } |
| } |
| |
| public Point getHeaderLocation(int context) { |
| int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); |
| return new Point(x, TOP_MARGIN); |
| } |
| |
| /** Update the call density based on the number of calls to be displayed and |
| * the available height to display them in. */ |
| public void updateCallDensity(int callCount, int displayHeight) { |
| mCallCount = callCount; |
| |
| if (displayHeight <= 0) { |
| displayHeight = callCount + 1; |
| } |
| |
| mNumCallsPerPixel = (callCount / displayHeight) + 1; |
| } |
| |
| /** Get the underscan value. In cases where there are more calls to be displayed |
| * than there are availble pixels, we only display 1 out of every underscan calls. */ |
| public int getCallUnderScanValue() { |
| return mNumCallsPerPixel; |
| } |
| |
| /** Get the index of the call at given y offset. */ |
| public int getCallAt(int y) { |
| if (!isWithinBounds(y)) { |
| return -1; |
| } |
| |
| Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); |
| return (y - displayBounds.y) * mNumCallsPerPixel; |
| } |
| |
| /** Does the provided y offset map to a valid call? */ |
| private boolean isWithinBounds(int y) { |
| Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); |
| if (y < displayBounds.y) { |
| return false; |
| } |
| |
| if (y > (displayBounds.y + displayBounds.height)) { |
| return false; |
| } |
| |
| return true; |
| } |
| } |
| } |