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.views; 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 import com.android.ide.eclipse.gltrace.widgets.ImageCanvas; 23 24 import org.eclipse.core.runtime.IProgressMonitor; 25 import org.eclipse.core.runtime.IStatus; 26 import org.eclipse.core.runtime.Status; 27 import org.eclipse.core.runtime.jobs.Job; 28 import org.eclipse.jface.action.IToolBarManager; 29 import org.eclipse.jface.layout.GridDataFactory; 30 import org.eclipse.jface.viewers.ColumnLabelProvider; 31 import org.eclipse.jface.viewers.IStructuredContentProvider; 32 import org.eclipse.jface.viewers.TableViewer; 33 import org.eclipse.jface.viewers.TableViewerColumn; 34 import org.eclipse.jface.viewers.Viewer; 35 import org.eclipse.jface.viewers.ViewerCell; 36 import org.eclipse.jface.viewers.ViewerComparator; 37 import org.eclipse.swt.SWT; 38 import org.eclipse.swt.custom.SashForm; 39 import org.eclipse.swt.events.ControlAdapter; 40 import org.eclipse.swt.events.ControlEvent; 41 import org.eclipse.swt.events.SelectionAdapter; 42 import org.eclipse.swt.events.SelectionEvent; 43 import org.eclipse.swt.events.SelectionListener; 44 import org.eclipse.swt.graphics.Image; 45 import org.eclipse.swt.layout.GridLayout; 46 import org.eclipse.swt.widgets.Composite; 47 import org.eclipse.swt.widgets.Control; 48 import org.eclipse.swt.widgets.Display; 49 import org.eclipse.swt.widgets.Label; 50 import org.eclipse.swt.widgets.Table; 51 import org.eclipse.swt.widgets.TableColumn; 52 import org.eclipse.ui.part.Page; 53 54 import java.util.EnumMap; 55 import java.util.List; 56 import java.util.Map; 57 58 /** 59 * A {@link FrameSummaryViewPage} displays summary information regarding a frame. This includes 60 * the contents of the frame buffer at the end of the frame, and statistics regarding the 61 * OpenGL Calls present in the frame. 62 */ 63 public class FrameSummaryViewPage extends Page { 64 private GLTrace mTrace; 65 66 private final Object mLock = new Object(); 67 private Job mRefresherJob; 68 private int mCurrentFrame; 69 70 private SashForm mSash; 71 private ImageCanvas mImageCanvas; 72 73 private Label mWallClockTimeLabel; 74 private Label mThreadTimeLabel; 75 76 private TableViewer mStatsTableViewer; 77 private StatsLabelProvider mStatsLabelProvider; 78 private StatsTableComparator mStatsTableComparator; 79 80 private FitToCanvasAction mFitToCanvasAction; 81 private SaveImageAction mSaveImageAction; 82 83 private static final String[] STATS_TABLE_PROPERTIES = { 84 "Function", 85 "Count", 86 "Wall Time (ns)", 87 "Thread Time (ns)", 88 }; 89 private static final float[] STATS_TABLE_COLWIDTH_RATIOS = { 90 0.4f, 0.1f, 0.25f, 0.25f, 91 }; 92 private static final int[] STATS_TABLE_COL_ALIGNMENT = { 93 SWT.LEFT, SWT.LEFT, SWT.RIGHT, SWT.RIGHT, 94 }; 95 96 public FrameSummaryViewPage(GLTrace trace) { 97 mTrace = trace; 98 } 99 100 public void setInput(GLTrace trace) { 101 mTrace = trace; 102 } 103 104 @Override 105 public void createControl(Composite parent) { 106 mSash = new SashForm(parent, SWT.VERTICAL); 107 108 // create image canvas where the framebuffer is displayed 109 mImageCanvas = new ImageCanvas(mSash); 110 111 // create a composite where the frame statistics are displayed 112 createFrameStatisticsPart(mSash); 113 114 mSash.setWeights(new int[] {70, 30}); 115 116 mFitToCanvasAction = new FitToCanvasAction(true, mImageCanvas); 117 mSaveImageAction = new SaveImageAction(mImageCanvas); 118 119 IToolBarManager toolbarManager = getSite().getActionBars().getToolBarManager(); 120 toolbarManager.add(mFitToCanvasAction); 121 toolbarManager.add(mSaveImageAction); 122 } 123 124 private void createFrameStatisticsPart(Composite parent) { 125 Composite c = new Composite(parent, SWT.NONE); 126 c.setLayout(new GridLayout(2, false)); 127 GridDataFactory.fillDefaults().grab(true, true).applyTo(c); 128 129 Label l = new Label(c, SWT.NONE); 130 l.setText("Cumulative call duration of all OpenGL Calls in this frame:"); 131 l.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY)); 132 GridDataFactory.fillDefaults().span(2, 1).applyTo(l); 133 134 l = new Label(c, SWT.NONE); 135 l.setText("Wall Clock Time: "); 136 GridDataFactory.fillDefaults().align(SWT.RIGHT, SWT.CENTER).applyTo(l); 137 138 mWallClockTimeLabel = new Label(c, SWT.NONE); 139 GridDataFactory.defaultsFor(mWallClockTimeLabel) 140 .grab(true, false) 141 .applyTo(mWallClockTimeLabel); 142 143 l = new Label(c, SWT.NONE); 144 l.setText("Thread Time: "); 145 GridDataFactory.fillDefaults().align(SWT.RIGHT, SWT.CENTER).applyTo(l); 146 147 mThreadTimeLabel = new Label(c, SWT.NONE); 148 GridDataFactory.defaultsFor(mThreadTimeLabel) 149 .grab(true, false) 150 .applyTo(mThreadTimeLabel); 151 152 l = new Label(c, SWT.HORIZONTAL | SWT.SEPARATOR); 153 GridDataFactory.fillDefaults().span(2, 1).applyTo(l); 154 155 l = new Label(c, SWT.NONE); 156 l.setText("Per OpenGL Function Statistics:"); 157 l.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY)); 158 GridDataFactory.fillDefaults().span(2, 1).applyTo(l); 159 160 final Table table = new Table(c, SWT.BORDER | SWT.FULL_SELECTION); 161 GridDataFactory.fillDefaults().grab(true, true).span(2, 1).applyTo(table); 162 163 table.setLinesVisible(true); 164 table.setHeaderVisible(true); 165 166 mStatsTableViewer = new TableViewer(table); 167 mStatsLabelProvider = new StatsLabelProvider(); 168 mStatsTableComparator = new StatsTableComparator(1); 169 170 // when a column is selected, sort the table based on that column 171 SelectionListener columnSelectionListener = new SelectionAdapter() { 172 @Override 173 public void widgetSelected(SelectionEvent e) { 174 TableColumn tc = (TableColumn) e.widget; 175 String colText = tc.getText(); 176 for (int i = 0; i < STATS_TABLE_PROPERTIES.length; i++) { 177 if (STATS_TABLE_PROPERTIES[i].equals(colText)) { 178 mStatsTableComparator.setSortColumn(i); 179 table.setSortColumn(tc); 180 table.setSortDirection(mStatsTableComparator.getDirection()); 181 mStatsTableViewer.refresh(); 182 break; 183 } 184 } 185 } 186 }; 187 188 for (int i = 0; i < STATS_TABLE_PROPERTIES.length; i++) { 189 TableViewerColumn tvc = new TableViewerColumn(mStatsTableViewer, SWT.NONE); 190 tvc.getColumn().setText(STATS_TABLE_PROPERTIES[i]); 191 tvc.setLabelProvider(mStatsLabelProvider); 192 tvc.getColumn().setAlignment(STATS_TABLE_COL_ALIGNMENT[i]); 193 tvc.getColumn().addSelectionListener(columnSelectionListener); 194 } 195 mStatsTableViewer.setContentProvider(new StatsContentProvider()); 196 mStatsTableViewer.setInput(null); 197 mStatsTableViewer.setComparator(mStatsTableComparator); 198 199 // resize columns appropriately when the size of the widget changes 200 table.addControlListener(new ControlAdapter() { 201 @Override 202 public void controlResized(ControlEvent e) { 203 int w = table.getClientArea().width; 204 205 for (int i = 0; i < STATS_TABLE_COLWIDTH_RATIOS.length; i++) { 206 table.getColumn(i).setWidth((int) (w * STATS_TABLE_COLWIDTH_RATIOS[i])); 207 } 208 } 209 }); 210 } 211 212 @Override 213 public Control getControl() { 214 return mSash; 215 } 216 217 @Override 218 public void setFocus() { 219 } 220 221 public void setSelectedFrame(int frame) { 222 if (mTrace == null) { 223 return; 224 } 225 226 synchronized (mLock) { 227 mCurrentFrame = frame; 228 229 if (mRefresherJob != null) { 230 return; 231 } 232 233 mRefresherJob = new Job("Update Frame Summary Task") { 234 @Override 235 protected IStatus run(IProgressMonitor monitor) { 236 final int currentFrame; 237 synchronized (mLock) { 238 currentFrame = mCurrentFrame; 239 mRefresherJob = null; 240 }; 241 242 updateImageCanvas(currentFrame); 243 updateFrameStats(currentFrame); 244 245 return Status.OK_STATUS; 246 } 247 }; 248 mRefresherJob.setPriority(Job.SHORT); 249 mRefresherJob.schedule(500); 250 }; 251 } 252 253 private void updateFrameStats(int frame) { 254 final List<GLCall> calls = mTrace.getGLCallsForFrame(frame); 255 256 Job job = new Job("Update Frame Statistics") { 257 @Override 258 protected IStatus run(IProgressMonitor monitor) { 259 long wallClockDuration = 0; 260 long threadDuration = 0; 261 262 final Map<Function, PerCallStats> cumulativeStats = 263 new EnumMap<Function, PerCallStats>(Function.class); 264 265 for (GLCall c: calls) { 266 wallClockDuration += c.getWallDuration(); 267 threadDuration += c.getThreadDuration(); 268 269 PerCallStats stats = cumulativeStats.get(c.getFunction()); 270 if (stats == null) { 271 stats = new PerCallStats(); 272 } 273 274 stats.count++; 275 stats.threadDuration += c.getThreadDuration(); 276 stats.wallDuration += c.getWallDuration(); 277 278 cumulativeStats.put(c.getFunction(), stats); 279 } 280 281 final String wallTime = formatMilliSeconds(wallClockDuration); 282 final String threadTime = formatMilliSeconds(threadDuration); 283 284 Display.getDefault().syncExec(new Runnable() { 285 @Override 286 public void run() { 287 mWallClockTimeLabel.setText(wallTime); 288 mThreadTimeLabel.setText(threadTime); 289 mStatsTableViewer.setInput(cumulativeStats); 290 } 291 }); 292 293 return Status.OK_STATUS; 294 } 295 }; 296 job.setUser(true); 297 job.schedule(); 298 } 299 300 private String formatMilliSeconds(long nanoSeconds) { 301 double milliSeconds = (double) nanoSeconds / 1000000; 302 return String.format("%.2f ms", milliSeconds); //$NON-NLS-1$ 303 } 304 305 private void updateImageCanvas(int frame) { 306 int lastCallIndex = mTrace.getFrame(frame).getEndIndex() - 1; 307 if (lastCallIndex >= 0 && lastCallIndex < mTrace.getGLCalls().size()) { 308 GLCall call = mTrace.getGLCalls().get(lastCallIndex); 309 final Image image = mTrace.getImage(call); 310 Display.getDefault().asyncExec(new Runnable() { 311 @Override 312 public void run() { 313 mImageCanvas.setImage(image); 314 315 mFitToCanvasAction.setEnabled(image != null); 316 mSaveImageAction.setEnabled(image != null); 317 } 318 }); 319 } 320 } 321 322 /** Cumulative stats maintained for each type of OpenGL Function in a particular frame. */ 323 private static class PerCallStats { 324 public int count; 325 public long wallDuration; 326 public long threadDuration; 327 } 328 329 private static class StatsContentProvider implements IStructuredContentProvider { 330 @Override 331 public void dispose() { 332 } 333 334 @Override 335 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 336 } 337 338 @Override 339 public Object[] getElements(Object inputElement) { 340 if (inputElement instanceof Map<?, ?>) { 341 return ((Map<?, ?>) inputElement).entrySet().toArray(); 342 } 343 344 return null; 345 } 346 } 347 348 private static class StatsLabelProvider extends ColumnLabelProvider { 349 @Override 350 public void update(ViewerCell cell) { 351 Object element = cell.getElement(); 352 if (!(element instanceof Map.Entry<?, ?>)) { 353 return; 354 } 355 356 Function f = (Function) ((Map.Entry<?, ?>) element).getKey(); 357 PerCallStats stats = (PerCallStats) ((Map.Entry<?, ?>) element).getValue(); 358 359 switch (cell.getColumnIndex()) { 360 case 0: 361 cell.setText(f.toString()); 362 break; 363 case 1: 364 cell.setText(Integer.toString(stats.count)); 365 break; 366 case 2: 367 cell.setText(formatDuration(stats.wallDuration)); 368 break; 369 case 3: 370 cell.setText(formatDuration(stats.threadDuration)); 371 break; 372 default: 373 // should not happen 374 cell.setText("??"); //$NON-NLS-1$ 375 break; 376 } 377 } 378 379 private String formatDuration(long time) { 380 // Max duration is in the 10s of milliseconds = xx,xxx,xxx ns 381 // So we require a format specifier that is 10 characters wide 382 return String.format("%,10d", time); //$NON-NLS-1$ 383 } 384 } 385 386 private static class StatsTableComparator extends ViewerComparator { 387 private int mSortColumn; 388 private boolean mDescending = true; 389 390 private StatsTableComparator(int defaultSortColIndex) { 391 mSortColumn = defaultSortColIndex; 392 } 393 394 public void setSortColumn(int index) { 395 if (index == mSortColumn) { 396 // if same column as what we are currently sorting on, 397 // then toggle the direction 398 mDescending = !mDescending; 399 } else { 400 mSortColumn = index; 401 mDescending = true; 402 } 403 } 404 405 public int getDirection() { 406 return mDescending ? SWT.UP : SWT.DOWN; 407 } 408 409 @Override 410 public int compare(Viewer viewer, Object e1, Object e2) { 411 Map.Entry<?, ?> entry1; 412 Map.Entry<?, ?> entry2; 413 414 if (mDescending) { 415 entry1 = (Map.Entry<?, ?>) e1; 416 entry2 = (Map.Entry<?, ?>) e2; 417 } else { 418 entry1 = (Map.Entry<?, ?>) e2; 419 entry2 = (Map.Entry<?, ?>) e1; 420 } 421 422 String k1 = entry1.getKey().toString(); 423 String k2 = entry2.getKey().toString(); 424 425 PerCallStats stats1 = (PerCallStats) entry1.getValue(); 426 PerCallStats stats2 = (PerCallStats) entry2.getValue(); 427 428 switch (mSortColumn) { 429 case 0: // function name 430 return String.CASE_INSENSITIVE_ORDER.compare(k1, k2); 431 case 1: 432 return stats1.count - stats2.count; 433 case 2: 434 return (int) (stats1.wallDuration - stats2.wallDuration); 435 case 3: 436 return (int) (stats1.threadDuration - stats2.threadDuration); 437 default: 438 return super.compare(viewer, e1, e2); 439 } 440 } 441 } 442 } 443