1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.gltrace.editors; 18 19 import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function; 20 import com.android.ide.eclipse.gltrace.model.GLCall; 21 import com.android.ide.eclipse.gltrace.model.GLTrace; 22 23 import org.eclipse.jface.resource.FontRegistry; 24 import org.eclipse.swt.SWT; 25 import org.eclipse.swt.events.MouseAdapter; 26 import org.eclipse.swt.events.MouseEvent; 27 import org.eclipse.swt.events.MouseMoveListener; 28 import org.eclipse.swt.events.MouseTrackListener; 29 import org.eclipse.swt.events.PaintEvent; 30 import org.eclipse.swt.events.PaintListener; 31 import org.eclipse.swt.graphics.Color; 32 import org.eclipse.swt.graphics.FontData; 33 import org.eclipse.swt.graphics.GC; 34 import org.eclipse.swt.graphics.Image; 35 import org.eclipse.swt.graphics.Point; 36 import org.eclipse.swt.graphics.Rectangle; 37 import org.eclipse.swt.widgets.Canvas; 38 import org.eclipse.swt.widgets.Composite; 39 import org.eclipse.swt.widgets.Display; 40 import org.eclipse.swt.widgets.Event; 41 import org.eclipse.swt.widgets.Listener; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 46 public class DurationMinimap extends Canvas { 47 /** Default alpha value. */ 48 private static final int DEFAULT_ALPHA = 255; 49 50 /** Alpha value for highlighting visible calls. */ 51 private static final int VISIBLE_CALLS_HIGHLIGHT_ALPHA = 50; 52 53 /** Clamp call durations at this value. */ 54 private static final long CALL_DURATION_CLAMP = 20000; 55 56 private static final String FONT_KEY = "default.font"; //$NON-NLS-1$ 57 58 /** Scale font size by this amount to get the max display length of call duration. */ 59 private static final int MAX_DURATION_LENGTH_SCALE = 6; 60 61 /** List of GL Calls in the trace. */ 62 private List<GLCall> mCalls; 63 64 /** Number of GL contexts in the trace. */ 65 private int mContextCount; 66 67 /** Starting call index of currently displayed frame. */ 68 private int mStartCallIndex; 69 70 /** Ending call index of currently displayed frame. */ 71 private int mEndCallIndex; 72 73 /** The top index that is currently visible in the table. */ 74 private int mVisibleCallTopIndex; 75 76 /** The bottom index that is currently visible in the table. */ 77 private int mVisibleCallBottomIndex; 78 79 private Color mBackgroundColor; 80 private Color mDurationLineColor; 81 private Color mGlDrawColor; 82 private Color mGlErrorColor; 83 private Color mContextHeaderColor; 84 private Color mVisibleCallsHighlightColor; 85 private Color mMouseMarkerColor; 86 87 private FontRegistry mFontRegistry; 88 private int mFontWidth; 89 private int mFontHeight; 90 91 // back buffers used for double buffering 92 private Image mBackBufferImage; 93 private GC mBackBufferGC; 94 95 // mouse state 96 private boolean mMouseInSelf; 97 private int mMouseY; 98 99 // helper object used to position various items on screen 100 private final PositionHelper mPositionHelper; 101 DurationMinimap(Composite parent, GLTrace trace)102 public DurationMinimap(Composite parent, GLTrace trace) { 103 super(parent, SWT.NO_BACKGROUND); 104 105 setInput(trace); 106 107 initializeColors(); 108 initializeFonts(); 109 110 mPositionHelper = new PositionHelper( 111 mFontHeight, 112 mContextCount, 113 mFontWidth * MAX_DURATION_LENGTH_SCALE, /* max display length for call. */ 114 CALL_DURATION_CLAMP /* max duration */); 115 116 addPaintListener(new PaintListener() { 117 @Override 118 public void paintControl(PaintEvent e) { 119 draw(e.display, e.gc); 120 } 121 }); 122 123 addListener(SWT.Resize, new Listener() { 124 @Override 125 public void handleEvent(Event event) { 126 controlResized(); 127 } 128 }); 129 130 addMouseMoveListener(new MouseMoveListener() { 131 @Override 132 public void mouseMove(MouseEvent e) { 133 mouseMoved(e); 134 } 135 }); 136 137 addMouseListener(new MouseAdapter() { 138 @Override 139 public void mouseUp(MouseEvent e) { 140 mouseClicked(e); 141 } 142 }); 143 144 addMouseTrackListener(new MouseTrackListener() { 145 @Override 146 public void mouseHover(MouseEvent e) { 147 } 148 149 @Override 150 public void mouseExit(MouseEvent e) { 151 mMouseInSelf = false; 152 redraw(); 153 } 154 155 @Override 156 public void mouseEnter(MouseEvent e) { 157 mMouseInSelf = true; 158 redraw(); 159 } 160 }); 161 } 162 setInput(GLTrace trace)163 public void setInput(GLTrace trace) { 164 if (trace != null) { 165 mCalls = trace.getGLCalls(); 166 mContextCount = trace.getContexts().size(); 167 } else { 168 mCalls = null; 169 mContextCount = 1; 170 } 171 } 172 173 @Override dispose()174 public void dispose() { 175 disposeColors(); 176 disposeBackBuffer(); 177 super.dispose(); 178 } 179 initializeColors()180 private void initializeColors() { 181 mBackgroundColor = new Color(getDisplay(), 0x33, 0x33, 0x33); 182 mDurationLineColor = new Color(getDisplay(), 0x08, 0x51, 0x9c); 183 mGlDrawColor = new Color(getDisplay(), 0x6b, 0xae, 0xd6); 184 mContextHeaderColor = new Color(getDisplay(), 0xd1, 0xe5, 0xf0); 185 mVisibleCallsHighlightColor = new Color(getDisplay(), 0xcc, 0xcc, 0xcc); 186 mMouseMarkerColor = new Color(getDisplay(), 0xaa, 0xaa, 0xaa); 187 188 mGlErrorColor = getDisplay().getSystemColor(SWT.COLOR_RED); 189 } 190 disposeColors()191 private void disposeColors() { 192 mBackgroundColor.dispose(); 193 mDurationLineColor.dispose(); 194 mGlDrawColor.dispose(); 195 mContextHeaderColor.dispose(); 196 mVisibleCallsHighlightColor.dispose(); 197 mMouseMarkerColor.dispose(); 198 } 199 initializeFonts()200 private void initializeFonts() { 201 mFontRegistry = new FontRegistry(getDisplay()); 202 mFontRegistry.put(FONT_KEY, 203 new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ 204 205 GC gc = new GC(getDisplay()); 206 gc.setFont(mFontRegistry.get(FONT_KEY)); 207 mFontWidth = gc.getFontMetrics().getAverageCharWidth(); 208 mFontHeight = gc.getFontMetrics().getHeight(); 209 gc.dispose(); 210 } 211 initializeBackBuffer()212 private void initializeBackBuffer() { 213 Rectangle clientArea = getClientArea(); 214 215 if (clientArea.width == 0 || clientArea.height == 0) { 216 mBackBufferImage = null; 217 mBackBufferGC = null; 218 return; 219 } 220 221 mBackBufferImage = new Image(getDisplay(), 222 clientArea.width, 223 clientArea.height); 224 mBackBufferGC = new GC(mBackBufferImage); 225 } 226 disposeBackBuffer()227 private void disposeBackBuffer() { 228 if (mBackBufferImage != null) { 229 mBackBufferImage.dispose(); 230 mBackBufferImage = null; 231 } 232 233 if (mBackBufferGC != null) { 234 mBackBufferGC.dispose(); 235 mBackBufferGC = null; 236 } 237 } 238 mouseMoved(MouseEvent e)239 private void mouseMoved(MouseEvent e) { 240 mMouseY = e.y; 241 redraw(); 242 } 243 mouseClicked(MouseEvent e)244 private void mouseClicked(MouseEvent e) { 245 if (mMouseInSelf) { 246 int selIndex = mPositionHelper.getCallAt(mMouseY); 247 sendCallSelectedEvent(selIndex); 248 redraw(); 249 } 250 } 251 draw(Display display, GC gc)252 private void draw(Display display, GC gc) { 253 if (mBackBufferImage == null) { 254 initializeBackBuffer(); 255 } 256 257 if (mBackBufferImage == null) { 258 return; 259 } 260 261 // draw contents onto the back buffer 262 drawBackground(mBackBufferGC, mBackBufferImage.getBounds()); 263 drawContextHeaders(mBackBufferGC); 264 drawCallDurations(mBackBufferGC); 265 drawVisibleCallHighlights(mBackBufferGC); 266 drawMouseMarkers(mBackBufferGC); 267 268 // finally copy over the rendered back buffer onto screen 269 int width = getClientArea().width; 270 int height = getClientArea().height; 271 gc.drawImage(mBackBufferImage, 272 0, 0, width, height, 273 0, 0, width, height); 274 } 275 drawBackground(GC gc, Rectangle bounds)276 private void drawBackground(GC gc, Rectangle bounds) { 277 gc.setBackground(mBackgroundColor); 278 gc.fillRectangle(bounds); 279 } 280 drawContextHeaders(GC gc)281 private void drawContextHeaders(GC gc) { 282 if (mContextCount <= 1) { 283 return; 284 } 285 286 gc.setForeground(mContextHeaderColor); 287 gc.setFont(mFontRegistry.get(FONT_KEY)); 288 for (int i = 0; i < mContextCount; i++) { 289 Point p = mPositionHelper.getHeaderLocation(i); 290 gc.drawText("CTX" + Integer.toString(i), p.x, p.y); 291 } 292 } 293 294 /** Draw the call durations as a sequence of lines. 295 * 296 * Calls are arranged on the y-axis based on the sequence in which they were originally 297 * called by the application. If the display height is lesser than the number of calls, then 298 * not every call is shown - the calls are underscanned based the height of the display. 299 * 300 * The x-axis shows two pieces of information: the duration of the call, and the context 301 * in which the call was made. The duration controls how long the displayed line is, and 302 * the context controls the starting offset of the line. 303 */ drawCallDurations(GC gc)304 private void drawCallDurations(GC gc) { 305 if (mCalls == null || mCalls.size() < mEndCallIndex) { 306 return; 307 } 308 309 gc.setBackground(mDurationLineColor); 310 311 int callUnderScan = mPositionHelper.getCallUnderScanValue(); 312 for (int i = mStartCallIndex; i < mEndCallIndex; i += callUnderScan) { 313 boolean resetColor = false; 314 GLCall c = mCalls.get(i); 315 316 long duration = c.getWallDuration(); 317 318 if (c.hasErrors()) { 319 gc.setBackground(mGlErrorColor); 320 resetColor = true; 321 322 // If the call has any errors, we want it to be visible in the minimap 323 // regardless of how long it took. 324 duration = mPositionHelper.getMaxDuration(); 325 } else if (c.getFunction() == Function.glDrawArrays 326 || c.getFunction() == Function.glDrawElements 327 || c.getFunction() == Function.eglSwapBuffers) { 328 gc.setBackground(mGlDrawColor); 329 resetColor = true; 330 331 // render all draw calls & swap buffer at max length 332 duration = mPositionHelper.getMaxDuration(); 333 } 334 335 Rectangle bounds = mPositionHelper.getDurationBounds( 336 i - mStartCallIndex, 337 c.getContextId(), 338 duration); 339 gc.fillRectangle(bounds); 340 341 if (resetColor) { 342 gc.setBackground(mDurationLineColor); 343 } 344 } 345 } 346 347 /** 348 * Draw a bounding box that highlights the currently visible range of calls in the 349 * {@link GLFunctionTraceViewer} table. 350 */ drawVisibleCallHighlights(GC gc)351 private void drawVisibleCallHighlights(GC gc) { 352 gc.setAlpha(VISIBLE_CALLS_HIGHLIGHT_ALPHA); 353 gc.setBackground(mVisibleCallsHighlightColor); 354 gc.fillRectangle(mPositionHelper.getBoundsFramingCalls( 355 mVisibleCallTopIndex - mStartCallIndex, 356 mVisibleCallBottomIndex - mStartCallIndex)); 357 gc.setAlpha(DEFAULT_ALPHA); 358 } 359 drawMouseMarkers(GC gc)360 private void drawMouseMarkers(GC gc) { 361 if (!mMouseInSelf) { 362 return; 363 } 364 365 if (mPositionHelper.getCallAt(mMouseY) < 0) { 366 return; 367 } 368 369 gc.setForeground(mMouseMarkerColor); 370 gc.drawLine(0, mMouseY, getClientArea().width, mMouseY); 371 } 372 controlResized()373 private void controlResized() { 374 // regenerate back buffer on size changes 375 disposeBackBuffer(); 376 initializeBackBuffer(); 377 378 redraw(); 379 } 380 getMinimumWidth()381 public int getMinimumWidth() { 382 return mPositionHelper.getMinimumWidth(); 383 } 384 385 /** Set the GL Call start and end indices for currently displayed frame. */ setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex)386 public void setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex) { 387 mStartCallIndex = startCallIndex; 388 mEndCallIndex = endCallIndex; 389 mPositionHelper.updateCallDensity(mEndCallIndex - mStartCallIndex, getClientArea().height); 390 redraw(); 391 } 392 393 /** 394 * Set the call range that is currently visible in the {@link GLFunctionTraceViewer} table. 395 * @param visibleTopIndex index of call currently visible at the top of the table. 396 * @param visibleBottomIndex index of call currently visible at the bottom of the table. 397 */ setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex)398 public void setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex) { 399 mVisibleCallTopIndex = visibleTopIndex; 400 mVisibleCallBottomIndex = visibleBottomIndex; 401 redraw(); 402 } 403 404 public interface ICallSelectionListener { callSelected(int selectedCallIndex)405 void callSelected(int selectedCallIndex); 406 } 407 408 private List<ICallSelectionListener> mListeners = new ArrayList<ICallSelectionListener>(); 409 addCallSelectionListener(ICallSelectionListener l)410 public void addCallSelectionListener(ICallSelectionListener l) { 411 mListeners.add(l); 412 } 413 sendCallSelectedEvent(int selectedCall)414 private void sendCallSelectedEvent(int selectedCall) { 415 for (ICallSelectionListener l : mListeners) { 416 l.callSelected(selectedCall); 417 } 418 } 419 420 /** Utility class to help with the positioning and sizes of elements in the canvas. */ 421 private static class PositionHelper { 422 /** Left Margin after which duration lines are drawn. */ 423 private static final int LEFT_MARGIN = 5; 424 425 /** Top margin after which header is drawn. */ 426 private static final int TOP_MARGIN = 5; 427 428 /** # of pixels of padding between duration markers for different contexts. */ 429 private static final int CONTEXT_PADDING = 10; 430 431 private final int mHeaderMargin; 432 private final int mContextCount; 433 private final int mMaxDurationLength; 434 private final long mMaxDuration; 435 private final double mScale; 436 437 private int mCallCount; 438 private int mNumCallsPerPixel = 1; 439 PositionHelper(int fontHeight, int contextCount, int maxDurationLength, long maxDuration)440 public PositionHelper(int fontHeight, int contextCount, 441 int maxDurationLength, long maxDuration) { 442 mContextCount = contextCount; 443 mMaxDurationLength = maxDurationLength; 444 mMaxDuration = maxDuration; 445 mScale = (double) maxDurationLength / maxDuration; 446 447 // header region is present only there are multiple contexts 448 if (mContextCount > 1) { 449 mHeaderMargin = fontHeight * 3; 450 } else { 451 mHeaderMargin = 0; 452 } 453 } 454 455 /** Get the minimum width of the canvas. */ getMinimumWidth()456 public int getMinimumWidth() { 457 return LEFT_MARGIN + (mMaxDurationLength + CONTEXT_PADDING) * mContextCount; 458 } 459 460 /** Get the bounds for a call duration line. */ getDurationBounds(int callIndex, int context, long duration)461 public Rectangle getDurationBounds(int callIndex, int context, long duration) { 462 if (duration <= 0) { 463 duration = 1; 464 } else if (duration > mMaxDuration) { 465 duration = mMaxDuration; 466 } 467 468 int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); 469 int y = (callIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; 470 int w = (int) (duration * mScale); 471 int h = 1; 472 473 return new Rectangle(x, y, w, h); 474 } 475 getMaxDuration()476 public long getMaxDuration() { 477 return mMaxDuration; 478 } 479 480 /** Get the bounds for calls spanning given range. */ getBoundsFramingCalls(int startCallIndex, int endCallIndex)481 public Rectangle getBoundsFramingCalls(int startCallIndex, int endCallIndex) { 482 if (startCallIndex >= 0 && endCallIndex >= startCallIndex 483 && endCallIndex <= mCallCount) { 484 int x = LEFT_MARGIN; 485 int y = (startCallIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; 486 int w = ((mMaxDurationLength + CONTEXT_PADDING) * mContextCount); 487 int h = (endCallIndex - startCallIndex)/mNumCallsPerPixel; 488 return new Rectangle(x, y, w, h); 489 } else { 490 return new Rectangle(0, 0, 0, 0); 491 } 492 } 493 getHeaderLocation(int context)494 public Point getHeaderLocation(int context) { 495 int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); 496 return new Point(x, TOP_MARGIN); 497 } 498 499 /** Update the call density based on the number of calls to be displayed and 500 * the available height to display them in. */ updateCallDensity(int callCount, int displayHeight)501 public void updateCallDensity(int callCount, int displayHeight) { 502 mCallCount = callCount; 503 504 if (displayHeight <= 0) { 505 displayHeight = callCount + 1; 506 } 507 508 mNumCallsPerPixel = (callCount / displayHeight) + 1; 509 } 510 511 /** Get the underscan value. In cases where there are more calls to be displayed 512 * than there are availble pixels, we only display 1 out of every underscan calls. */ getCallUnderScanValue()513 public int getCallUnderScanValue() { 514 return mNumCallsPerPixel; 515 } 516 517 /** Get the index of the call at given y offset. */ getCallAt(int y)518 public int getCallAt(int y) { 519 if (!isWithinBounds(y)) { 520 return -1; 521 } 522 523 Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); 524 return (y - displayBounds.y) * mNumCallsPerPixel; 525 } 526 527 /** Does the provided y offset map to a valid call? */ isWithinBounds(int y)528 private boolean isWithinBounds(int y) { 529 Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); 530 if (y < displayBounds.y) { 531 return false; 532 } 533 534 if (y > (displayBounds.y + displayBounds.height)) { 535 return false; 536 } 537 538 return true; 539 } 540 } 541 } 542