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 final List<GLCall> mCalls; 63 64 /** Number of GL contexts in the trace. */ 65 private final 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 mCalls = trace.getGLCalls(); 106 mContextCount = trace.getContexts().size(); 107 108 initializeColors(); 109 initializeFonts(); 110 111 mPositionHelper = new PositionHelper( 112 mFontHeight, 113 mContextCount, 114 mFontWidth * MAX_DURATION_LENGTH_SCALE, /* max display length for call. */ 115 CALL_DURATION_CLAMP /* max duration */); 116 117 addPaintListener(new PaintListener() { 118 @Override 119 public void paintControl(PaintEvent e) { 120 draw(e.display, e.gc); 121 } 122 }); 123 124 addListener(SWT.Resize, new Listener() { 125 @Override 126 public void handleEvent(Event event) { 127 controlResized(); 128 } 129 }); 130 131 addMouseMoveListener(new MouseMoveListener() { 132 @Override 133 public void mouseMove(MouseEvent e) { 134 mouseMoved(e); 135 } 136 }); 137 138 addMouseListener(new MouseAdapter() { 139 @Override 140 public void mouseUp(MouseEvent e) { 141 mouseClicked(e); 142 } 143 }); 144 145 addMouseTrackListener(new MouseTrackListener() { 146 @Override 147 public void mouseHover(MouseEvent e) { 148 } 149 150 @Override 151 public void mouseExit(MouseEvent e) { 152 mMouseInSelf = false; 153 redraw(); 154 } 155 156 @Override 157 public void mouseEnter(MouseEvent e) { 158 mMouseInSelf = true; 159 redraw(); 160 } 161 }); 162 } 163 164 @Override dispose()165 public void dispose() { 166 disposeColors(); 167 disposeBackBuffer(); 168 super.dispose(); 169 } 170 initializeColors()171 private void initializeColors() { 172 mBackgroundColor = new Color(getDisplay(), 0x33, 0x33, 0x33); 173 mDurationLineColor = new Color(getDisplay(), 0x08, 0x51, 0x9c); 174 mGlDrawColor = new Color(getDisplay(), 0x6b, 0xae, 0xd6); 175 mContextHeaderColor = new Color(getDisplay(), 0xd1, 0xe5, 0xf0); 176 mVisibleCallsHighlightColor = new Color(getDisplay(), 0xcc, 0xcc, 0xcc); 177 mMouseMarkerColor = new Color(getDisplay(), 0xaa, 0xaa, 0xaa); 178 179 mGlErrorColor = getDisplay().getSystemColor(SWT.COLOR_RED); 180 } 181 disposeColors()182 private void disposeColors() { 183 mBackgroundColor.dispose(); 184 mDurationLineColor.dispose(); 185 mGlDrawColor.dispose(); 186 mContextHeaderColor.dispose(); 187 mVisibleCallsHighlightColor.dispose(); 188 mMouseMarkerColor.dispose(); 189 } 190 initializeFonts()191 private void initializeFonts() { 192 mFontRegistry = new FontRegistry(getDisplay()); 193 mFontRegistry.put(FONT_KEY, 194 new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ 195 196 GC gc = new GC(getDisplay()); 197 gc.setFont(mFontRegistry.get(FONT_KEY)); 198 mFontWidth = gc.getFontMetrics().getAverageCharWidth(); 199 mFontHeight = gc.getFontMetrics().getHeight(); 200 gc.dispose(); 201 } 202 initializeBackBuffer()203 private void initializeBackBuffer() { 204 Rectangle clientArea = getClientArea(); 205 206 if (clientArea.width == 0 || clientArea.height == 0) { 207 mBackBufferImage = null; 208 mBackBufferGC = null; 209 return; 210 } 211 212 mBackBufferImage = new Image(getDisplay(), 213 clientArea.width, 214 clientArea.height); 215 mBackBufferGC = new GC(mBackBufferImage); 216 } 217 disposeBackBuffer()218 private void disposeBackBuffer() { 219 if (mBackBufferImage != null) { 220 mBackBufferImage.dispose(); 221 mBackBufferImage = null; 222 } 223 224 if (mBackBufferGC != null) { 225 mBackBufferGC.dispose(); 226 mBackBufferGC = null; 227 } 228 } 229 mouseMoved(MouseEvent e)230 private void mouseMoved(MouseEvent e) { 231 mMouseY = e.y; 232 redraw(); 233 } 234 mouseClicked(MouseEvent e)235 private void mouseClicked(MouseEvent e) { 236 if (mMouseInSelf) { 237 int selIndex = mPositionHelper.getCallAt(mMouseY); 238 sendCallSelectedEvent(selIndex); 239 redraw(); 240 } 241 } 242 draw(Display display, GC gc)243 private void draw(Display display, GC gc) { 244 if (mBackBufferImage == null) { 245 initializeBackBuffer(); 246 } 247 248 if (mBackBufferImage == null) { 249 return; 250 } 251 252 // draw contents onto the back buffer 253 drawBackground(mBackBufferGC, mBackBufferImage.getBounds()); 254 drawContextHeaders(mBackBufferGC); 255 drawCallDurations(mBackBufferGC); 256 drawVisibleCallHighlights(mBackBufferGC); 257 drawMouseMarkers(mBackBufferGC); 258 259 // finally copy over the rendered back buffer onto screen 260 int width = getClientArea().width; 261 int height = getClientArea().height; 262 gc.drawImage(mBackBufferImage, 263 0, 0, width, height, 264 0, 0, width, height); 265 } 266 drawBackground(GC gc, Rectangle bounds)267 private void drawBackground(GC gc, Rectangle bounds) { 268 gc.setBackground(mBackgroundColor); 269 gc.fillRectangle(bounds); 270 } 271 drawContextHeaders(GC gc)272 private void drawContextHeaders(GC gc) { 273 if (mContextCount <= 1) { 274 return; 275 } 276 277 gc.setForeground(mContextHeaderColor); 278 gc.setFont(mFontRegistry.get(FONT_KEY)); 279 for (int i = 0; i < mContextCount; i++) { 280 Point p = mPositionHelper.getHeaderLocation(i); 281 gc.drawText("CTX" + Integer.toString(i), p.x, p.y); 282 } 283 } 284 285 /** Draw the call durations as a sequence of lines. 286 * 287 * Calls are arranged on the y-axis based on the sequence in which they were originally 288 * called by the application. If the display height is lesser than the number of calls, then 289 * not every call is shown - the calls are underscanned based the height of the display. 290 * 291 * The x-axis shows two pieces of information: the duration of the call, and the context 292 * in which the call was made. The duration controls how long the displayed line is, and 293 * the context controls the starting offset of the line. 294 */ drawCallDurations(GC gc)295 private void drawCallDurations(GC gc) { 296 if (mCalls == null || mCalls.size() < mEndCallIndex) { 297 return; 298 } 299 300 gc.setBackground(mDurationLineColor); 301 302 int callUnderScan = mPositionHelper.getCallUnderScanValue(); 303 for (int i = mStartCallIndex; i < mEndCallIndex; i += callUnderScan) { 304 boolean resetColor = false; 305 GLCall c = mCalls.get(i); 306 307 long duration = c.getWallDuration(); 308 309 if (c.hasErrors()) { 310 gc.setBackground(mGlErrorColor); 311 resetColor = true; 312 313 // If the call has any errors, we want it to be visible in the minimap 314 // regardless of how long it took. 315 duration = mPositionHelper.getMaxDuration(); 316 } else if (c.getFunction() == Function.glDrawArrays 317 || c.getFunction() == Function.glDrawElements 318 || c.getFunction() == Function.eglSwapBuffers) { 319 gc.setBackground(mGlDrawColor); 320 resetColor = true; 321 322 // render all draw calls & swap buffer at max length 323 duration = mPositionHelper.getMaxDuration(); 324 } 325 326 Rectangle bounds = mPositionHelper.getDurationBounds( 327 i - mStartCallIndex, 328 c.getContextId(), 329 duration); 330 gc.fillRectangle(bounds); 331 332 if (resetColor) { 333 gc.setBackground(mDurationLineColor); 334 } 335 } 336 } 337 338 /** 339 * Draw a bounding box that highlights the currently visible range of calls in the 340 * {@link GLFunctionTraceViewer} table. 341 */ drawVisibleCallHighlights(GC gc)342 private void drawVisibleCallHighlights(GC gc) { 343 gc.setAlpha(VISIBLE_CALLS_HIGHLIGHT_ALPHA); 344 gc.setBackground(mVisibleCallsHighlightColor); 345 gc.fillRectangle(mPositionHelper.getBoundsFramingCalls( 346 mVisibleCallTopIndex - mStartCallIndex, 347 mVisibleCallBottomIndex - mStartCallIndex)); 348 gc.setAlpha(DEFAULT_ALPHA); 349 } 350 drawMouseMarkers(GC gc)351 private void drawMouseMarkers(GC gc) { 352 if (!mMouseInSelf) { 353 return; 354 } 355 356 if (mPositionHelper.getCallAt(mMouseY) < 0) { 357 return; 358 } 359 360 gc.setForeground(mMouseMarkerColor); 361 gc.drawLine(0, mMouseY, getClientArea().width, mMouseY); 362 } 363 controlResized()364 private void controlResized() { 365 // regenerate back buffer on size changes 366 disposeBackBuffer(); 367 initializeBackBuffer(); 368 369 redraw(); 370 } 371 getMinimumWidth()372 public int getMinimumWidth() { 373 return mPositionHelper.getMinimumWidth(); 374 } 375 376 /** Set the GL Call start and end indices for currently displayed frame. */ setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex)377 public void setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex) { 378 mStartCallIndex = startCallIndex; 379 mEndCallIndex = endCallIndex; 380 mPositionHelper.updateCallDensity(mEndCallIndex - mStartCallIndex, getClientArea().height); 381 redraw(); 382 } 383 384 /** 385 * Set the call range that is currently visible in the {@link GLFunctionTraceViewer} table. 386 * @param visibleTopIndex index of call currently visible at the top of the table. 387 * @param visibleBottomIndex index of call currently visible at the bottom of the table. 388 */ setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex)389 public void setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex) { 390 mVisibleCallTopIndex = visibleTopIndex; 391 mVisibleCallBottomIndex = visibleBottomIndex; 392 redraw(); 393 } 394 395 public interface ICallSelectionListener { callSelected(int selectedCallIndex)396 void callSelected(int selectedCallIndex); 397 } 398 399 private List<ICallSelectionListener> mListeners = new ArrayList<ICallSelectionListener>(); 400 addCallSelectionListener(ICallSelectionListener l)401 public void addCallSelectionListener(ICallSelectionListener l) { 402 mListeners.add(l); 403 } 404 sendCallSelectedEvent(int selectedCall)405 private void sendCallSelectedEvent(int selectedCall) { 406 for (ICallSelectionListener l : mListeners) { 407 l.callSelected(selectedCall); 408 } 409 } 410 411 /** Utility class to help with the positioning and sizes of elements in the canvas. */ 412 private static class PositionHelper { 413 /** Left Margin after which duration lines are drawn. */ 414 private static final int LEFT_MARGIN = 5; 415 416 /** Top margin after which header is drawn. */ 417 private static final int TOP_MARGIN = 5; 418 419 /** # of pixels of padding between duration markers for different contexts. */ 420 private static final int CONTEXT_PADDING = 10; 421 422 private final int mHeaderMargin; 423 private final int mContextCount; 424 private final int mMaxDurationLength; 425 private final long mMaxDuration; 426 private final double mScale; 427 428 private int mCallCount; 429 private int mNumCallsPerPixel = 1; 430 PositionHelper(int fontHeight, int contextCount, int maxDurationLength, long maxDuration)431 public PositionHelper(int fontHeight, int contextCount, 432 int maxDurationLength, long maxDuration) { 433 mContextCount = contextCount; 434 mMaxDurationLength = maxDurationLength; 435 mMaxDuration = maxDuration; 436 mScale = (double) maxDurationLength / maxDuration; 437 438 // header region is present only there are multiple contexts 439 if (mContextCount > 1) { 440 mHeaderMargin = fontHeight * 3; 441 } else { 442 mHeaderMargin = 0; 443 } 444 } 445 446 /** Get the minimum width of the canvas. */ getMinimumWidth()447 public int getMinimumWidth() { 448 return LEFT_MARGIN + (mMaxDurationLength + CONTEXT_PADDING) * mContextCount; 449 } 450 451 /** Get the bounds for a call duration line. */ getDurationBounds(int callIndex, int context, long duration)452 public Rectangle getDurationBounds(int callIndex, int context, long duration) { 453 if (duration <= 0) { 454 duration = 1; 455 } else if (duration > mMaxDuration) { 456 duration = mMaxDuration; 457 } 458 459 int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); 460 int y = (callIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; 461 int w = (int) (duration * mScale); 462 int h = 1; 463 464 return new Rectangle(x, y, w, h); 465 } 466 getMaxDuration()467 public long getMaxDuration() { 468 return mMaxDuration; 469 } 470 471 /** Get the bounds for calls spanning given range. */ getBoundsFramingCalls(int startCallIndex, int endCallIndex)472 public Rectangle getBoundsFramingCalls(int startCallIndex, int endCallIndex) { 473 if (startCallIndex >= 0 && endCallIndex >= startCallIndex 474 && endCallIndex <= mCallCount) { 475 int x = LEFT_MARGIN; 476 int y = (startCallIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin; 477 int w = ((mMaxDurationLength + CONTEXT_PADDING) * mContextCount); 478 int h = (endCallIndex - startCallIndex)/mNumCallsPerPixel; 479 return new Rectangle(x, y, w, h); 480 } else { 481 return new Rectangle(0, 0, 0, 0); 482 } 483 } 484 getHeaderLocation(int context)485 public Point getHeaderLocation(int context) { 486 int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context); 487 return new Point(x, TOP_MARGIN); 488 } 489 490 /** Update the call density based on the number of calls to be displayed and 491 * the available height to display them in. */ updateCallDensity(int callCount, int displayHeight)492 public void updateCallDensity(int callCount, int displayHeight) { 493 mCallCount = callCount; 494 495 if (displayHeight <= 0) { 496 displayHeight = callCount + 1; 497 } 498 499 mNumCallsPerPixel = (callCount / displayHeight) + 1; 500 } 501 502 /** Get the underscan value. In cases where there are more calls to be displayed 503 * than there are availble pixels, we only display 1 out of every underscan calls. */ getCallUnderScanValue()504 public int getCallUnderScanValue() { 505 return mNumCallsPerPixel; 506 } 507 508 /** Get the index of the call at given y offset. */ getCallAt(int y)509 public int getCallAt(int y) { 510 if (!isWithinBounds(y)) { 511 return -1; 512 } 513 514 Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); 515 return (y - displayBounds.y) * mNumCallsPerPixel; 516 } 517 518 /** Does the provided y offset map to a valid call? */ isWithinBounds(int y)519 private boolean isWithinBounds(int y) { 520 Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount); 521 if (y < displayBounds.y) { 522 return false; 523 } 524 525 if (y > (displayBounds.y + displayBounds.height)) { 526 return false; 527 } 528 529 return true; 530 } 531 } 532 } 533