| Index: pkg/compiler/tool/stats/client.dart
|
| diff --git a/pkg/compiler/tool/stats/client.dart b/pkg/compiler/tool/stats/client.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..db0cb1a12148e6f27ec254be32d7bda8348966cb
|
| --- /dev/null
|
| +++ b/pkg/compiler/tool/stats/client.dart
|
| @@ -0,0 +1,238 @@
|
| +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/// Client component to display [GlobalResult]s as a web app.
|
| +library compiler.tool.stats.client;
|
| +
|
| +import 'dart:html' hide Entry;
|
| +import 'dart:convert';
|
| +import 'package:compiler/src/stats/stats.dart';
|
| +import 'package:charcode/charcode.dart';
|
| +
|
| +GlobalResult data;
|
| +main() async {
|
| + data = GlobalResult.fromJson(
|
| + JSON.decode(await HttpRequest.getString('/data')));
|
| +
|
| + routeByHash();
|
| + window.onHashChange.listen((_) => routeByHash());
|
| +}
|
| +
|
| +/// Does basic routing for the client UI.
|
| +routeByHash() {
|
| + var hash = window.location.hash;
|
| + if (hash.isEmpty || hash == '#' || hash == '#!') {
|
| + handleHomePage();
|
| + } else if (hash.startsWith('#!')) {
|
| + handleFileView(hash.substring(2));
|
| + }
|
| +}
|
| +
|
| +/// Renders the home screen: a list of files with results.
|
| +handleHomePage() {
|
| + var files = UrlRetriever.run(data);
|
| + var html = new StringBuffer()..write('<ul>');
|
| + for (var file in files) {
|
| + html.write('<li> <a href="#!$file">$file</a></li>');
|
| + }
|
| + html.write('</ul>');
|
| + document.body.setInnerHtml('$html', treeSanitizer: NodeTreeSanitizer.trusted);
|
| +}
|
| +
|
| +/// Renders the results of a single file: the code with highlighting for each
|
| +/// send.
|
| +handleFileView(String path) async {
|
| + var contents = await HttpRequest.getString('file/$path');
|
| + var visitor = new SendHighlighter(path, contents);
|
| + data.accept(visitor);
|
| + var code = '${visitor.code}';
|
| + document.body.setInnerHtml('''
|
| + <div class="grid">
|
| + <div class="main code">$code</div>
|
| + <div id="selections" class="right code"></div>
|
| + </div>
|
| + ''',
|
| + treeSanitizer: NodeTreeSanitizer.trusted);
|
| +
|
| + var div = document.querySelector('#selections');
|
| + visitAllMetrics((metric, _) {
|
| + if (metric is GroupedMetric || metric.name == 'reachable functions') return;
|
| + var cssClassName = _classNameForMetric(metric);
|
| + var node = new Element.html('<div>'
|
| + '<span class="send $cssClassName inactive">${metric.name}</span>'
|
| + '</div>');
|
| + node.children[0].onClick.listen((_) {
|
| + document.querySelectorAll('.$cssClassName').classes.toggle('inactive');
|
| + });
|
| + div.append(node);
|
| + });
|
| +}
|
| +
|
| +/// Extracts urls for all files mentioned in the results.
|
| +class UrlRetriever extends RecursiveResultVisitor {
|
| + List<String> _paths = [];
|
| +
|
| + static List<String> run(GlobalResult results) {
|
| + var visitor = new UrlRetriever();
|
| + results.accept(visitor);
|
| + return visitor._paths;
|
| + }
|
| +
|
| + @override
|
| + visitCompilationUnit(CompilationUnitResult unit) {
|
| + _paths.add(unit.uri.path);
|
| + }
|
| +
|
| + @override
|
| + visitFunction(FunctionResult function) {}
|
| +}
|
| +
|
| +/// Visitors that highlights every send in the text of a file using HTML
|
| +/// `<span>` tags.
|
| +class SendHighlighter extends RecursiveResultVisitor {
|
| + final String path;
|
| + final StringEditBuffer code;
|
| +
|
| + SendHighlighter(this.path, String contents)
|
| + : code = new StringEditBuffer(contents) {
|
| + code.insert(0, '<span class="line">');
|
| + for (int i = 0; i < contents.length; i++) {
|
| + if (contents.codeUnitAt(i) == $lt) {
|
| + code.replace(i, i + 1, '<');
|
| + } else if (contents.codeUnitAt(i) == $gt) {
|
| + code.replace(i, i + 1, '>');
|
| + } else if (contents.codeUnitAt(i) == $lf) {
|
| + code.insert(i + 1, '</span><span class="line">');
|
| + }
|
| + }
|
| + code.insert(contents.length, '</span>');
|
| + }
|
| +
|
| + @override
|
| + visitCompilationUnit(CompilationUnitResult unit) {
|
| + // TODO(sigmund): change results to store a map from Uri to file results.
|
| + if (path != unit.uri.path) return;
|
| + super.visitCompilationUnit(unit);
|
| + }
|
| +
|
| + @override
|
| + visitFunction(FunctionResult function) {
|
| + var entries = function.measurements.entries;
|
| + for (var metric in entries.keys) {
|
| + if (metric is GroupedMetric) continue;
|
| + var cssClassName = _classNameForMetric(metric);
|
| + for (var entry in entries[metric]) {
|
| + code.insert(entry.begin,
|
| + '<span class="send ${cssClassName} inactive">', -entry.end);
|
| + code.insert(entry.end, '</span>');
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +_classNameForMetric(Metric metric) => metric.name.replaceAll(' ', '-');
|
| +
|
| +/// A buffer meant to apply edits on a string (rather than building a string
|
| +/// from scratch). Each change is described using the location information on
|
| +/// the original string. Internally this buffer keeps track of how a
|
| +/// modification in one portion can offset a modification further down the
|
| +/// string.
|
| +class StringEditBuffer {
|
| + final String original;
|
| + final _edits = <_StringEdit>[];
|
| +
|
| + StringEditBuffer(this.original);
|
| +
|
| + bool get hasEdits => _edits.length > 0;
|
| +
|
| + /// Edit the original text, replacing text on the range [begin] and
|
| + /// exclusive [end] with the [replacement] string.
|
| + void replace(int begin, int end, String replacement, [int sortId]) {
|
| + _edits.add(new _StringEdit(begin, end, replacement, sortId));
|
| + }
|
| +
|
| + /// Insert [string] at [offset].
|
| + /// Equivalent to `replace(offset, offset, string)`.
|
| + void insert(int offset, String string, [sortId]) =>
|
| + replace(offset, offset, string, sortId);
|
| +
|
| + /// Remove text from the range [begin] to exclusive [end].
|
| + /// Equivalent to `replace(begin, end, '')`.
|
| + void remove(int begin, int end) => replace(begin, end, '');
|
| +
|
| + /// Applies all pending [edit]s and returns a new string.
|
| + ///
|
| + /// This method is non-destructive: it does not discard existing edits or
|
| + /// change the [original] string. Further edits can be added and this method
|
| + /// can be called again.
|
| + ///
|
| + /// Throws [UnsupportedError] if the edits were overlapping. If no edits were
|
| + /// made, the original string will be returned.
|
| + String toString() {
|
| + var sb = new StringBuffer();
|
| + if (_edits.length == 0) return original;
|
| +
|
| + // Sort edits by start location.
|
| + _edits.sort();
|
| +
|
| + int consumed = 0;
|
| + for (var edit in _edits) {
|
| + if (consumed > edit.begin) {
|
| + sb = new StringBuffer();
|
| + sb.write('overlapping edits. Insert at offset ');
|
| + sb.write(edit.begin);
|
| + sb.write(' but have consumed ');
|
| + sb.write(consumed);
|
| + sb.write(' input characters. List of edits:');
|
| + for (var e in _edits) {
|
| + sb.write('\n ');
|
| + sb.write(e);
|
| + }
|
| + throw new UnsupportedError(sb.toString());
|
| + }
|
| +
|
| + // Add characters from the original string between this edit and the last
|
| + // one, if any.
|
| + var betweenEdits = original.substring(consumed, edit.begin);
|
| + sb.write(betweenEdits);
|
| + sb.write(edit.string);
|
| + consumed = edit.end;
|
| + }
|
| +
|
| + // Add any text from the end of the original string that was not replaced.
|
| + sb.write(original.substring(consumed));
|
| + return sb.toString();
|
| + }
|
| +}
|
| +
|
| +/// A single edit in a [StringEditBuffer].
|
| +class _StringEdit implements Comparable<_StringEdit> {
|
| + // Offset where edit begins
|
| + final int begin;
|
| +
|
| + // Offset where edit ends
|
| + final int end;
|
| +
|
| + // Sort index as a tie-breaker for edits that have the same location.
|
| + final int sortId;
|
| +
|
| + // String to insert
|
| + final String string;
|
| +
|
| + _StringEdit(int begin, this.end, this.string, [int sortId])
|
| + : begin = begin, sortId = sortId == null ? begin : sortId;
|
| +
|
| + int get length => end - begin;
|
| +
|
| + String toString() => '(Edit @ $begin,$end: "$string")';
|
| +
|
| + int compareTo(_StringEdit other) {
|
| + int diff = begin - other.begin;
|
| + if (diff != 0) return diff;
|
| + diff = end - other.end;
|
| + if (diff != 0) return diff;
|
| + // use edit order as a tie breaker
|
| + return sortId - other.sortId;
|
| + }
|
| +}
|
|
|