diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors')
6 files changed, 2277 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java new file mode 100644 index 000000000..9b4c57cad --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java @@ -0,0 +1,541 @@ +/* + * 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; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java new file mode 100644 index 000000000..226d4831a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java @@ -0,0 +1,202 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; + +public class GLCallGroups { + /** + * A {@link GLCallNode} is a simple wrapper around a {@link GLCall} that + * adds the notion of hierarchy. + */ + public interface GLCallNode { + /** Does this call have child nodes? */ + boolean hasChildren(); + + /** Returns a list of child nodes of this call. */ + List<GLCallNode> getChildren(); + + /** Returns the {@link GLCall} that is wrapped by this node. */ + GLCall getCall(); + + /** Returns the parent of this node, the parent is null if this is a top level node */ + GLCallNode getParent(); + + /** Set the parent node. */ + void setParent(GLCallNode parent); + } + + private static class GLTreeNode implements GLCallNode { + private final GLCall mCall; + private GLCallNode mParent; + private List<GLCallNode> mGLCallNodes; + + public GLTreeNode(GLCall call) { + mCall = call; + mGLCallNodes = new ArrayList<GLCallNode>(); + } + + @Override + public boolean hasChildren() { + return true; + } + + @Override + public GLCallNode getParent() { + return mParent; + } + + @Override + public void setParent(GLCallNode parent) { + mParent = parent; + } + + @Override + public List<GLCallNode> getChildren() { + return mGLCallNodes; + } + + public void addChild(GLCallNode n) { + mGLCallNodes.add(n); + n.setParent(this); + } + + @Override + public GLCall getCall() { + return mCall; + } + } + + private static class GLLeafNode implements GLCallNode { + private final GLCall mCall; + private GLCallNode mParent; + + public GLLeafNode(GLCall call) { + mCall = call; + } + + @Override + public boolean hasChildren() { + return false; + } + + @Override + public List<GLCallNode> getChildren() { + return null; + } + + @Override + public GLCallNode getParent() { + return mParent; + } + + @Override + public void setParent(GLCallNode parent) { + mParent = parent; + } + + @Override + public GLCall getCall() { + return mCall; + } + } + + /** + * Impose a hierarchy on a list of {@link GLCall}'s based on the presence of + * {@link Function#glPushGroupMarkerEXT} and {@link Function#glPopGroupMarkerEXT} calls. + * Such a hierarchy is possible only if calls from a single context are considered. + * @param trace trace to look at + * @param start starting call index + * @param end ending call index + * @param contextToGroup context from which calls should be grouped. If no such context + * is present, then all calls in the given range will be returned back as a flat + * list. + * @return a tree structured list of {@link GLCallNode} objects + */ + public static List<GLCallNode> constructCallHierarchy(GLTrace trace, int start, int end, + int contextToGroup) { + if (trace == null) { + return Collections.emptyList(); + } + + if (contextToGroup < 0 || contextToGroup > trace.getContexts().size()) { + return flatHierarchy(trace, start, end); + } + + List<GLCall> calls = trace.getGLCalls(); + + Stack<GLTreeNode> hierarchyStack = new Stack<GLTreeNode>(); + List<GLCallNode> items = new ArrayList<GLCallNode>(); + + for (int i = start; i < end; i++) { + GLCall c = calls.get(i); + if (c.getContextId() != contextToGroup) { + // skip this call if it is not part of the context we need to display + continue; + } + + if (c.getFunction() == Function.glPushGroupMarkerEXT) { + GLTreeNode group = new GLTreeNode(c); + if (hierarchyStack.size() > 0) { + hierarchyStack.peek().addChild(group); + } else { + items.add(group); + } + hierarchyStack.push(group); + } else if (c.getFunction() == Function.glPopGroupMarkerEXT) { + if (hierarchyStack.size() > 0) { + hierarchyStack.pop(); + } else { + // FIXME: If we are attempting to pop from an empty stack, + // that implies that a push marker was seen in a prior frame + // (in a call before @start). In such a case, we simply continue + // adding further calls to the root of the hierarchy rather than + // searching backwards in the call list for the corresponding + // push markers. + items.add(new GLLeafNode(c)); + } + } else { + GLLeafNode leaf = new GLLeafNode(c); + if (hierarchyStack.size() > 0) { + hierarchyStack.peek().addChild(leaf); + } else { + items.add(leaf); + } + } + } + + return items; + } + + private static List<GLCallNode> flatHierarchy(GLTrace trace, int start, int end) { + List<GLCallNode> items = new ArrayList<GLCallNode>(); + + List<GLCall> calls = trace.getGLCalls(); + for (int i = start; i < end; i++) { + items.add(new GLLeafNode(calls.get(i))); + } + + return items; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java new file mode 100644 index 000000000..b809ddddf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java @@ -0,0 +1,984 @@ +/* + * Copyright (C) 2011 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.ddmuilib.AbstractBufferFindTarget; +import com.android.ddmuilib.FindDialog; +import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function; +import com.android.ide.eclipse.gltrace.GlTracePlugin; +import com.android.ide.eclipse.gltrace.SwtUtils; +import com.android.ide.eclipse.gltrace.TraceFileParserTask; +import com.android.ide.eclipse.gltrace.editors.DurationMinimap.ICallSelectionListener; +import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode; +import com.android.ide.eclipse.gltrace.model.GLCall; +import com.android.ide.eclipse.gltrace.model.GLFrame; +import com.android.ide.eclipse.gltrace.model.GLTrace; +import com.android.ide.eclipse.gltrace.views.FrameSummaryViewPage; +import com.android.ide.eclipse.gltrace.views.detail.DetailsPage; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +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.jface.action.Action; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Scale; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Spinner; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IURIEditorInput; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.part.EditorPart; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Display OpenGL function trace in a tabular view. */ +public class GLFunctionTraceViewer extends EditorPart implements ISelectionProvider { + public static final String ID = "com.android.ide.eclipse.gltrace.GLFunctionTrace"; //$NON-NLS-1$ + + private static final String DEFAULT_FILTER_MESSAGE = "Filter list of OpenGL calls. Accepts Java regexes."; + private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$ + + private static Image sExpandAllIcon; + + private static String sLastExportedToFolder; + + private String mFilePath; + private Scale mFrameSelectionScale; + private Spinner mFrameSelectionSpinner; + + private GLTrace mTrace; + + private TreeViewer mFrameTreeViewer; + private List<GLCallNode> mTreeViewerNodes; + + private Text mFilterText; + private GLCallFilter mGLCallFilter; + + private Color mGldrawTextColor; + private Color mGlCallErrorColor; + + /** + * Job to refresh the tree view & frame summary view. + * + * When the currently displayed frame is changed, either via the {@link #mFrameSelectionScale} + * or via {@link #mFrameSelectionSpinner}, we need to update the displayed tree of calls for + * that frame, and the frame summary view. Both these operations need to happen on the UI + * thread, but are time consuming. This works out ok if the frame selection is not changing + * rapidly (i.e., when the spinner or scale is moved to the target frame in a single action). + * However, if the spinner is constantly pressed, then the user is scrolling through a sequence + * of frames, and rather than refreshing the details for each of the intermediate frames, + * we create a job to refresh the details and schedule the job after a short interval + * {@link #TREE_REFRESH_INTERVAL}. This allows us to stay responsive to the spinner/scale, + * and not do the costly refresh for each of the intermediate frames. + */ + private Job mTreeRefresherJob; + private final Object mTreeRefresherLock = new Object(); + private static final int TREE_REFRESH_INTERVAL_MS = 250; + + private int mCurrentFrame; + + // Currently displayed frame's start and end call indices. + private int mCallStartIndex; + private int mCallEndIndex; + + private DurationMinimap mDurationMinimap; + private ScrollBar mVerticalScrollBar; + + private Combo mContextSwitchCombo; + private boolean mShowContextSwitcher; + private int mCurrentlyDisplayedContext = -1; + + private StateViewPage mStateViewPage; + private FrameSummaryViewPage mFrameSummaryViewPage; + private DetailsPage mDetailsPage; + + private ToolItem mExpandAllToolItem; + private ToolItem mCollapseAllToolItem; + private ToolItem mSaveAsToolItem; + + public GLFunctionTraceViewer() { + mGldrawTextColor = Display.getDefault().getSystemColor(SWT.COLOR_BLUE); + mGlCallErrorColor = Display.getDefault().getSystemColor(SWT.COLOR_RED); + } + + @Override + public void doSave(IProgressMonitor monitor) { + } + + @Override + public void doSaveAs() { + } + + @Override + public void init(IEditorSite site, IEditorInput input) throws PartInitException { + // we use a IURIEditorInput to allow opening files not within the workspace + if (!(input instanceof IURIEditorInput)) { + throw new PartInitException("GL Function Trace View: unsupported input type."); + } + + setSite(site); + setInput(input); + mFilePath = ((IURIEditorInput) input).getURI().getPath(); + + // set the editor part name to be the name of the file. + File f = new File(mFilePath); + setPartName(f.getName()); + } + + @Override + public boolean isDirty() { + return false; + } + + @Override + public boolean isSaveAsAllowed() { + return false; + } + + @Override + public void createPartControl(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(1, false)); + GridData gd = new GridData(GridData.FILL_BOTH); + c.setLayoutData(gd); + + setInput(parent.getShell(), mFilePath); + + createFrameSelectionControls(c); + createOptionsBar(c); + createFrameTraceView(c); + + getSite().setSelectionProvider(mFrameTreeViewer); + + IActionBars actionBars = getEditorSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), + new Action("Copy") { + @Override + public void run() { + copySelectionToClipboard(); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), + new Action("Select All") { + @Override + public void run() { + selectAll(); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), + new Action("Find") { + @Override + public void run() { + showFindDialog(); + } + }); + } + + public void setInput(Shell shell, String tracePath) { + ProgressMonitorDialog dlg = new ProgressMonitorDialog(shell); + TraceFileParserTask parser = new TraceFileParserTask(mFilePath); + try { + dlg.run(true, true, parser); + } catch (InvocationTargetException e) { + // exception while parsing, display error to user + MessageDialog.openError(shell, + "Error parsing OpenGL Trace File", + e.getCause().getMessage()); + return; + } catch (InterruptedException e) { + // operation canceled by user, just return + return; + } + + mTrace = parser.getTrace(); + mShowContextSwitcher = (mTrace == null) ? false : mTrace.getContexts().size() > 1; + if (mStateViewPage != null) { + mStateViewPage.setInput(mTrace); + } + if (mFrameSummaryViewPage != null) { + mFrameSummaryViewPage.setInput(mTrace); + } + if (mDetailsPage != null) { + mDetailsPage.setInput(mTrace); + } + if (mDurationMinimap != null) { + mDurationMinimap.setInput(mTrace); + } + + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + refreshUI(); + } + }); + } + + private void refreshUI() { + if (mTrace == null || mTrace.getGLCalls().size() == 0) { + setFrameCount(0); + return; + } + + setFrameCount(mTrace.getFrames().size()); + selectFrame(1); + } + + private void createFrameSelectionControls(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(3, false)); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + c.setLayoutData(gd); + + Label l = new Label(c, SWT.NONE); + l.setText("Select Frame:"); + + mFrameSelectionScale = new Scale(c, SWT.HORIZONTAL); + mFrameSelectionScale.setMinimum(1); + mFrameSelectionScale.setMaximum(1); + mFrameSelectionScale.setSelection(0); + gd = new GridData(GridData.FILL_HORIZONTAL); + mFrameSelectionScale.setLayoutData(gd); + + mFrameSelectionScale.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int selectedFrame = mFrameSelectionScale.getSelection(); + mFrameSelectionSpinner.setSelection(selectedFrame); + selectFrame(selectedFrame); + } + }); + + mFrameSelectionSpinner = new Spinner(c, SWT.BORDER); + gd = new GridData(); + // width to hold atleast 6 digits + gd.widthHint = SwtUtils.getApproximateFontWidth(mFrameSelectionSpinner) * 6; + mFrameSelectionSpinner.setLayoutData(gd); + + mFrameSelectionSpinner.setMinimum(1); + mFrameSelectionSpinner.setMaximum(1); + mFrameSelectionSpinner.setSelection(0); + mFrameSelectionSpinner.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int selectedFrame = mFrameSelectionSpinner.getSelection(); + mFrameSelectionScale.setSelection(selectedFrame); + selectFrame(selectedFrame); + } + }); + } + + private void setFrameCount(int nFrames) { + boolean en = nFrames > 0; + mFrameSelectionScale.setEnabled(en); + mFrameSelectionSpinner.setEnabled(en); + + mFrameSelectionScale.setMaximum(nFrames); + mFrameSelectionSpinner.setMaximum(nFrames); + } + + private void selectFrame(int selectedFrame) { + mFrameSelectionScale.setSelection(selectedFrame); + mFrameSelectionSpinner.setSelection(selectedFrame); + + synchronized (mTreeRefresherLock) { + if (mTrace != null) { + GLFrame f = mTrace.getFrame(selectedFrame - 1); + mCallStartIndex = f.getStartIndex(); + mCallEndIndex = f.getEndIndex(); + } else { + mCallStartIndex = mCallEndIndex = 0; + } + + mCurrentFrame = selectedFrame - 1; + + scheduleNewRefreshJob(); + } + + // update minimap view + mDurationMinimap.setCallRangeForCurrentFrame(mCallStartIndex, mCallEndIndex); + } + + /** + * Show only calls from the given context + * @param context context id whose calls should be displayed. Illegal values will result in + * calls from all contexts being displayed. + */ + private void selectContext(int context) { + if (mCurrentlyDisplayedContext == context) { + return; + } + + synchronized (mTreeRefresherLock) { + mCurrentlyDisplayedContext = context; + scheduleNewRefreshJob(); + } + } + + private void scheduleNewRefreshJob() { + if (mTreeRefresherJob != null) { + return; + } + + mTreeRefresherJob = new Job("Refresh GL Trace View Tree") { + @Override + protected IStatus run(IProgressMonitor monitor) { + final int start, end, context; + + synchronized (mTreeRefresherLock) { + start = mCallStartIndex; + end = mCallEndIndex; + context = mCurrentlyDisplayedContext; + + mTreeRefresherJob = null; + } + + // update tree view in the editor + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + refreshTree(start, end, context); + + // update the frame summary view + if (mFrameSummaryViewPage != null) { + mFrameSummaryViewPage.setSelectedFrame(mCurrentFrame); + } + } + }); + return Status.OK_STATUS; + } + }; + mTreeRefresherJob.setPriority(Job.SHORT); + mTreeRefresherJob.schedule(TREE_REFRESH_INTERVAL_MS); + } + + private void refreshTree(int startCallIndex, int endCallIndex, int contextToDisplay) { + mTreeViewerNodes = GLCallGroups.constructCallHierarchy(mTrace, + startCallIndex, endCallIndex, + contextToDisplay); + mFrameTreeViewer.setInput(mTreeViewerNodes); + mFrameTreeViewer.refresh(); + mFrameTreeViewer.expandAll(); + } + + private void createOptionsBar(Composite parent) { + int numColumns = mShowContextSwitcher ? 4 : 3; + + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(numColumns, false)); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + c.setLayoutData(gd); + + Label l = new Label(c, SWT.NONE); + l.setText("Filter:"); + + mFilterText = new Text(c, SWT.BORDER | SWT.ICON_SEARCH | SWT.SEARCH | SWT.ICON_CANCEL); + mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterText.setMessage(DEFAULT_FILTER_MESSAGE); + mFilterText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + updateAppliedFilters(); + } + }); + + if (mShowContextSwitcher) { + mContextSwitchCombo = new Combo(c, SWT.BORDER | SWT.READ_ONLY); + + // Setup the combo such that "All Contexts" is the first item, + // and then we have an item for each context. + mContextSwitchCombo.add("All Contexts"); + mContextSwitchCombo.select(0); + mCurrentlyDisplayedContext = -1; // showing all contexts + for (int i = 0; i < mTrace.getContexts().size(); i++) { + mContextSwitchCombo.add("Context " + i); + } + + mContextSwitchCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + selectContext(mContextSwitchCombo.getSelectionIndex() - 1); + } + }); + } else { + mCurrentlyDisplayedContext = 0; + } + + ToolBar toolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); + + mExpandAllToolItem = new ToolItem(toolBar, SWT.PUSH); + mExpandAllToolItem.setToolTipText("Expand All"); + if (sExpandAllIcon == null) { + ImageDescriptor id = GlTracePlugin.getImageDescriptor("/icons/expandall.png"); + sExpandAllIcon = id.createImage(); + } + if (sExpandAllIcon != null) { + mExpandAllToolItem.setImage(sExpandAllIcon); + } + + mCollapseAllToolItem = new ToolItem(toolBar, SWT.PUSH); + mCollapseAllToolItem.setToolTipText("Collapse All"); + mCollapseAllToolItem.setImage( + PlatformUI.getWorkbench().getSharedImages().getImage( + ISharedImages.IMG_ELCL_COLLAPSEALL)); + + mSaveAsToolItem = new ToolItem(toolBar, SWT.PUSH); + mSaveAsToolItem.setToolTipText("Export Trace"); + mSaveAsToolItem.setImage( + PlatformUI.getWorkbench().getSharedImages().getImage( + ISharedImages.IMG_ETOOL_SAVEAS_EDIT)); + + SelectionListener toolbarSelectionListener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == mCollapseAllToolItem) { + setTreeItemsExpanded(false); + } else if (e.getSource() == mExpandAllToolItem) { + setTreeItemsExpanded(true); + } else if (e.getSource() == mSaveAsToolItem) { + exportTrace(); + } + } + }; + mExpandAllToolItem.addSelectionListener(toolbarSelectionListener); + mCollapseAllToolItem.addSelectionListener(toolbarSelectionListener); + mSaveAsToolItem.addSelectionListener(toolbarSelectionListener); + } + + private void updateAppliedFilters() { + mGLCallFilter.setFilters(mFilterText.getText().trim()); + mFrameTreeViewer.refresh(); + } + + private void createFrameTraceView(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(2, false)); + GridData gd = new GridData(GridData.FILL_BOTH); + c.setLayoutData(gd); + + final Tree tree = new Tree(c, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); + gd = new GridData(GridData.FILL_BOTH); + tree.setLayoutData(gd); + tree.setLinesVisible(true); + tree.setHeaderVisible(true); + + mFrameTreeViewer = new TreeViewer(tree); + CellLabelProvider labelProvider = new GLFrameLabelProvider(); + + // column showing the GL context id + TreeViewerColumn tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE); + tvc.setLabelProvider(labelProvider); + TreeColumn column = tvc.getColumn(); + column.setText("Function"); + column.setWidth(500); + + // column showing the GL function duration (wall clock time) + tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE); + tvc.setLabelProvider(labelProvider); + column = tvc.getColumn(); + column.setText("Wall Time (ns)"); + column.setWidth(150); + column.setAlignment(SWT.RIGHT); + + // column showing the GL function duration (thread time) + tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE); + tvc.setLabelProvider(labelProvider); + column = tvc.getColumn(); + column.setText("Thread Time (ns)"); + column.setWidth(150); + column.setAlignment(SWT.RIGHT); + + mFrameTreeViewer.setContentProvider(new GLFrameContentProvider()); + + mGLCallFilter = new GLCallFilter(); + mFrameTreeViewer.addFilter(mGLCallFilter); + + // when the control is resized, give all the additional space + // to the function name column. + tree.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + int w = mFrameTreeViewer.getTree().getClientArea().width; + if (w > 200) { + mFrameTreeViewer.getTree().getColumn(2).setWidth(100); + mFrameTreeViewer.getTree().getColumn(1).setWidth(100); + mFrameTreeViewer.getTree().getColumn(0).setWidth(w - 200); + } + } + }); + + mDurationMinimap = new DurationMinimap(c, mTrace); + gd = new GridData(GridData.FILL_VERTICAL); + gd.widthHint = gd.minimumWidth = mDurationMinimap.getMinimumWidth(); + mDurationMinimap.setLayoutData(gd); + mDurationMinimap.addCallSelectionListener(new ICallSelectionListener() { + @Override + public void callSelected(int selectedCallIndex) { + if (selectedCallIndex > 0 && selectedCallIndex < mTreeViewerNodes.size()) { + TreeItem item = tree.getItem(selectedCallIndex); + tree.select(item); + tree.setTopItem(item); + } + } + }); + + mVerticalScrollBar = tree.getVerticalBar(); + mVerticalScrollBar.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateVisibleRange(); + } + }); + } + + private void updateVisibleRange() { + int visibleCallTopIndex = mCallStartIndex; + int visibleCallBottomIndex = mCallEndIndex; + + if (mVerticalScrollBar.isEnabled()) { + int selection = mVerticalScrollBar.getSelection(); + int thumb = mVerticalScrollBar.getThumb(); + int max = mVerticalScrollBar.getMaximum(); + + // from the scrollbar values, compute the visible fraction + double top = (double) selection / max; + double bottom = (double) (selection + thumb) / max; + + // map the fraction to the call indices + int range = mCallEndIndex - mCallStartIndex; + visibleCallTopIndex = mCallStartIndex + (int) Math.floor(range * top); + visibleCallBottomIndex = mCallStartIndex + (int) Math.ceil(range * bottom); + } + + mDurationMinimap.setVisibleCallRange(visibleCallTopIndex, visibleCallBottomIndex); + } + + @Override + public void setFocus() { + mFrameTreeViewer.getTree().setFocus(); + } + + private static class GLFrameContentProvider implements ITreeContentProvider { + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof List<?>) { + return ((List<?>) parentElement).toArray(); + } + + if (!(parentElement instanceof GLCallNode)) { + return null; + } + + GLCallNode parent = (GLCallNode) parentElement; + if (parent.hasChildren()) { + return parent.getChildren().toArray(); + } else { + return new Object[0]; + } + } + + @Override + public Object getParent(Object element) { + if (!(element instanceof GLCallNode)) { + return null; + } + + return ((GLCallNode) element).getParent(); + } + + @Override + public boolean hasChildren(Object element) { + if (!(element instanceof GLCallNode)) { + return false; + } + + return ((GLCallNode) element).hasChildren(); + } + } + + private class GLFrameLabelProvider extends ColumnLabelProvider { + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + if (!(element instanceof GLCallNode)) { + return; + } + + GLCall c = ((GLCallNode) element).getCall(); + + if (c.getFunction() == Function.glDrawArrays + || c.getFunction() == Function.glDrawElements) { + cell.setForeground(mGldrawTextColor); + } + + if (c.hasErrors()) { + cell.setForeground(mGlCallErrorColor); + } + + cell.setText(getColumnText(c, cell.getColumnIndex())); + } + + private String getColumnText(GLCall c, int columnIndex) { + switch (columnIndex) { + case 0: + if (c.getFunction() == Function.glPushGroupMarkerEXT) { + Object marker = c.getProperty(GLCall.PROPERTY_MARKERNAME); + if (marker instanceof String) { + return ((String) marker); + } + } + return c.toString(); + case 1: + return formatDuration(c.getWallDuration()); + case 2: + return formatDuration(c.getThreadDuration()); + default: + return Integer.toString(c.getContextId()); + } + } + + private String formatDuration(int time) { + // Max duration is in the 10s of milliseconds, so xx,xxx,xxx ns + // So we require a format specifier that is 10 characters wide + return String.format("%,10d", time); //$NON-NLS-1$ + } + } + + private static class GLCallFilter extends ViewerFilter { + private final List<Pattern> mPatterns = new ArrayList<Pattern>(); + + public void setFilters(String filter) { + mPatterns.clear(); + + // split the user input into multiple regexes + // we assume that the regexes are OR'ed together i.e., all text that matches + // any one of the regexes will be displayed + for (String regex : filter.split(" ")) { + mPatterns.add(Pattern.compile(regex, Pattern.CASE_INSENSITIVE)); + } + } + + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + if (!(element instanceof GLCallNode)) { + return true; + } + + String text = getTextUnderNode((GLCallNode) element); + + if (mPatterns.size() == 0) { + // match if there are no regex filters + return true; + } + + for (Pattern p : mPatterns) { + Matcher matcher = p.matcher(text); + if (matcher.find()) { + // match if atleast one of the regexes matches this text + return true; + } + } + + return false; + } + + /** Obtain a string representation of all functions under a given tree node. */ + private String getTextUnderNode(GLCallNode element) { + String func = element.getCall().getFunction().toString(); + if (!element.hasChildren()) { + return func; + } + + StringBuilder sb = new StringBuilder(100); + sb.append(func); + + for (GLCallNode child : element.getChildren()) { + sb.append(getTextUnderNode(child)); + } + + return sb.toString(); + } + } + + @Override + public void addSelectionChangedListener(ISelectionChangedListener listener) { + if (mFrameTreeViewer != null) { + mFrameTreeViewer.addSelectionChangedListener(listener); + } + } + + @Override + public ISelection getSelection() { + if (mFrameTreeViewer != null) { + return mFrameTreeViewer.getSelection(); + } else { + return null; + } + } + + @Override + public void removeSelectionChangedListener(ISelectionChangedListener listener) { + if (mFrameTreeViewer != null) { + mFrameTreeViewer.removeSelectionChangedListener(listener); + } + } + + @Override + public void setSelection(ISelection selection) { + if (mFrameTreeViewer != null) { + mFrameTreeViewer.setSelection(selection); + } + } + + public GLTrace getTrace() { + return mTrace; + } + + public StateViewPage getStateViewPage() { + if (mStateViewPage == null) { + mStateViewPage = new StateViewPage(mTrace); + } + + return mStateViewPage; + } + + public FrameSummaryViewPage getFrameSummaryViewPage() { + if (mFrameSummaryViewPage == null) { + mFrameSummaryViewPage = new FrameSummaryViewPage(mTrace); + } + + return mFrameSummaryViewPage; + } + + public DetailsPage getDetailsPage() { + if (mDetailsPage == null) { + mDetailsPage = new DetailsPage(mTrace); + } + + return mDetailsPage; + } + + private void copySelectionToClipboard() { + if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) { + return; + } + + StringBuilder sb = new StringBuilder(); + + for (TreeItem it: mFrameTreeViewer.getTree().getSelection()) { + Object data = it.getData(); + if (data instanceof GLCallNode) { + sb.append(((GLCallNode) data).getCall()); + sb.append(NEWLINE); + } + } + + if (sb.length() > 0) { + Clipboard cb = new Clipboard(Display.getDefault()); + cb.setContents( + new Object[] { sb.toString() }, + new Transfer[] { TextTransfer.getInstance() }); + cb.dispose(); + } + } + + private void selectAll() { + if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) { + return; + } + + mFrameTreeViewer.getTree().selectAll(); + } + + private void exportTrace() { + if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) { + return; + } + + if (mCallEndIndex == 0) { + return; + } + + FileDialog fd = new FileDialog(mFrameTreeViewer.getTree().getShell(), SWT.SAVE); + fd.setFilterExtensions(new String[] { "*.txt" }); + if (sLastExportedToFolder != null) { + fd.setFilterPath(sLastExportedToFolder); + } + + String path = fd.open(); + if (path == null) { + return; + } + + File f = new File(path); + sLastExportedToFolder = f.getParent(); + try { + exportFrameTo(f); + } catch (IOException e) { + ErrorDialog.openError(mFrameTreeViewer.getTree().getShell(), + "Export trace file.", + "Unexpected error exporting trace file.", + new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString())); + } + } + + private void exportFrameTo(File f) throws IOException { + String glCalls = serializeGlCalls(mTrace.getGLCalls(), mCallStartIndex, mCallEndIndex); + Files.write(glCalls, f, Charsets.UTF_8); + } + + private String serializeGlCalls(List<GLCall> glCalls, int start, int end) { + StringBuilder sb = new StringBuilder(); + while (start < end) { + sb.append(glCalls.get(start).toString()); + sb.append("\n"); //$NON-NLS-1$ + start++; + } + + return sb.toString(); + } + + private void setTreeItemsExpanded(boolean expand) { + if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) { + return; + } + + if (expand) { + mFrameTreeViewer.expandAll(); + } else { + mFrameTreeViewer.collapseAll(); + } + } + + private class TraceViewerFindTarget extends AbstractBufferFindTarget { + @Override + public int getItemCount() { + return mFrameTreeViewer.getTree().getItemCount(); + } + + @Override + public String getItem(int index) { + Object data = mFrameTreeViewer.getTree().getItem(index).getData(); + if (data instanceof GLCallNode) { + return ((GLCallNode) data).getCall().toString(); + } + return null; + } + + @Override + public void selectAndReveal(int index) { + Tree t = mFrameTreeViewer.getTree(); + t.deselectAll(); + t.select(t.getItem(index)); + t.showSelection(); + } + + @Override + public int getStartingIndex() { + return 0; + } + }; + + private FindDialog mFindDialog; + private TraceViewerFindTarget mFindTarget = new TraceViewerFindTarget(); + + private void showFindDialog() { + if (mFindDialog != null) { + // the dialog is already displayed + return; + } + + mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), + mFindTarget, + FindDialog.FIND_NEXT_ID); + mFindDialog.open(); // blocks until find dialog is closed + mFindDialog = null; + } + + public String getInputPath() { + return mFilePath; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java new file mode 100644 index 000000000..7bff168fc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 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.state.GLCompositeProperty; +import com.android.ide.eclipse.gltrace.state.GLListProperty; +import com.android.ide.eclipse.gltrace.state.GLSparseArrayProperty; +import com.android.ide.eclipse.gltrace.state.IGLProperty; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +public class StateContentProvider implements ITreeContentProvider { + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof GLListProperty) { + return ((GLListProperty) parentElement).getList().toArray(); + } + + if (parentElement instanceof GLCompositeProperty) { + return ((GLCompositeProperty) parentElement).getProperties().toArray(); + } + + if (parentElement instanceof GLSparseArrayProperty) { + return ((GLSparseArrayProperty) parentElement).getValues().toArray(); + } + + return null; + } + + @Override + public Object getParent(Object element) { + if (element instanceof IGLProperty) { + return ((IGLProperty) element).getParent(); + } + + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof IGLProperty) { + return ((IGLProperty) element).isComposite(); + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java new file mode 100644 index 000000000..e37ea77fd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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.state.GLListProperty; +import com.android.ide.eclipse.gltrace.state.GLSparseArrayProperty; +import com.android.ide.eclipse.gltrace.state.GLStateType; +import com.android.ide.eclipse.gltrace.state.IGLProperty; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; + +import java.util.Set; + +public class StateLabelProvider extends ColumnLabelProvider { + private Set<IGLProperty> mChangedProperties; + + private Color mHighlightForegroundColor; + private Color mNormalForegroundColor; + + public StateLabelProvider() { + mHighlightForegroundColor = Display.getDefault().getSystemColor(SWT.COLOR_BLUE); + mNormalForegroundColor = Display.getDefault().getSystemColor(SWT.COLOR_BLACK); + } + + public String getColumnText(IGLProperty property, int columnIndex) { + switch (columnIndex) { + case 0: + return getName(property); + case 1: + return getValue(property); + default: + return ""; + } + } + + private String getValue(IGLProperty element) { + return element.getStringValue(); + } + + private String getName(IGLProperty element) { + IGLProperty parent = element.getParent(); + if (parent instanceof GLListProperty) { + // For members of list, use the index in the list as the name as opposed to + // the property type + int index = ((GLListProperty) parent).indexOf(element); + if (element.getType() == GLStateType.GL_STATE_ES1) { + return String.format("Context %d (ES1)", index); + } else if (element.getType() == GLStateType.GL_STATE_ES2) { + return String.format("Context %d (ES2)", index); + } else { + return Integer.toString(index); + } + } else if (parent instanceof GLSparseArrayProperty) { + // For members of sparse array, use the key as the name as opposed to + // the property type + int index = ((GLSparseArrayProperty) parent).keyFor(element); + return Integer.toString(index); + } + + return element.getType().getDescription(); + } + + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + if (!(element instanceof IGLProperty)) { + return; + } + + IGLProperty prop = (IGLProperty) element; + + String text = getColumnText(prop, cell.getColumnIndex()); + cell.setText(text); + + if (mChangedProperties != null && mChangedProperties.contains(prop)) { + cell.setForeground(mHighlightForegroundColor); + } else { + cell.setForeground(mNormalForegroundColor); + } + } + + public void setChangedProperties(Set<IGLProperty> changedProperties) { + mChangedProperties = changedProperties; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java new file mode 100644 index 000000000..faa9561cb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2011 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.GlTracePlugin; +import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode; +import com.android.ide.eclipse.gltrace.model.GLCall; +import com.android.ide.eclipse.gltrace.model.GLTrace; +import com.android.ide.eclipse.gltrace.state.GLState; +import com.android.ide.eclipse.gltrace.state.IGLProperty; +import com.android.ide.eclipse.gltrace.state.StatePrettyPrinter; +import com.android.ide.eclipse.gltrace.state.transforms.IStateTransform; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.ILock; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.ui.ISelectionListener; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.IPageSite; +import org.eclipse.ui.part.Page; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A tree view of the OpenGL state. It listens to the current GLCall that is selected + * in the Function Trace view, and updates its view to reflect the state as of the selected call. + */ +public class StateViewPage extends Page implements ISelectionListener, ISelectionProvider { + public static final String ID = "com.android.ide.eclipse.gltrace.views.GLState"; //$NON-NLS-1$ + private static String sLastUsedPath; + private static final ILock sGlStateLock = Job.getJobManager().newLock(); + + private GLTrace mTrace; + private List<GLCall> mGLCalls; + + /** OpenGL State as of call {@link #mCurrentStateIndex}. */ + private IGLProperty mState; + private int mCurrentStateIndex; + + private String[] TREE_PROPERTIES = { "Name", "Value" }; + private TreeViewer mTreeViewer; + private StateLabelProvider mLabelProvider; + + public StateViewPage(GLTrace trace) { + setInput(trace); + } + + public void setInput(GLTrace trace) { + mTrace = trace; + if (trace != null) { + mGLCalls = trace.getGLCalls(); + } else { + mGLCalls = null; + } + + mState = GLState.createDefaultState(); + mCurrentStateIndex = -1; + + if (mTreeViewer != null) { + mTreeViewer.setInput(mState); + mTreeViewer.refresh(); + } + } + + @Override + public void createControl(Composite parent) { + final Tree tree = new Tree(parent, SWT.VIRTUAL | SWT.H_SCROLL | SWT.V_SCROLL); + GridDataFactory.fillDefaults().grab(true, true).applyTo(tree); + + tree.setHeaderVisible(true); + tree.setLinesVisible(true); + tree.setLayoutData(new GridData(GridData.FILL_BOTH)); + + TreeColumn col1 = new TreeColumn(tree, SWT.LEFT); + col1.setText(TREE_PROPERTIES[0]); + col1.setWidth(200); + + TreeColumn col2 = new TreeColumn(tree, SWT.LEFT); + col2.setText(TREE_PROPERTIES[1]); + col2.setWidth(200); + + mTreeViewer = new TreeViewer(tree); + mTreeViewer.setContentProvider(new StateContentProvider()); + mLabelProvider = new StateLabelProvider(); + mTreeViewer.setLabelProvider(mLabelProvider); + mTreeViewer.setInput(mState); + mTreeViewer.refresh(); + + final IToolBarManager manager = getSite().getActionBars().getToolBarManager(); + manager.add(new Action("Save to File", + PlatformUI.getWorkbench().getSharedImages().getImageDescriptor( + ISharedImages.IMG_ETOOL_SAVEAS_EDIT)) { + @Override + public void run() { + saveCurrentState(); + } + }); + } + + private void saveCurrentState() { + final Shell shell = mTreeViewer.getTree().getShell(); + FileDialog fd = new FileDialog(shell, SWT.SAVE); + fd.setFilterExtensions(new String[] { "*.txt" }); + if (sLastUsedPath != null) { + fd.setFilterPath(sLastUsedPath); + } + + String path = fd.open(); + if (path == null) { + return; + } + + File f = new File(path); + sLastUsedPath = f.getParent(); + + // export state to f + StatePrettyPrinter pp = new StatePrettyPrinter(); + synchronized (sGlStateLock) { + mState.prettyPrint(pp); + } + + try { + Files.write(pp.toString(), f, Charsets.UTF_8); + } catch (IOException e) { + ErrorDialog.openError(shell, + "Export GL State", + "Unexpected error while writing GL state to file.", + new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString())); + } + } + + @Override + public void init(IPageSite pageSite) { + super.init(pageSite); + pageSite.getPage().addSelectionListener(this); + } + + @Override + public void dispose() { + getSite().getPage().removeSelectionListener(this); + super.dispose(); + } + + @Override + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + if (!(part instanceof GLFunctionTraceViewer)) { + return; + } + + if (((GLFunctionTraceViewer) part).getTrace() != mTrace) { + return; + } + + if (!(selection instanceof TreeSelection)) { + return; + } + + GLCall selectedCall = null; + + Object data = ((TreeSelection) selection).getFirstElement(); + if (data instanceof GLCallNode) { + selectedCall = ((GLCallNode) data).getCall(); + } + + if (selectedCall == null) { + return; + } + + final int selectedCallIndex = selectedCall.getIndex(); + + // Creation of texture images takes a few seconds on the first run. So run + // the update task as an Eclipse job. + Job job = new Job("Updating GL State") { + @Override + protected IStatus run(IProgressMonitor monitor) { + Set<IGLProperty> changedProperties = null; + + try { + sGlStateLock.acquire(); + changedProperties = updateState(mCurrentStateIndex, + selectedCallIndex); + mCurrentStateIndex = selectedCallIndex; + } catch (Exception e) { + GlTracePlugin.getDefault().logMessage( + "Unexpected error while updating GL State."); + GlTracePlugin.getDefault().logMessage(e.getMessage()); + return new Status(Status.ERROR, + GlTracePlugin.PLUGIN_ID, + "Unexpected error while updating GL State.", + e); + } finally { + sGlStateLock.release(); + } + + mLabelProvider.setChangedProperties(changedProperties); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + if (!mTreeViewer.getTree().isDisposed()) { + mTreeViewer.refresh(); + } + } + }); + + return Status.OK_STATUS; + } + }; + job.setPriority(Job.SHORT); + job.schedule(); + } + + @Override + public Control getControl() { + if (mTreeViewer == null) { + return null; + } + + return mTreeViewer.getControl(); + } + + @Override + public void setFocus() { + } + + /** + * Update GL state from GL call at fromIndex to the call at toIndex. + * If fromIndex < toIndex, the GL state will be updated by applying all the transformations + * corresponding to calls from (fromIndex + 1) to toIndex (inclusive). + * If fromIndex > toIndex, the GL state will be updated by reverting all the calls from + * fromIndex (inclusive) to (toIndex + 1). + * @return GL state properties that changed as a result of this update. + */ + private Set<IGLProperty> updateState(int fromIndex, int toIndex) { + assert fromIndex >= -1 && fromIndex < mGLCalls.size(); + assert toIndex >= 0 && toIndex < mGLCalls.size(); + + if (fromIndex < toIndex) { + return applyTransformations(fromIndex, toIndex); + } else if (fromIndex > toIndex) { + return revertTransformations(fromIndex, toIndex); + } else { + return Collections.emptySet(); + } + } + + private Set<IGLProperty> applyTransformations(int fromIndex, int toIndex) { + int setSizeHint = 3 * (toIndex - fromIndex) + 10; + Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint); + + for (int i = fromIndex + 1; i <= toIndex; i++) { + GLCall call = mGLCalls.get(i); + for (IStateTransform f : call.getStateTransformations()) { + try { + f.apply(mState); + IGLProperty changedProperty = f.getChangedProperty(mState); + if (changedProperty != null) { + changedProperties.addAll(getHierarchy(changedProperty)); + } + } catch (Exception e) { + GlTracePlugin.getDefault().logMessage("Error applying transformations for " + + call); + GlTracePlugin.getDefault().logMessage(e.toString()); + } + } + } + + return changedProperties; + } + + private Set<IGLProperty> revertTransformations(int fromIndex, int toIndex) { + int setSizeHint = 3 * (fromIndex - toIndex) + 10; + Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint); + + for (int i = fromIndex; i > toIndex; i--) { + List<IStateTransform> transforms = mGLCalls.get(i).getStateTransformations(); + // When reverting transformations, iterate from the last to first so that the reversals + // are performed in the correct sequence. + for (int j = transforms.size() - 1; j >= 0; j--) { + IStateTransform f = transforms.get(j); + f.revert(mState); + + IGLProperty changedProperty = f.getChangedProperty(mState); + if (changedProperty != null) { + changedProperties.addAll(getHierarchy(changedProperty)); + } + } + } + + return changedProperties; + } + + /** + * Obtain the list of properties starting from the provided property up to + * the root of GL state. + */ + private List<IGLProperty> getHierarchy(IGLProperty changedProperty) { + List<IGLProperty> changedProperties = new ArrayList<IGLProperty>(5); + changedProperties.add(changedProperty); + + // add the entire parent chain until we reach the root + IGLProperty prop = changedProperty; + while ((prop = prop.getParent()) != null) { + changedProperties.add(prop); + } + + return changedProperties; + } + + @Override + public void addSelectionChangedListener(ISelectionChangedListener listener) { + mTreeViewer.addSelectionChangedListener(listener); + } + + @Override + public ISelection getSelection() { + return mTreeViewer.getSelection(); + } + + @Override + public void removeSelectionChangedListener(ISelectionChangedListener listener) { + mTreeViewer.removeSelectionChangedListener(listener); + } + + @Override + public void setSelection(ISelection selection) { + mTreeViewer.setSelection(selection); + } +} |