Chromium Code Reviews| Index: runtime/bin/vmservice/client/lib/src/elements/heap_profile.dart |
| diff --git a/runtime/bin/vmservice/client/lib/src/elements/heap_profile.dart b/runtime/bin/vmservice/client/lib/src/elements/heap_profile.dart |
| index 9626735d85fe3643820573a6b94d3c03f006dcf5..850710c2321446c2894e2a1fba9eef7bcda3b455 100644 |
| --- a/runtime/bin/vmservice/client/lib/src/elements/heap_profile.dart |
| +++ b/runtime/bin/vmservice/client/lib/src/elements/heap_profile.dart |
| @@ -8,21 +8,28 @@ import 'dart:html'; |
| import 'observatory_element.dart'; |
| import 'package:observatory/app.dart'; |
| import 'package:observatory/service.dart'; |
| -import 'package:logging/logging.dart'; |
| +import 'package:observatory/elements.dart'; |
| import 'package:polymer/polymer.dart'; |
| +class ClassSortedTable extends SortedTable { |
| + |
| + ClassSortedTable(columns) : super(columns); |
| + |
| + @override |
| + dynamic getSortKeyFor(int row, int col) { |
| + if (col == 0) { |
| + // Use class name as sort key. |
| + return rows[row].values[col].name; |
| + } |
| + return super.getSortKeyFor(row, col); |
| + } |
| +} |
| + |
| /// Displays an Error response. |
| @CustomTag('heap-profile') |
| class HeapProfileElement extends ObservatoryElement { |
| - // Indexes into VM provided map. |
| - static const ALLOCATED_BEFORE_GC = 0; |
| - static const ALLOCATED_BEFORE_GC_SIZE = 1; |
| - static const LIVE_AFTER_GC = 2; |
| - static const LIVE_AFTER_GC_SIZE = 3; |
| - static const ALLOCATED_SINCE_GC = 4; |
| - static const ALLOCATED_SINCE_GC_SIZE = 5; |
| - static const ACCUMULATED = 6; |
| - static const ACCUMULATED_SIZE = 7; |
| + @observable String lastServiceGC = '---'; |
| + @observable String lastAccumulatorReset = '---'; |
| // Pie chart of new space usage. |
| var _newPieDataTable; |
| @@ -32,92 +39,219 @@ class HeapProfileElement extends ObservatoryElement { |
| var _oldPieDataTable; |
| var _oldPieChart; |
| - @observable SortedTable classTable; |
| + @observable ClassSortedTable classTable; |
| + var _classTableBody; |
| @published ServiceMap profile; |
| + @observable Isolate isolate; |
| + |
| + void _trace(Stopwatch sw, String label) { |
| + print('$label took ${sw.elapsedMicroseconds} us.'); |
| + sw.reset(); |
| + } |
| + |
| + void _traceMillis(Stopwatch sw, String label) { |
| + print('$label took ${sw.elapsedMilliseconds} ms.'); |
| + sw.reset(); |
| + } |
| + |
| HeapProfileElement.created() : super.created() { |
| + // Create pie chart models. |
| _newPieDataTable = new DataTable(); |
| _newPieDataTable.addColumn('string', 'Type'); |
| _newPieDataTable.addColumn('number', 'Size'); |
| _oldPieDataTable = new DataTable(); |
| _oldPieDataTable.addColumn('string', 'Type'); |
| _oldPieDataTable.addColumn('number', 'Size'); |
| + |
| + // Create class table model. |
| var columns = [ |
| new SortedTableColumn('Class'), |
| - new SortedTableColumn.withFormatter('Accumulator Size (New)', |
| + new SortedTableColumn(''), // Spacer column. |
| + new SortedTableColumn.withFormatter('Accumulated Size (New)', |
| Utils.formatSize), |
| - new SortedTableColumn.withFormatter('Accumulator (New)', |
| + new SortedTableColumn.withFormatter('Accumulated Instances', |
| Utils.formatCommaSeparated), |
| - new SortedTableColumn.withFormatter('Current Size (New)', |
| + new SortedTableColumn.withFormatter('Current Size', |
| Utils.formatSize), |
| - new SortedTableColumn.withFormatter('Current (New)', |
| + new SortedTableColumn.withFormatter('Current Instances', |
| Utils.formatCommaSeparated), |
| + new SortedTableColumn(''), // Spacer column. |
| new SortedTableColumn.withFormatter('Accumulator Size (Old)', |
| Utils.formatSize), |
| - new SortedTableColumn.withFormatter('Accumulator (Old)', |
| + new SortedTableColumn.withFormatter('Accumulator Instances', |
| Utils.formatCommaSeparated), |
| - new SortedTableColumn.withFormatter('Current Size (Old)', |
| + new SortedTableColumn.withFormatter('Current Size', |
| Utils.formatSize), |
| - new SortedTableColumn.withFormatter('Current (Old)', |
| + new SortedTableColumn.withFormatter('Current Instances', |
| Utils.formatCommaSeparated) |
| ]; |
| - classTable = new SortedTable(columns); |
| - classTable.sortColumnIndex = 1; |
| + classTable = new ClassSortedTable(columns); |
| + // By default, start with accumulated new space bytes. |
| + classTable.sortColumnIndex = 2; |
| } |
| - void enteredView() { |
| - super.enteredView(); |
| + @override |
| + void attached() { |
| + super.attached(); |
| + // Grab the pie chart divs. |
| _newPieChart = new Chart('PieChart', |
| shadowRoot.querySelector('#newPieChart')); |
| - _newPieChart.options['title'] = 'New Space'; |
| _oldPieChart = new Chart('PieChart', |
| shadowRoot.querySelector('#oldPieChart')); |
| - _oldPieChart.options['title'] = 'Old Space'; |
| - _draw(); |
| + _classTableBody = shadowRoot.querySelector('#classTableBody'); |
| } |
| - void _updateChartData() { |
| - if ((profile == null) || (profile['members'] is! List) || |
| - (profile['members'].length == 0)) { |
| - return; |
| + void _updatePieCharts() { |
| + assert(profile != null); |
| + _newPieDataTable.clearRows(); |
| + var isolate = profile.isolate; |
| + _newPieDataTable.addRow(['Used', isolate.newSpace.used]); |
| + _newPieDataTable.addRow(['Free', |
| + isolate.newSpace.capacity - isolate.newSpace.used]); |
| + _newPieDataTable.addRow(['External', isolate.newSpace.external]); |
| + _oldPieDataTable.clearRows(); |
| + _oldPieDataTable.addRow(['Used', isolate.oldSpace.used]); |
| + _oldPieDataTable.addRow(['Free', |
| + isolate.oldSpace.capacity - isolate.oldSpace.used]); |
| + _oldPieDataTable.addRow(['External', isolate.oldSpace.external]); |
| + } |
| + |
| + void _updateClasses() { |
| + for (ServiceMap clsAllocations in profile['members']) { |
| + Class cls = clsAllocations['class']; |
| + if (cls == null) { |
| + continue; |
| + } |
| + cls.newSpace.update(clsAllocations['new']); |
| + cls.oldSpace.update(clsAllocations['old']); |
| } |
| - assert(classTable != null); |
| + } |
| + |
| + void _updateClassTable() { |
| classTable.clearRows(); |
| - for (ServiceMap cls in profile['members']) { |
| - if (_classHasNoAllocations(cls)) { |
| + for (ServiceMap clsAllocations in profile['members']) { |
| + Class cls = clsAllocations['class']; |
| + if (cls == null) { |
| + continue; |
| + } |
| + if (cls.hasNoAllocations) { |
| // If a class has no allocations, don't display it. |
| continue; |
| } |
| - var row = [cls['class'], |
| - _combinedTableColumnValue(cls, 1), |
| - _combinedTableColumnValue(cls, 2), |
| - _combinedTableColumnValue(cls, 3), |
| - _combinedTableColumnValue(cls, 4), |
| - _combinedTableColumnValue(cls, 5), |
| - _combinedTableColumnValue(cls, 6), |
| - _combinedTableColumnValue(cls, 7), |
| - _combinedTableColumnValue(cls, 8)]; |
| + var row = [cls, |
| + '', // Spacer column. |
| + cls.newSpace.accumulated.bytes, |
| + cls.newSpace.accumulated.instances, |
| + cls.newSpace.current.bytes, |
| + cls.newSpace.current.instances, |
| + '', // Spacer column. |
| + cls.oldSpace.accumulated.bytes, |
| + cls.oldSpace.accumulated.instances, |
| + cls.oldSpace.current.bytes, |
| + cls.oldSpace.current.instances]; |
| classTable.addRow(new SortedTableRow(row)); |
| } |
| classTable.sort(); |
| - _newPieDataTable.clearRows(); |
| - var heap = profile['heaps']['new']; |
| - _newPieDataTable.addRow(['Used', heap['used']]); |
| - _newPieDataTable.addRow(['Free', heap['capacity'] - heap['used']]); |
| - _newPieDataTable.addRow(['External', heap['external']]); |
| - _oldPieDataTable.clearRows(); |
| - heap = profile['heaps']['old']; |
| - _oldPieDataTable.addRow(['Used', heap['used']]); |
| - _oldPieDataTable.addRow(['Free', heap['capacity'] - heap['used']]); |
| - _oldPieDataTable.addRow(['External', heap['external']]); |
| - _draw(); |
| } |
| - void _draw() { |
| - if (_newPieChart == null) { |
| - return; |
| + void _addClassTableDomRow() { |
| + assert(_classTableBody != null); |
| + var tr = new TableRowElement(); |
| + |
| + // Add class ref. |
| + var cell = tr.insertCell(-1); |
| + ClassRefElement classRef = new Element.tag('class-ref'); |
| + cell.children.add(classRef); |
| + |
| + // Add spacer. |
| + cell = tr.insertCell(-1); |
| + cell.classes.add('left-border-spacer'); |
| + |
| + // Add new space. |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + |
| + // Add spacer. |
| + cell = tr.insertCell(-1); |
| + cell.classes.add('left-border-spacer'); |
| + |
| + // Add old space. |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + cell = tr.insertCell(-1); |
| + |
| + // Add row to table. |
| + _classTableBody.children.add(tr); |
| + } |
| + |
| + void _fillClassTableDomRow(TableRowElement tr, int rowIndex) { |
| + var row = classTable.rows[rowIndex]; |
| + // Add class ref. |
| + ClassRefElement classRef = tr.children[0].children[0]; |
| + classRef.ref = row.values[0]; |
| + |
| + var cell = tr.children[2]; |
| + cell.title = row.values[2].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 2); |
| + cell = tr.children[3]; |
| + cell.title = row.values[3].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 3); |
| + cell = tr.children[4]; |
| + cell.title = row.values[4].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 4); |
| + cell = tr.children[5]; |
| + cell.title = row.values[5].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 5); |
| + |
| + cell = tr.children[7]; |
| + cell.title = row.values[7].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 7); |
| + cell = tr.children[8]; |
| + cell.title = row.values[8].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 8); |
| + |
| + cell = tr.children[9]; |
| + cell.title = row.values[9].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 9); |
| + |
| + cell = tr.children[10]; |
| + cell.title = row.values[10].toString(); |
| + cell.text = classTable.getFormattedValue(rowIndex, 10); |
|
koda
2014/06/18 17:04:02
Consider having a constant SPACER_COLUMNS = [1, 6]
Cutch
2014/06/18 17:13:03
Done.
|
| + } |
| + |
| + void _updateClassTableInDom() { |
| + assert(_classTableBody != null); |
| + // Resize DOM table. |
| + if (_classTableBody.children.length > classTable.sortedRows.length) { |
| + // Shrink the table. |
| + var deadRows = |
| + _classTableBody.children.length - classTable.sortedRows.length; |
| + for (var i = 0; i < deadRows; i++) { |
| + _classTableBody.children.removeLast(); |
| + } |
| + } else if (_classTableBody.children.length < classTable.sortedRows.length) { |
| + // Grow table. |
| + var newRows = |
| + classTable.sortedRows.length - _classTableBody.children.length; |
| + for (var i = 0; i < newRows; i++) { |
| + _addClassTableDomRow(); |
| + } |
| + } |
| + assert(_classTableBody.children.length == classTable.sortedRows.length); |
| + // Fill table. |
| + for (var i = 0; i < classTable.sortedRows.length; i++) { |
| + var rowIndex = classTable.sortedRows[i]; |
| + var tr = _classTableBody.children[i]; |
| + _fillClassTableDomRow(tr, rowIndex); |
| } |
| + } |
| + |
| + void _drawCharts() { |
| _newPieChart.draw(_newPieDataTable); |
| _oldPieChart.draw(_oldPieDataTable); |
| } |
| @@ -126,133 +260,101 @@ class HeapProfileElement extends ObservatoryElement { |
| if (target is TableCellElement) { |
| if (classTable.sortColumnIndex != target.cellIndex) { |
| classTable.sortColumnIndex = target.cellIndex; |
| - classTable.sort(); |
| + classTable.sortDescending = true; |
| + } else { |
| + classTable.sortDescending = !classTable.sortDescending; |
| } |
| + classTable.sort(); |
| + Stopwatch sw = new Stopwatch()..start(); |
| + _updateClassTableInDom(); |
| + _traceMillis(sw, 'Inserting class table into DOM'); |
| } |
| } |
| - bool _classHasNoAllocations(Map v) { |
| - var newSpace = v['new']; |
| - var oldSpace = v['old']; |
| - for (var allocation in newSpace) { |
| - if (allocation != 0) { |
| - return false; |
| - } |
| - } |
| - for (var allocation in oldSpace) { |
| - if (allocation != 0) { |
| - return false; |
| - } |
| - } |
| - return true; |
| - } |
| - |
| - dynamic _combinedTableColumnValue(Map v, int index) { |
| - assert(index >= 0); |
| - assert(index < 9); |
| - switch (index) { |
| - case 0: |
| - return v['class']['user_name']; |
| - case 1: |
| - return v['new'][ACCUMULATED_SIZE]; |
| - case 2: |
| - return v['new'][ACCUMULATED]; |
| - case 3: |
| - return v['new'][LIVE_AFTER_GC_SIZE] + |
| - v['new'][ALLOCATED_SINCE_GC_SIZE]; |
| - case 4: |
| - return v['new'][LIVE_AFTER_GC] + |
| - v['new'][ALLOCATED_SINCE_GC]; |
| - case 5: |
| - return v['old'][ACCUMULATED_SIZE]; |
| - case 6: |
| - return v['old'][ACCUMULATED]; |
| - case 7: |
| - return v['old'][LIVE_AFTER_GC_SIZE] + |
| - v['old'][ALLOCATED_SINCE_GC_SIZE]; |
| - case 8: |
| - return v['old'][LIVE_AFTER_GC] + |
| - v['old'][ALLOCATED_SINCE_GC]; |
| - } |
| - throw new FallThroughError(); |
| - } |
| - |
| void refresh(var done) { |
| if (profile == null) { |
| return; |
| } |
| var isolate = profile.isolate; |
| - isolate.get('/allocationprofile').then((ServiceMap response) { |
| - assert(response['type'] == 'AllocationProfile'); |
| - profile = response; |
| - }).catchError((e, st) { |
| - Logger.root.info('$e $st'); |
| - }).whenComplete(done); |
| + isolate.get('/allocationprofile').then(_update).whenComplete(done); |
| } |
| void refreshGC(var done) { |
| - if (profile == null) { |
| - return; |
| - } |
| - var isolate = profile.isolate; |
| - isolate.get('/allocationprofile?gc=full').then((ServiceMap response) { |
| - assert(response['type'] == 'AllocationProfile'); |
| - profile = response; |
| - }).catchError((e, st) { |
| - Logger.root.info('$e $st'); |
| - }).whenComplete(done); |
| + if (profile == null) { |
| + return; |
| } |
| + var isolate = profile.isolate; |
| + isolate.get('/allocationprofile?gc=full').then(_update).whenComplete(done); |
| + } |
| void resetAccumulator(var done) { |
| if (profile == null) { |
| return; |
| } |
| var isolate = profile.isolate; |
| - isolate.get('/allocationprofile?reset=true').then((ServiceMap response) { |
| - assert(response['type'] == 'AllocationProfile'); |
| - profile = response; |
| - }).catchError((e, st) { |
| - Logger.root.info('$e $st'); |
| - }).whenComplete(done); |
| + isolate.get('/allocationprofile?reset=true').then(_update). |
| + whenComplete(done); |
| + } |
| + |
| + void _update(ServiceMap newProfile) { |
| + profile = newProfile; |
| } |
| void profileChanged(oldValue) { |
| - try { |
| - _updateChartData(); |
| - } catch (e, st) { |
| - Logger.root.info('$e $st'); |
| + if (profile == null) { |
| + return; |
| + } |
| + Stopwatch sw = new Stopwatch()..start(); |
| + isolate = profile.isolate; |
| + isolate.updateHeapsFromMap(profile['heaps']); |
| + var millis = int.parse(profile['dateLastAccumulatorReset']); |
| + if (millis != 0) { |
| + lastAccumulatorReset = |
| + new DateTime.fromMillisecondsSinceEpoch(millis).toString(); |
| + } |
| + millis = int.parse(profile['dateLastServiceGC']); |
| + if (millis != 0) { |
| + lastServiceGC = |
| + new DateTime.fromMillisecondsSinceEpoch(millis).toString(); |
| } |
| - notifyPropertyChange(#formattedAverage, [], formattedAverage); |
| - notifyPropertyChange(#formattedTotalCollectionTime, [], |
| - formattedTotalCollectionTime); |
| - notifyPropertyChange(#formattedCollections, [], formattedCollections); |
| + _trace(sw, 'Updating heaps'); |
| + _updatePieCharts(); |
| + _trace(sw, 'Updating pie charts'); |
| + _updateClasses(); |
| + _trace(sw, 'Updating classes'); |
| + _updateClassTable(); |
| + _trace(sw, 'Updating class table'); |
| + _updateClassTableInDom(); |
| + _traceMillis(sw, 'Inserting class table into DOM'); |
| + _drawCharts(); |
| + _traceMillis(sw, 'Drawing pie charts'); |
| + notifyPropertyChange(#formattedAverage, 0, 1); |
| + notifyPropertyChange(#formattedTotalCollectionTime, 0, 1); |
| + notifyPropertyChange(#formattedCollections, 0, 1); |
| } |
| @observable String formattedAverage(bool newSpace) { |
| if (profile == null) { |
| return ''; |
| } |
| - String space = newSpace ? 'new' : 'old'; |
| - Map heap = profile['heaps'][space]; |
| - var r = ((heap['time'] * 1000.0) / heap['collections']).toStringAsFixed(2); |
| - return '$r ms'; |
| + var heap = newSpace ? profile.isolate.newSpace : profile.isolate.oldSpace; |
| + var avg = ((heap.totalCollectionTimeInSeconds * 1000.0) / heap.collections); |
| + return '${avg.toStringAsFixed(2)} ms'; |
| } |
| @observable String formattedCollections(bool newSpace) { |
| if (profile == null) { |
| return ''; |
| } |
| - String space = newSpace ? 'new' : 'old'; |
| - Map heap = profile['heaps'][space]; |
| - return '${heap['collections']}'; |
| + var heap = newSpace ? profile.isolate.newSpace : profile.isolate.oldSpace; |
| + return heap.collections.toString(); |
| } |
| @observable String formattedTotalCollectionTime(bool newSpace) { |
| if (profile == null) { |
| return ''; |
| } |
| - String space = newSpace ? 'new' : 'old'; |
| - Map heap = profile['heaps'][space]; |
| - return '${Utils.formatSeconds(heap['time'])} secs'; |
| + var heap = newSpace ? profile.isolate.newSpace : profile.isolate.oldSpace; |
| + return '${Utils.formatSeconds(heap.totalCollectionTimeInSeconds)} secs'; |
| } |
| } |