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..132e43b871b5af3caa3a3be33be72df1fb451670 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,149 +39,208 @@ class HeapProfileElement extends ObservatoryElement { |
var _oldPieDataTable; |
var _oldPieChart; |
- @observable SortedTable classTable; |
+ @observable ClassSortedTable classTable; |
+ var _classTableBody; |
@published ServiceMap profile; |
+ @observable Isolate isolate; |
+ |
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; |
- } |
- _newPieChart.draw(_newPieDataTable); |
- _oldPieChart.draw(_oldPieDataTable); |
+ 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); |
} |
- @observable void changeSort(Event e, var detail, Element target) { |
- if (target is TableCellElement) { |
- if (classTable.sortColumnIndex != target.cellIndex) { |
- classTable.sortColumnIndex = target.cellIndex; |
- classTable.sort(); |
+ void _fillClassTableDomRow(TableRowElement tr, int rowIndex) { |
+ const SPACER_COLUMNS = const [1, 6]; |
+ |
+ var row = classTable.rows[rowIndex]; |
+ // Add class ref. |
+ ClassRefElement classRef = tr.children[0].children[0]; |
+ classRef.ref = row.values[0]; |
+ |
+ for (var i = 1; i < row.values.length; i++) { |
+ if (SPACER_COLUMNS.contains(i)) { |
+ // Skip spacer columns. |
+ continue; |
} |
+ var cell = tr.children[i]; |
+ cell.title = row.values[i].toString(); |
+ cell.text = classTable.getFormattedValue(rowIndex, i); |
} |
} |
- bool _classHasNoAllocations(Map v) { |
- var newSpace = v['new']; |
- var oldSpace = v['old']; |
- for (var allocation in newSpace) { |
- if (allocation != 0) { |
- return false; |
+ 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(); |
} |
- } |
- for (var allocation in oldSpace) { |
- if (allocation != 0) { |
- return false; |
+ } 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(); |
} |
} |
- return true; |
+ 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); |
+ } |
} |
- 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]; |
+ void _drawCharts() { |
+ _newPieChart.draw(_newPieDataTable); |
+ _oldPieChart.draw(_oldPieDataTable); |
+ } |
+ |
+ @observable void changeSort(Event e, var detail, Element target) { |
+ if (target is TableCellElement) { |
+ if (classTable.sortColumnIndex != target.cellIndex) { |
+ classTable.sortColumnIndex = target.cellIndex; |
+ classTable.sortDescending = true; |
+ } else { |
+ classTable.sortDescending = !classTable.sortDescending; |
+ } |
+ classTable.sort(); |
+ _updateClassTableInDom(); |
} |
- throw new FallThroughError(); |
} |
void refresh(var done) { |
@@ -182,77 +248,78 @@ class HeapProfileElement extends ObservatoryElement { |
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; |
+ } |
+ 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); |
+ _updatePieCharts(); |
+ _updateClasses(); |
+ _updateClassTable(); |
+ _updateClassTableInDom(); |
+ _drawCharts(); |
+ 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'; |
} |
} |