Index: runtime/observatory/lib/src/elements/cpu_profile.dart |
diff --git a/runtime/observatory/lib/src/elements/cpu_profile.dart b/runtime/observatory/lib/src/elements/cpu_profile.dart |
index b4406ac4ea2b9e176bceb51c89224799183fe8fb..479e6c3914c559edc979fbb8c4ee23d4fd463dd0 100644 |
--- a/runtime/observatory/lib/src/elements/cpu_profile.dart |
+++ b/runtime/observatory/lib/src/elements/cpu_profile.dart |
@@ -224,7 +224,7 @@ class CodeProfileTreeRow extends ProfileTreeRow<CodeCallTreeNode> { |
percentNode.text = percent; |
percentNode.style.minWidth = '5em'; |
percentNode.style.textAlign = 'right'; |
- percentNode.title = 'Self: $selfPercent'; |
+ percentNode.title = 'Executing: $selfPercent'; |
methodColumn.children.add(percentNode); |
// Gap. |
@@ -319,7 +319,7 @@ class FunctionProfileTreeRow extends ProfileTreeRow<FunctionCallTreeNode> { |
parentPercent.style.minWidth = '4em'; |
parentPercent.style.alignSelf = 'center'; |
parentPercent.style.textAlign = 'right'; |
- parentPercent.title = 'Self: $selfPercent'; |
+ parentPercent.title = 'Executing: $selfPercent'; |
functionRow.children.add(parentPercent); |
// Gap. |
@@ -346,7 +346,7 @@ class FunctionProfileTreeRow extends ProfileTreeRow<FunctionCallTreeNode> { |
functionRow.children.add(infoBox); |
if (node.profileFunction.function.kind.hasDartCode()) { |
- infoBox.children.add(div('Hot code for current node')); |
+ infoBox.children.add(div('Code for current node')); |
infoBox.children.add(br()); |
var totalTicks = node.totalCodesTicks; |
var numCodes = node.codes.length; |
@@ -392,7 +392,7 @@ class FunctionProfileTreeRow extends ProfileTreeRow<FunctionCallTreeNode> { |
}); |
if (node.profileFunction.function.kind.hasDartCode()) { |
- infoBox.children.add(div('Hot code containing function')); |
+ infoBox.children.add(div('Code containing function')); |
infoBox.children.add(br()); |
var totalTicks = profile.sampleCount; |
var codes = node.profileFunction.profileCodes; |
@@ -510,10 +510,11 @@ class CpuProfileElement extends ObservatoryElement { |
fetchTime = formatTimeMilliseconds(_sw.elapsedMilliseconds); |
} |
- _onLoadStarted() { |
+ Future _onLoadStarted() { |
_sw.reset(); |
_sw.start(); |
state = 'Loading'; |
+ return window.animationFrame; |
} |
_onLoadFinished() { |
@@ -537,9 +538,9 @@ class CpuProfileElement extends ObservatoryElement { |
} |
_onFetchStarted(); |
return isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) |
- .then((response) { |
+ .then((response) async { |
_onFetchFinished(); |
- _onLoadStarted(); |
+ await _onLoadStarted(); |
try { |
profile.load(isolate, response); |
_onLoadFinished(); |
@@ -610,3 +611,516 @@ class CpuProfileElement extends ObservatoryElement { |
codeTree.initialize(rootRow); |
} |
} |
+ |
+class NameSortedTable extends SortedTable { |
+ NameSortedTable(columns) : super(columns); |
+ @override |
+ dynamic getSortKeyFor(int row, int col) { |
+ if (col == FUNCTION_COLUMN) { |
+ // Use name as sort key. |
+ return rows[row].values[col].name; |
+ } |
+ return super.getSortKeyFor(row, col); |
+ } |
+ |
+ SortedTableRow rowFromIndex(int tableIndex) { |
+ final modelIndex = sortedRows[tableIndex]; |
+ return rows[modelIndex]; |
+ } |
+ |
+ static const FUNCTION_SPACER_COLUMNS = const []; |
+ static const FUNCTION_COLUMN = 2; |
+ TableRowElement _makeFunctionRow() { |
+ var tr = new TableRowElement(); |
+ var cell; |
+ |
+ // Add percentage. |
+ cell = tr.insertCell(-1); |
+ cell = tr.insertCell(-1); |
+ |
+ // Add function ref. |
+ cell = tr.insertCell(-1); |
+ var functionRef = new Element.tag('function-ref'); |
+ cell.children.add(functionRef); |
+ |
+ return tr; |
+ } |
+ |
+ static const CALL_SPACER_COLUMNS = const []; |
+ static const CALL_FUNCTION_COLUMN = 1; |
+ TableRowElement _makeCallRow() { |
+ var tr = new TableRowElement(); |
+ var cell; |
+ |
+ // Add percentage. |
+ cell = tr.insertCell(-1); |
+ // Add function ref. |
+ cell = tr.insertCell(-1); |
+ var functionRef = new Element.tag('function-ref'); |
+ cell.children.add(functionRef); |
+ return tr; |
+ } |
+ |
+ _updateRow(TableRowElement tr, |
+ int rowIndex, |
+ List spacerColumns, |
+ int refColumn) { |
+ var row = rows[rowIndex]; |
+ // Set reference |
+ var ref = tr.children[refColumn].children[0]; |
+ ref.ref = row.values[refColumn]; |
+ |
+ for (var i = 0; i < row.values.length; i++) { |
+ if (spacerColumns.contains(i) || (i == refColumn)) { |
+ // Skip spacer columns. |
+ continue; |
+ } |
+ var cell = tr.children[i]; |
+ cell.title = row.values[i].toString(); |
+ cell.text = getFormattedValue(rowIndex, i); |
+ } |
+ } |
+ |
+ _updateTableView(HtmlElement table, |
+ HtmlElement makeEmptyRow(), |
+ void onRowClick(TableRowElement tr), |
+ List spacerColumns, |
+ int refColumn) { |
+ assert(table != null); |
+ |
+ // Resize DOM table. |
+ if (table.children.length > sortedRows.length) { |
+ // Shrink the table. |
+ var deadRows = table.children.length - sortedRows.length; |
+ for (var i = 0; i < deadRows; i++) { |
+ table.children.removeLast(); |
+ } |
+ } else if (table.children.length < sortedRows.length) { |
+ // Grow table. |
+ var newRows = sortedRows.length - table.children.length; |
+ for (var i = 0; i < newRows; i++) { |
+ var row = makeEmptyRow(); |
+ row.onClick.listen((e) { |
+ e.stopPropagation(); |
+ e.preventDefault(); |
+ onRowClick(row); |
+ }); |
+ table.children.add(row); |
+ } |
+ } |
+ |
+ assert(table.children.length == sortedRows.length); |
+ |
+ // Fill table. |
+ for (var i = 0; i < sortedRows.length; i++) { |
+ var rowIndex = sortedRows[i]; |
+ var tr = table.children[i]; |
+ _updateRow(tr, rowIndex, spacerColumns, refColumn); |
+ } |
+ } |
+} |
+ |
+@CustomTag('cpu-profile-table') |
+class CpuProfileTableElement extends ObservatoryElement { |
+ final Stopwatch _sw = new Stopwatch(); |
+ final CpuProfile profile = new CpuProfile(); |
+ StreamSubscription _resizeSubscription; |
+ @observable NameSortedTable profileTable; |
+ @observable NameSortedTable profileCallersTable; |
+ @observable NameSortedTable profileCalleesTable; |
+ @observable ServiceFunction focusedFunction; |
+ @observable int focusedRow; |
+ @observable String fetchTime = ''; |
+ @observable String loadTime = ''; |
+ @observable String state = 'Requested'; |
+ @observable var exception; |
+ @observable var stackTrace; |
+ @observable Isolate isolate; |
+ @observable String sampleCount = ''; |
+ @observable String refreshTime = ''; |
+ @observable String sampleRate = ''; |
+ @observable String stackDepth = ''; |
+ @observable String timeSpan = ''; |
+ @observable String directionSelector = 'Up'; |
+ |
+ CpuProfileTableElement.created() : super.created() { |
+ var columns = [ |
+ new SortedTableColumn.withFormatter('Executing (%)', |
+ Utils.formatPercentNormalized), |
+ new SortedTableColumn.withFormatter('In stack (%)', |
+ Utils.formatPercentNormalized), |
+ new SortedTableColumn('Method'), |
+ ]; |
+ profileTable = new NameSortedTable(columns); |
+ profileTable.sortColumnIndex = 0; |
+ |
+ columns = [ |
+ new SortedTableColumn.withFormatter('Callees (%)', |
+ Utils.formatPercentNormalized), |
+ new SortedTableColumn('Method') |
+ ]; |
+ profileCalleesTable = new NameSortedTable(columns); |
+ profileCalleesTable.sortColumnIndex = 0; |
+ |
+ columns = [ |
+ new SortedTableColumn.withFormatter('Callers (%)', |
+ Utils.formatPercentNormalized), |
+ new SortedTableColumn('Method') |
+ ]; |
+ profileCallersTable = new NameSortedTable(columns); |
+ profileCallersTable.sortColumnIndex = 0; |
+ } |
+ |
+ attached() { |
+ super.attached(); |
+ _resizeSubscription = window.onResize.listen((_) => _updateSize()); |
+ _updateSize(); |
+ } |
+ |
+ detached() { |
+ super.detached(); |
+ if (_resizeSubscription != null) { |
+ _resizeSubscription.cancel(); |
+ } |
+ } |
+ |
+ _updateSize() { |
+ HtmlElement e = $['main']; |
+ final totalHeight = window.innerHeight; |
+ final top = e.offset.top; |
+ final bottomMargin = 32; |
+ final mainHeight = totalHeight - top - bottomMargin; |
+ e.style.setProperty('height', '${mainHeight}px'); |
+ } |
+ |
+ isolateChanged() { |
+ _getCpuProfile().whenComplete(checkParameters); |
+ } |
+ |
+ checkParameters() { |
+ var functionId = app.locationManager.uri.queryParameters['functionId']; |
+ if (functionId == null) { |
+ _focusOnFunction(null); |
+ return; |
+ } |
+ if (isolate == null) { |
+ return; |
+ } |
+ isolate.getObject(functionId).then((func) => _focusOnFunction(func)); |
+ } |
+ |
+ void directionSelectorChanged(oldValue) { |
+ _updateFunctionTreeView(); |
+ } |
+ |
+ void refresh(var done) { |
+ _getCpuProfile().whenComplete(done); |
+ } |
+ |
+ void clear(var done) { |
+ _clearCpuProfile().whenComplete(done); |
+ } |
+ |
+ _onFetchStarted() { |
+ _sw.reset(); |
+ _sw.start(); |
+ state = 'Requested'; |
+ } |
+ |
+ _onFetchFinished() { |
+ _sw.stop(); |
+ fetchTime = formatTimeMilliseconds(_sw.elapsedMilliseconds); |
+ } |
+ |
+ _onLoadStarted() { |
+ _sw.reset(); |
+ _sw.start(); |
+ state = 'Loading'; |
+ } |
+ |
+ _onLoadFinished() { |
+ _sw.stop(); |
+ loadTime = formatTimeMilliseconds(_sw.elapsedMilliseconds); |
+ state = 'Loaded'; |
+ } |
+ |
+ Future _clearCpuProfile() { |
+ profile.clear(); |
+ _clearView(); |
+ if (isolate == null) { |
+ return new Future.value(null); |
+ } |
+ return isolate.invokeRpc('clearCpuProfile', { }) |
+ .then((ServiceMap response) { |
+ _updateView(); |
+ }); |
+ } |
+ |
+ Future _getCpuProfile() { |
+ profile.clear(); |
+ _clearView(); |
+ if (isolate == null) { |
+ return new Future.value(null); |
+ } |
+ _onFetchStarted(); |
+ return isolate.invokeRpc('getCpuProfile', { 'tags': 'None' }) |
+ .then((response) { |
+ _onFetchFinished(); |
+ _onLoadStarted(); |
+ try { |
+ profile.load(isolate, response); |
+ profile.buildFunctionCallerAndCallees(); |
+ _onLoadFinished(); |
+ _updateView(); |
+ } catch (e, st) { |
+ state = 'Exception'; |
+ exception = e; |
+ stackTrace = st; |
+ } |
+ }).catchError((e, st) { |
+ state = 'Exception'; |
+ exception = e; |
+ stackTrace = st; |
+ }); |
+ } |
+ |
+ _clearView() { |
+ profileTable.clearRows(); |
+ _renderTable(); |
+ } |
+ |
+ _updateView() { |
+ sampleCount = profile.sampleCount.toString(); |
+ refreshTime = new DateTime.now().toString(); |
+ stackDepth = profile.stackDepth.toString(); |
+ sampleRate = profile.sampleRate.toStringAsFixed(0); |
+ timeSpan = formatTime(profile.timeSpan); |
+ _buildFunctionTable(); |
+ _renderTable(); |
+ _updateFunctionTreeView(); |
+ } |
+ |
+ int _findFunctionRow(ServiceFunction function) { |
+ for (var i = 0; i < profileTable.sortedRows.length; i++) { |
+ var rowIndex = profileTable.sortedRows[i]; |
+ var row = profileTable.rows[rowIndex]; |
+ if (row.values[NameSortedTable.FUNCTION_COLUMN] == function) { |
+ return i; |
+ } |
+ } |
+ return -1; |
+ } |
+ |
+ _scrollToFunction(ServiceFunction function) { |
+ TableSectionElement tableBody = $['profile-table']; |
+ var row = _findFunctionRow(function); |
+ if (row == -1) { |
+ return; |
+ } |
+ tableBody.children[row].classes.remove('shake'); |
+ // trigger reflow. |
+ tableBody.children[row].offsetHeight; |
+ tableBody.children[row].scrollIntoView(ScrollAlignment.CENTER); |
+ tableBody.children[row].classes.add('shake'); |
+ } |
+ |
+ _clearFocusedFunction() { |
+ TableSectionElement tableBody = $['profile-table']; |
+ // Clear current focus. |
+ if (focusedRow != null) { |
+ tableBody.children[focusedRow].classes.remove('focused'); |
+ } |
+ focusedRow = null; |
+ focusedFunction = null; |
+ } |
+ |
+ _focusOnFunction(ServiceFunction function) { |
+ if (focusedFunction == function) { |
+ // Do nothing. |
+ return; |
+ } |
+ |
+ _clearFocusedFunction(); |
+ |
+ if (function == null) { |
+ _updateFunctionTreeView(); |
+ _clearCallTables(); |
+ return; |
+ } |
+ |
+ var row = _findFunctionRow(function); |
+ if (row == -1) { |
+ _updateFunctionTreeView(); |
+ _clearCallTables(); |
+ return; |
+ } |
+ |
+ focusedRow = row; |
+ focusedFunction = function; |
+ |
+ TableSectionElement tableBody = $['profile-table']; |
+ tableBody.children[focusedRow].classes.add('focused'); |
+ _updateFunctionTreeView(); |
+ _buildCallersTable(focusedFunction); |
+ _buildCalleesTable(focusedFunction); |
+ } |
+ |
+ _onRowClick(TableRowElement tr) { |
+ var tableBody = $['profile-table']; |
+ var row = profileTable.rowFromIndex(tableBody.children.indexOf(tr)); |
+ var function = row.values[NameSortedTable.FUNCTION_COLUMN]; |
+ app.locationManager.goParameter( |
+ { |
+ 'functionId': function.id |
+ } |
+ ); |
+ } |
+ |
+ _renderTable() { |
+ profileTable._updateTableView($['profile-table'], |
+ profileTable._makeFunctionRow, |
+ _onRowClick, |
+ NameSortedTable.FUNCTION_SPACER_COLUMNS, |
+ NameSortedTable.FUNCTION_COLUMN); |
+ } |
+ |
+ _buildFunctionTable() { |
+ for (var func in profile.functions) { |
+ if ((func.exclusiveTicks == 0) && (func.inclusiveTicks == 0)) { |
+ // Skip. |
+ continue; |
+ } |
+ var row = [ |
+ func.normalizedExclusiveTicks, |
+ func.normalizedInclusiveTicks, |
+ func.function, |
+ ]; |
+ profileTable.addRow(new SortedTableRow(row)); |
+ } |
+ profileTable.sort(); |
+ } |
+ |
+ _renderCallTable(TableSectionElement view, |
+ NameSortedTable model, |
+ void onRowClick(TableRowElement tr)) { |
+ model._updateTableView(view, |
+ model._makeCallRow, |
+ onRowClick, |
+ NameSortedTable.CALL_SPACER_COLUMNS, |
+ NameSortedTable.CALL_FUNCTION_COLUMN); |
+ } |
+ |
+ _buildCallTable(Map<ProfileFunction, int> calls, |
+ NameSortedTable model) { |
+ model.clearRows(); |
+ if (calls == null) { |
+ return; |
+ } |
+ var sum = 0; |
+ calls.values.forEach((i) => sum += i); |
+ calls.forEach((func, count) { |
+ var row = [ |
+ count / sum, |
+ func.function, |
+ ]; |
+ model.addRow(new SortedTableRow(row)); |
+ }); |
+ model.sort(); |
+ } |
+ |
+ _clearCallTables() { |
+ _buildCallersTable(null); |
+ _buildCalleesTable(null); |
+ } |
+ |
+ _onCallersClick(TableRowElement tr) { |
+ var table = $['callers-table']; |
+ final row = profileCallersTable.rowFromIndex(table.children.indexOf(tr)); |
+ var function = row.values[NameSortedTable.CALL_FUNCTION_COLUMN]; |
+ _scrollToFunction(function); |
+ } |
+ |
+ _buildCallersTable(ServiceFunction function) { |
+ var calls = (function != null) ? function.profile.callers : null; |
+ var table = $['callers-table']; |
+ _buildCallTable(calls, profileCallersTable); |
+ _renderCallTable(table, profileCallersTable, _onCallersClick); |
+ } |
+ |
+ _onCalleesClick(TableRowElement tr) { |
+ var table = $['callees-table']; |
+ final row = profileCalleesTable.rowFromIndex(table.children.indexOf(tr)); |
+ var function = row.values[NameSortedTable.CALL_FUNCTION_COLUMN]; |
+ _scrollToFunction(function); |
+ } |
+ |
+ _buildCalleesTable(ServiceFunction function) { |
+ var calls = (function != null) ? function.profile.callees : null; |
+ var table = $['callees-table']; |
+ _buildCallTable(calls, profileCalleesTable); |
+ _renderCallTable(table, profileCalleesTable, _onCalleesClick); |
+ } |
+ |
+ _changeSort(Element target, NameSortedTable table) { |
+ if (target is TableCellElement) { |
+ if (table.sortColumnIndex != target.cellIndex) { |
+ table.sortColumnIndex = target.cellIndex; |
+ table.sortDescending = true; |
+ } else { |
+ table.sortDescending = !profileTable.sortDescending; |
+ } |
+ table.sort(); |
+ } |
+ } |
+ |
+ changeSortProfile(Event e, var detail, Element target) { |
+ _changeSort(target, profileTable); |
+ _renderTable(); |
+ } |
+ |
+ changeSortCallers(Event e, var detail, Element target) { |
+ _changeSort(target, profileCallersTable); |
+ _renderCallTable($['callers-table'], profileCallersTable, _onCallersClick); |
+ } |
+ |
+ changeSortCallees(Event e, var detail, Element target) { |
+ _changeSort(target, profileCalleesTable); |
+ _renderCallTable($['callees-table'], profileCalleesTable, _onCalleesClick); |
+ } |
+ |
+ ////// |
+ /// |
+ /// Function tree. |
+ /// |
+ TableTree functionTree; |
+ _updateFunctionTreeView() { |
+ if (functionTree != null) { |
+ functionTree.clear(); |
+ functionTree = null; |
+ } |
+ _buildFunctionTree(); |
+ } |
+ |
+ void _buildFunctionTree() { |
+ if (functionTree == null) { |
+ var tableBody = shadowRoot.querySelector('#treeBody'); |
+ assert(tableBody != null); |
+ functionTree = new TableTree(tableBody, 2); |
+ } |
+ if (focusedFunction == null) { |
+ return; |
+ } |
+ bool exclusive = directionSelector == 'Up'; |
+ var tree = profile.loadFunctionTree(exclusive ? 'exclusive' : 'inclusive'); |
+ if (tree == null) { |
+ return; |
+ } |
+ var filter = (FunctionCallTreeNode node) { |
+ return node.profileFunction.function == focusedFunction; |
+ }; |
+ tree = tree.filtered(filter); |
+ var rootRow = |
+ new FunctionProfileTreeRow(functionTree, null, profile, tree.root); |
+ functionTree.initialize(rootRow); |
+ } |
+} |