| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library heap_profile_element; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:html'; | |
| 9 import 'class_ref_wrapper.dart'; | |
| 10 import 'observatory_element.dart'; | |
| 11 import 'package:charted/charted.dart'; | |
| 12 import 'package:observatory/app.dart'; | |
| 13 import 'package:observatory/service.dart'; | |
| 14 import 'package:observatory/elements.dart'; | |
| 15 import 'package:polymer/polymer.dart'; | |
| 16 | |
| 17 class ClassSortedTable extends SortedTable { | |
| 18 | |
| 19 ClassSortedTable(columns) : super(columns); | |
| 20 | |
| 21 @override | |
| 22 dynamic getSortKeyFor(int row, int col) { | |
| 23 if (col == 0) { | |
| 24 // Use class name as sort key. | |
| 25 return rows[row].values[col].name; | |
| 26 } | |
| 27 return super.getSortKeyFor(row, col); | |
| 28 } | |
| 29 } | |
| 30 | |
| 31 @CustomTag('heap-profile') | |
| 32 class HeapProfileElement extends ObservatoryElement { | |
| 33 @observable String lastServiceGC = '---'; | |
| 34 @observable String lastAccumulatorReset = '---'; | |
| 35 | |
| 36 // Pie chart of new space usage. | |
| 37 var _newPieChart; | |
| 38 final _newPieChartRows = []; | |
| 39 // Pie chart of old space usage. | |
| 40 var _oldPieChart; | |
| 41 final _oldPieChartRows = []; | |
| 42 | |
| 43 @observable ClassSortedTable classTable; | |
| 44 var _classTableBody; | |
| 45 | |
| 46 @published bool autoRefresh = false; | |
| 47 var _subscriptionFuture; | |
| 48 | |
| 49 @published Isolate isolate; | |
| 50 @observable ServiceMap profile; | |
| 51 | |
| 52 final _pieChartColumns = [ | |
| 53 new ChartColumnSpec(label: 'Type', type: ChartColumnSpec.TYPE_STRING), | |
| 54 new ChartColumnSpec(label: 'Size', formatter: (v) => v.toString()) | |
| 55 ]; | |
| 56 | |
| 57 HeapProfileElement.created() : super.created() { | |
| 58 _initPieChartData(_newPieChartRows); | |
| 59 _initPieChartData(_oldPieChartRows); | |
| 60 | |
| 61 // Create class table model. | |
| 62 var columns = [ | |
| 63 new SortedTableColumn('Class'), | |
| 64 new SortedTableColumn(''), // Spacer column. | |
| 65 new SortedTableColumn.withFormatter('Accumulated Size (New)', | |
| 66 Utils.formatSize), | |
| 67 new SortedTableColumn.withFormatter('Accumulated Instances', | |
| 68 Utils.formatCommaSeparated), | |
| 69 new SortedTableColumn.withFormatter('Current Size', | |
| 70 Utils.formatSize), | |
| 71 new SortedTableColumn.withFormatter('Current Instances', | |
| 72 Utils.formatCommaSeparated), | |
| 73 new SortedTableColumn(''), // Spacer column. | |
| 74 new SortedTableColumn.withFormatter('Accumulator Size (Old)', | |
| 75 Utils.formatSize), | |
| 76 new SortedTableColumn.withFormatter('Accumulator Instances', | |
| 77 Utils.formatCommaSeparated), | |
| 78 new SortedTableColumn.withFormatter('Current Size', | |
| 79 Utils.formatSize), | |
| 80 new SortedTableColumn.withFormatter('Current Instances', | |
| 81 Utils.formatCommaSeparated) | |
| 82 ]; | |
| 83 classTable = new ClassSortedTable(columns); | |
| 84 // By default, start with accumulated new space bytes. | |
| 85 classTable.sortColumnIndex = 2; | |
| 86 } | |
| 87 | |
| 88 LayoutArea _makePieChart(String id, List rows) { | |
| 89 var wrapper = shadowRoot.querySelector(id); | |
| 90 var areaHost = wrapper.querySelector('.chart-host'); | |
| 91 assert(areaHost != null); | |
| 92 var legendHost = wrapper.querySelector('.chart-legend-host'); | |
| 93 assert(legendHost != null); | |
| 94 var series = new ChartSeries(id, [1], new PieChartRenderer( | |
| 95 sortDataByValue: false | |
| 96 )); | |
| 97 var config = new ChartConfig([series], [0]); | |
| 98 config.minimumSize = new Rect(300, 300); | |
| 99 config.legend = new ChartLegend(legendHost, showValues: true); | |
| 100 var data = new ChartData(_pieChartColumns, rows); | |
| 101 var area = new LayoutArea(areaHost, | |
| 102 data, | |
| 103 config, | |
| 104 state: new ChartState(), | |
| 105 autoUpdate: false); | |
| 106 area.addChartBehavior(new Hovercard()); | |
| 107 area.addChartBehavior(new AxisLabelTooltip()); | |
| 108 return area; | |
| 109 } | |
| 110 | |
| 111 @override | |
| 112 void attached() { | |
| 113 super.attached(); | |
| 114 _newPieChart = _makePieChart('#new-pie-chart', _newPieChartRows); | |
| 115 _oldPieChart = _makePieChart('#old-pie-chart', _oldPieChartRows); | |
| 116 _classTableBody = shadowRoot.querySelector('#classTableBody'); | |
| 117 _subscriptionFuture = | |
| 118 app.vm.listenEventStream(VM.kGCStream, _onEvent); | |
| 119 } | |
| 120 | |
| 121 @override | |
| 122 void detached() { | |
| 123 cancelFutureSubscription(_subscriptionFuture); | |
| 124 _subscriptionFuture = null; | |
| 125 super.detached(); | |
| 126 } | |
| 127 | |
| 128 // Keep at most one outstanding auto-refresh RPC. | |
| 129 bool refreshAutoPending = false; | |
| 130 bool refreshAutoQueued = false; | |
| 131 | |
| 132 void _onEvent(ServiceEvent event) { | |
| 133 assert(event.kind == 'GC'); | |
| 134 if (autoRefresh) { | |
| 135 if (!refreshAutoPending) { | |
| 136 refreshAuto(); | |
| 137 } else { | |
| 138 // Remember to refresh once more, to ensure latest profile. | |
| 139 refreshAutoQueued = true; | |
| 140 } | |
| 141 } | |
| 142 } | |
| 143 | |
| 144 void refreshAuto() { | |
| 145 refreshAutoPending = true; | |
| 146 refreshAutoQueued = false; | |
| 147 refresh().then((_) { | |
| 148 refreshAutoPending = false; | |
| 149 // Keep refreshing if at least one GC event was received while waiting. | |
| 150 if (refreshAutoQueued) { | |
| 151 refreshAuto(); | |
| 152 } | |
| 153 }).catchError(app.handleException); | |
| 154 } | |
| 155 | |
| 156 static const _USED_INDEX = 0; | |
| 157 static const _FREE_INDEX = 1; | |
| 158 static const _EXTERNAL_INDEX = 2; | |
| 159 | |
| 160 static const _VALUE_INDEX = 1; | |
| 161 | |
| 162 void _initPieChartData(List rows) { | |
| 163 rows.add(['Used', 0]); | |
| 164 rows.add(['Free', 0]); | |
| 165 rows.add(['External', 0]); | |
| 166 } | |
| 167 | |
| 168 void _updatePieChartData(List rows, HeapSpace space) { | |
| 169 rows[_USED_INDEX][_VALUE_INDEX] = space.used; | |
| 170 rows[_FREE_INDEX][_VALUE_INDEX] = space.capacity - space.used; | |
| 171 rows[_EXTERNAL_INDEX][_VALUE_INDEX] = space.external; | |
| 172 } | |
| 173 | |
| 174 void _updatePieCharts() { | |
| 175 assert(profile != null); | |
| 176 _updatePieChartData(_newPieChartRows, isolate.newSpace); | |
| 177 _updatePieChartData(_oldPieChartRows, isolate.oldSpace); | |
| 178 } | |
| 179 | |
| 180 void _updateClasses() { | |
| 181 for (ServiceMap clsAllocations in profile['members']) { | |
| 182 Class cls = clsAllocations['class']; | |
| 183 if (cls == null) { | |
| 184 continue; | |
| 185 } | |
| 186 cls.newSpace.update(clsAllocations['new']); | |
| 187 cls.oldSpace.update(clsAllocations['old']); | |
| 188 } | |
| 189 } | |
| 190 | |
| 191 void _updateClassTable() { | |
| 192 classTable.clearRows(); | |
| 193 for (ServiceMap clsAllocations in profile['members']) { | |
| 194 Class cls = clsAllocations['class']; | |
| 195 if (cls == null) { | |
| 196 continue; | |
| 197 } | |
| 198 if (cls.hasNoAllocations) { | |
| 199 // If a class has no allocations, don't display it. | |
| 200 continue; | |
| 201 } | |
| 202 var row = [cls, | |
| 203 '', // Spacer column. | |
| 204 cls.newSpace.accumulated.bytes, | |
| 205 cls.newSpace.accumulated.instances, | |
| 206 cls.newSpace.current.bytes, | |
| 207 cls.newSpace.current.instances, | |
| 208 '', // Spacer column. | |
| 209 cls.oldSpace.accumulated.bytes, | |
| 210 cls.oldSpace.accumulated.instances, | |
| 211 cls.oldSpace.current.bytes, | |
| 212 cls.oldSpace.current.instances]; | |
| 213 classTable.addRow(new SortedTableRow(row)); | |
| 214 } | |
| 215 classTable.sort(); | |
| 216 } | |
| 217 | |
| 218 void _addClassTableDomRow() { | |
| 219 assert(_classTableBody != null); | |
| 220 var tr = new TableRowElement(); | |
| 221 | |
| 222 // Add class ref. | |
| 223 var cell = tr.insertCell(-1); | |
| 224 ClassRefElement classRef = new Element.tag('class-ref'); | |
| 225 cell.children.add(classRef); | |
| 226 | |
| 227 // Add spacer. | |
| 228 cell = tr.insertCell(-1); | |
| 229 cell.classes.add('left-border-spacer'); | |
| 230 | |
| 231 // Add new space. | |
| 232 cell = tr.insertCell(-1); | |
| 233 cell = tr.insertCell(-1); | |
| 234 cell = tr.insertCell(-1); | |
| 235 cell = tr.insertCell(-1); | |
| 236 | |
| 237 // Add spacer. | |
| 238 cell = tr.insertCell(-1); | |
| 239 cell.classes.add('left-border-spacer'); | |
| 240 | |
| 241 // Add old space. | |
| 242 cell = tr.insertCell(-1); | |
| 243 cell = tr.insertCell(-1); | |
| 244 cell = tr.insertCell(-1); | |
| 245 cell = tr.insertCell(-1); | |
| 246 | |
| 247 // Add row to table. | |
| 248 _classTableBody.children.add(tr); | |
| 249 } | |
| 250 | |
| 251 void _fillClassTableDomRow(TableRowElement tr, int rowIndex) { | |
| 252 const SPACER_COLUMNS = const [1, 6]; | |
| 253 | |
| 254 var row = classTable.rows[rowIndex]; | |
| 255 // Add class ref. | |
| 256 ClassRefElementWrapper classRef = tr.children[0].children[0]; | |
| 257 classRef.ref = row.values[0]; | |
| 258 | |
| 259 for (var i = 1; i < row.values.length; i++) { | |
| 260 if (SPACER_COLUMNS.contains(i)) { | |
| 261 // Skip spacer columns. | |
| 262 continue; | |
| 263 } | |
| 264 var cell = tr.children[i]; | |
| 265 cell.title = row.values[i].toString(); | |
| 266 cell.text = classTable.getFormattedValue(rowIndex, i); | |
| 267 if (i > 1) { // Numbers. | |
| 268 cell.style.textAlign = 'right'; | |
| 269 cell.style.paddingLeft = '1em'; | |
| 270 } | |
| 271 } | |
| 272 } | |
| 273 | |
| 274 void _updateClassTableInDom() { | |
| 275 assert(_classTableBody != null); | |
| 276 // Resize DOM table. | |
| 277 if (_classTableBody.children.length > classTable.sortedRows.length) { | |
| 278 // Shrink the table. | |
| 279 var deadRows = | |
| 280 _classTableBody.children.length - classTable.sortedRows.length; | |
| 281 for (var i = 0; i < deadRows; i++) { | |
| 282 _classTableBody.children.removeLast(); | |
| 283 } | |
| 284 } else if (_classTableBody.children.length < classTable.sortedRows.length) { | |
| 285 // Grow table. | |
| 286 var newRows = | |
| 287 classTable.sortedRows.length - _classTableBody.children.length; | |
| 288 for (var i = 0; i < newRows; i++) { | |
| 289 _addClassTableDomRow(); | |
| 290 } | |
| 291 } | |
| 292 assert(_classTableBody.children.length == classTable.sortedRows.length); | |
| 293 // Fill table. | |
| 294 for (var i = 0; i < classTable.sortedRows.length; i++) { | |
| 295 var rowIndex = classTable.sortedRows[i]; | |
| 296 var tr = _classTableBody.children[i]; | |
| 297 _fillClassTableDomRow(tr, rowIndex); | |
| 298 } | |
| 299 } | |
| 300 | |
| 301 void _drawCharts() { | |
| 302 _newPieChart.draw(); | |
| 303 _oldPieChart.draw(); | |
| 304 } | |
| 305 | |
| 306 @observable void changeSort(Event e, var detail, Element target) { | |
| 307 if (target is TableCellElement) { | |
| 308 if (classTable.sortColumnIndex != target.cellIndex) { | |
| 309 classTable.sortColumnIndex = target.cellIndex; | |
| 310 classTable.sortDescending = true; | |
| 311 } else { | |
| 312 classTable.sortDescending = !classTable.sortDescending; | |
| 313 } | |
| 314 classTable.sort(); | |
| 315 _updateClassTableInDom(); | |
| 316 } | |
| 317 } | |
| 318 | |
| 319 void isolateChanged(oldValue) { | |
| 320 if (isolate == null) { | |
| 321 profile = null; | |
| 322 return; | |
| 323 } | |
| 324 isolate.invokeRpc('_getAllocationProfile', {}) | |
| 325 .then(_update) | |
| 326 .catchError(app.handleException); | |
| 327 } | |
| 328 | |
| 329 Future refresh() { | |
| 330 if (isolate == null) { | |
| 331 return new Future.value(null); | |
| 332 } | |
| 333 return isolate.invokeRpc('_getAllocationProfile', {}) | |
| 334 .then(_update); | |
| 335 } | |
| 336 | |
| 337 Future refreshGC() { | |
| 338 if (isolate == null) { | |
| 339 return new Future.value(null); | |
| 340 } | |
| 341 return isolate.invokeRpc('_getAllocationProfile', { 'gc': 'full' }) | |
| 342 .then(_update); | |
| 343 } | |
| 344 | |
| 345 Future resetAccumulator() { | |
| 346 if (isolate == null) { | |
| 347 return new Future.value(null); | |
| 348 } | |
| 349 return isolate.invokeRpc('_getAllocationProfile', { 'reset': 'true' }) | |
| 350 .then(_update); | |
| 351 } | |
| 352 | |
| 353 void _update(ServiceMap newProfile) { | |
| 354 profile = newProfile; | |
| 355 } | |
| 356 | |
| 357 void profileChanged(oldValue) { | |
| 358 if (profile == null) { | |
| 359 return; | |
| 360 } | |
| 361 isolate.updateHeapsFromMap(profile['heaps']); | |
| 362 var millis = int.parse(profile['dateLastAccumulatorReset']); | |
| 363 if (millis != 0) { | |
| 364 lastAccumulatorReset = | |
| 365 new DateTime.fromMillisecondsSinceEpoch(millis).toString(); | |
| 366 } | |
| 367 millis = int.parse(profile['dateLastServiceGC']); | |
| 368 if (millis != 0) { | |
| 369 lastServiceGC = | |
| 370 new DateTime.fromMillisecondsSinceEpoch(millis).toString(); | |
| 371 } | |
| 372 _updatePieCharts(); | |
| 373 _updateClasses(); | |
| 374 _updateClassTable(); | |
| 375 _updateClassTableInDom(); | |
| 376 _drawCharts(); | |
| 377 notifyPropertyChange(#formattedAverage, 0, 1); | |
| 378 notifyPropertyChange(#formattedTotalCollectionTime, 0, 1); | |
| 379 notifyPropertyChange(#formattedCollections, 0, 1); | |
| 380 } | |
| 381 | |
| 382 @observable String formattedAverage(bool newSpace) { | |
| 383 if (profile == null) { | |
| 384 return ''; | |
| 385 } | |
| 386 var heap = newSpace ? isolate.newSpace : isolate.oldSpace; | |
| 387 var avg = ((heap.totalCollectionTimeInSeconds * 1000.0) / heap.collections); | |
| 388 return '${avg.toStringAsFixed(2)} ms'; | |
| 389 } | |
| 390 | |
| 391 @observable String formattedCollections(bool newSpace) { | |
| 392 if (profile == null) { | |
| 393 return ''; | |
| 394 } | |
| 395 var heap = newSpace ? isolate.newSpace : isolate.oldSpace; | |
| 396 return heap.collections.toString(); | |
| 397 } | |
| 398 | |
| 399 @observable String formattedTotalCollectionTime(bool newSpace) { | |
| 400 if (profile == null) { | |
| 401 return ''; | |
| 402 } | |
| 403 var heap = newSpace ? isolate.newSpace : isolate.oldSpace; | |
| 404 return '${Utils.formatSeconds(heap.totalCollectionTimeInSeconds)} secs'; | |
| 405 } | |
| 406 } | |
| OLD | NEW |