| Index: runtime/observatory/lib/src/elements/script_inset.dart
|
| diff --git a/runtime/observatory/lib/src/elements/script_inset.dart b/runtime/observatory/lib/src/elements/script_inset.dart
|
| index aa3c0a832024d7f1eb4bd5f4f08c26a4b97f2383..d54e320cc45f18617a445c0543a6997ebb5186a8 100644
|
| --- a/runtime/observatory/lib/src/elements/script_inset.dart
|
| +++ b/runtime/observatory/lib/src/elements/script_inset.dart
|
| @@ -361,6 +361,42 @@ class FunctionDeclarationAnnotation extends DeclarationAnnotation {
|
| }
|
| }
|
|
|
| +class ScriptLineProfile {
|
| + ScriptLineProfile(this.line, this.sampleCount);
|
| +
|
| + static const kHotThreshold = 0.05; // 5%.
|
| + static const kMediumThreshold = 0.02; // 2%.
|
| +
|
| + final int line;
|
| + final int sampleCount;
|
| +
|
| + int selfTicks = 0;
|
| + int totalTicks = 0;
|
| +
|
| + void process(int exclusive, int inclusive) {
|
| + selfTicks += exclusive;
|
| + totalTicks += inclusive;
|
| + }
|
| +
|
| + String get formattedSelfTicks {
|
| + return Utils.formatPercent(selfTicks, sampleCount);
|
| + }
|
| +
|
| + String get formattedTotalTicks {
|
| + return Utils.formatPercent(totalTicks, sampleCount);
|
| + }
|
| +
|
| + double _percent() {
|
| + if (sampleCount == 0) {
|
| + return 0.0;
|
| + }
|
| + return totalTicks / sampleCount;
|
| + }
|
| +
|
| + bool get isHot => _percent() > kHotThreshold;
|
| + bool get isMedium => _percent() > kMediumThreshold;
|
| +}
|
| +
|
| /// Box with script source code in it.
|
| @CustomTag('script-inset')
|
| class ScriptInsetElement extends ObservatoryElement {
|
| @@ -378,6 +414,7 @@ class ScriptInsetElement extends ObservatoryElement {
|
|
|
| @published Element scroller;
|
| RefreshButtonElement _refreshButton;
|
| + ToggleButtonElement _toggleProfileButton;
|
|
|
| int _currentLine;
|
| int _currentCol;
|
| @@ -387,6 +424,7 @@ class ScriptInsetElement extends ObservatoryElement {
|
| Map<int, List<ServiceMap>> _rangeMap = {};
|
| Set _callSites = new Set<CallSite>();
|
| Set _possibleBreakpointLines = new Set<int>();
|
| + Map<int, ScriptLineProfile> _profileMap = {};
|
|
|
| var annotations = [];
|
| var annotationsCursor;
|
| @@ -396,6 +434,7 @@ class ScriptInsetElement extends ObservatoryElement {
|
| StreamSubscription _scrollSubscription;
|
|
|
| bool hasLoadedLibraryDeclarations = false;
|
| + bool _includeProfile = false;
|
|
|
| String makeLineId(int line) {
|
| return 'line-$line';
|
| @@ -435,13 +474,17 @@ class ScriptInsetElement extends ObservatoryElement {
|
| }
|
|
|
| void _onScroll(event) {
|
| - if (_refreshButton == null) {
|
| - return;
|
| + if (_refreshButton != null) {
|
| + var newTop = _buttonTop(_refreshButton);
|
| + if (_refreshButton.style.top != newTop) {
|
| + _refreshButton.style.top = '${newTop}px';
|
| + }
|
| }
|
| - var currentTop = _refreshButton.style.top;
|
| - var newTop = _refreshButtonTop();
|
| - if (currentTop != newTop) {
|
| - _refreshButton.style.top = '${newTop}px';
|
| + if (_toggleProfileButton != null) {
|
| + var newTop = _buttonTop(_toggleProfileButton);
|
| + if (_toggleProfileButton.style.top != newTop) {
|
| + _toggleProfileButton.style.top = '${newTop}px';
|
| + }
|
| }
|
| }
|
|
|
| @@ -535,12 +578,18 @@ class ScriptInsetElement extends ObservatoryElement {
|
|
|
| // Build _rangeMap and _callSites from a source report.
|
| Future _refreshSourceReport() async {
|
| + var reports = [Isolate.kCallSitesReport,
|
| + Isolate.kPossibleBreakpointsReport];
|
| + if (_includeProfile) {
|
| + reports.add(Isolate.kProfileReport);
|
| + }
|
| var sourceReport = await script.isolate.getSourceReport(
|
| - [Isolate.kCallSitesReport, Isolate.kPossibleBreakpointsReport],
|
| + reports,
|
| script, startPos, endPos);
|
| _possibleBreakpointLines = getPossibleBreakpointLines(sourceReport, script);
|
| _rangeMap.clear();
|
| _callSites.clear();
|
| + _profileMap.clear();
|
| for (var range in sourceReport['ranges']) {
|
| int startLine = script.tokenToLine(range['startPos']);
|
| int endLine = script.tokenToLine(range['endPos']);
|
| @@ -552,6 +601,28 @@ class ScriptInsetElement extends ObservatoryElement {
|
| rangeList.add(range);
|
| }
|
| }
|
| + if (_includeProfile && range['profile'] != null) {
|
| + List positions = range['profile']['positions'];
|
| + List exclusiveTicks = range['profile']['exclusiveTicks'];
|
| + List inclusiveTicks = range['profile']['inclusiveTicks'];
|
| + int sampleCount = range['profile']['metadata']['sampleCount'];
|
| + assert(positions.length == exclusiveTicks.length);
|
| + assert(positions.length == inclusiveTicks.length);
|
| + for (int i = 0; i < positions.length; i++) {
|
| + if (positions[i] is String) {
|
| + // String positions are classifying token positions.
|
| + // TODO(johnmccutchan): Add classifier data to UI.
|
| + continue;
|
| + }
|
| + int line = script.tokenToLine(positions[i]);
|
| + ScriptLineProfile lineProfile = _profileMap[line];
|
| + if (lineProfile == null) {
|
| + lineProfile = new ScriptLineProfile(line, sampleCount);
|
| + _profileMap[line] = lineProfile;
|
| + }
|
| + lineProfile.process(exclusiveTicks[i], inclusiveTicks[i]);
|
| + }
|
| + }
|
| if (range['compiled']) {
|
| var rangeCallSites = range['callSites'];
|
| if (rangeCallSites != null) {
|
| @@ -901,14 +972,14 @@ class ScriptInsetElement extends ObservatoryElement {
|
| }
|
| }
|
|
|
| - int _refreshButtonTop() {
|
| - if (_refreshButton == null) {
|
| + int _buttonTop(Element element) {
|
| + if (element == null) {
|
| return 5;
|
| }
|
| const padding = 5;
|
| const navbarHeight = NavBarElement.height;
|
| var rect = getBoundingClientRect();
|
| - var buttonHeight = _refreshButton.clientHeight;
|
| + var buttonHeight = element.clientHeight;
|
| return min(max(0, navbarHeight - rect.top) + padding,
|
| rect.height - (buttonHeight + padding));
|
| }
|
| @@ -917,9 +988,35 @@ class ScriptInsetElement extends ObservatoryElement {
|
| var button = new Element.tag('refresh-button');
|
| button.style.position = 'absolute';
|
| button.style.display = 'inline-block';
|
| - button.style.top = '${_refreshButtonTop()}px';
|
| + button.style.top = '${_buttonTop(null)}px';
|
| button.style.right = '5px';
|
| button.callback = _refresh;
|
| + button.title = 'Refresh coverage';
|
| + return button;
|
| + }
|
| +
|
| + ToggleButtonElement _newToggleProfileButton() {
|
| + ToggleButtonElement button = new Element.tag('toggle-button');
|
| + button.style.position = 'absolute';
|
| + button.style.display = 'inline-block';
|
| + button.style.top = '${_buttonTop(null)}px';
|
| + button.style.right = '30px';
|
| + button.title = 'Toggle CPU profile information';
|
| + button.callback = (enabled) async {
|
| + _includeProfile = enabled;
|
| + if (button.children.length > 0) {
|
| + var content = button.children[0];
|
| + if (enabled) {
|
| + content.style.color = 'black';
|
| + } else {
|
| + content.style.color = 'rgba(0, 0, 0 ,.3)';
|
| + }
|
| + }
|
| + await update();
|
| + };
|
| + button.children.add(new Element.tag('icon-whatshot'));
|
| + button.children[0].style.color = 'rgba(0, 0, 0 ,.3)';
|
| + button.enabled = _includeProfile;
|
| return button;
|
| }
|
|
|
| @@ -928,7 +1025,9 @@ class ScriptInsetElement extends ObservatoryElement {
|
| table.classes.add("sourceTable");
|
|
|
| _refreshButton = _newRefreshButton();
|
| + _toggleProfileButton = _newToggleProfileButton();
|
| table.append(_refreshButton);
|
| + table.append(_toggleProfileButton);
|
|
|
| if (_startLine == null || _endLine == null) {
|
| return table;
|
| @@ -996,10 +1095,60 @@ class ScriptInsetElement extends ObservatoryElement {
|
| e.classes.add("sourceRow");
|
| e.append(lineBreakpointElement(line));
|
| e.append(lineNumberElement(line, lineNumPad));
|
| + if (_includeProfile) {
|
| + e.append(lineProfileElement(line, false));
|
| + e.append(lineProfileElement(line, true));
|
| + }
|
| e.append(lineSourceElement(line));
|
| return e;
|
| }
|
|
|
| + Element lineProfileElement(ScriptLine line, bool self) {
|
| + var e = span('');
|
| + e.classes.add('noCopy');
|
| + if (self) {
|
| + e.title = 'Self %';
|
| + } else {
|
| + e.title = 'Total %';
|
| + }
|
| +
|
| + if (line == null) {
|
| + e.classes.add('notSourceProfile');
|
| + e.text = nbsp;
|
| + return e;
|
| + }
|
| +
|
| + var ranges = _rangeMap[line.line];
|
| + if ((ranges == null) || ranges.isEmpty) {
|
| + e.classes.add('notSourceProfile');
|
| + e.text = nbsp;
|
| + return e;
|
| + }
|
| +
|
| + ScriptLineProfile lineProfile = _profileMap[line.line];
|
| + if (lineProfile == null) {
|
| + e.classes.add('noProfile');
|
| + e.text = nbsp;
|
| + return e;
|
| + }
|
| +
|
| + if (self) {
|
| + e.text = lineProfile.formattedSelfTicks;
|
| + } else {
|
| + e.text = lineProfile.formattedTotalTicks;
|
| + }
|
| +
|
| + if (lineProfile.isHot) {
|
| + e.classes.add('hotProfile');
|
| + } else if (lineProfile.isMedium) {
|
| + e.classes.add('mediumProfile');
|
| + } else {
|
| + e.classes.add('coldProfile');
|
| + }
|
| +
|
| + return e;
|
| + }
|
| +
|
| Element lineBreakpointElement(ScriptLine line) {
|
| var e = new DivElement();
|
| if (line == null || !_possibleBreakpointLines.contains(line.line)) {
|
| @@ -1191,6 +1340,22 @@ class RefreshButtonElement extends PolymerElement {
|
| }
|
|
|
|
|
| +@CustomTag('toggle-button')
|
| +class ToggleButtonElement extends PolymerElement {
|
| + ToggleButtonElement.created() : super.created();
|
| +
|
| + @published var callback = null;
|
| + @observable bool enabled = false;
|
| +
|
| + Future buttonClick(var event, var b, var c) async {
|
| + enabled = !enabled;
|
| + if (callback != null) {
|
| + await callback(enabled);
|
| + }
|
| + }
|
| +}
|
| +
|
| +
|
| @CustomTag('source-inset')
|
| class SourceInsetElement extends PolymerElement {
|
| SourceInsetElement.created() : super.created();
|
|
|