blob: 9b56e64cfa7cea892c09a9a1ed5a38e001942b4e [file] [log] [blame]
// 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.
library protoc.benchmark.html_view;
import 'dart:async' show Stream, StreamController, EventSink;
import 'dart:html';
import 'generated/benchmark.pb.dart' as pb;
import 'dashboard_model.dart';
import 'report.dart' show encodeReport;
/// A dashboard allowing the user to run a benchmark suite and compare the
/// results to any saved report.
class DashboardView {
static const noBaseline = "<none>";
static final _template = new DivElement()
..innerHtml = '''
<div>
<button class="dv-run"></button>
<button class="dv-select-all"></button>
<button class="dv-select-none"></button>
<span class="dv-status"></span>
</div>
<pre class="dv-env"></pre>
Choose baseline: <select class="dv-menu"></select>
<table class="dv-table">
<tr>
<th></th>
<th>Benchmark</th>
<th colspan=5>Samples</th>
</tr>
<tr>
<th colspan=2></th>
<th>Baseline</th>
<th>Median</th>
<th>Max</th>
<th>Count</th>
<th>Units</th>
<tr>
</table>
<div class="dv-json"></div>
''';
final DivElement elt;
final _Button _runButton;
final _Button _selectAllButton;
final _Button _selectNoneButton;
final _Label _status;
final PreElement _envElt;
final _Menu _menu;
final TableElement _responseTable;
final _JsonView _jsonView;
String _renderedPlatform;
final rowViews = <_ResponseView>[];
final _selectionChanges =
new StreamController<SelectEvent<pb.Request>>.broadcast();
DashboardView._raw(
this.elt,
this._runButton,
this._selectAllButton,
this._selectNoneButton,
this._status,
this._envElt,
this._menu,
this._responseTable,
this._jsonView);
factory DashboardView() {
Element elt = _template.clone(true);
find(String q) => elt.querySelector(q);
_Button button(q) => new _Button(find(q));
label(q) => new _Label(find(q));
menu(q) => new _Menu(find(q));
json(q) => new _JsonView(find(q));
return new DashboardView._raw(
elt,
button('.dv-run')
..elt.style.color = "#FFFFFF"
..elt.style.backgroundColor = "rgb(209, 72, 64)",
button('.dv-select-all'),
button('.dv-select-none'),
label('.dv-status'),
find('.dv-env'),
menu('.dv-menu'),
find('.dv-table'),
json('.dv-json'));
}
Stream get onRunButtonClick => _runButton.onClick;
Stream get onSelectAllClick => _selectAllButton.onClick;
Stream get onSelectNoneClick => _selectNoneButton.onClick;
Stream<String> get onMenuChange =>
_menu.onChange.map((item) => item == noBaseline ? null : item);
Stream<SelectEvent<pb.Request>> get onSelectionChange =>
_selectionChanges.stream;
void render(DashboardModel model) {
_runButton.render("Run", model.canRun);
_selectAllButton.render("Select All", true);
_selectNoneButton.render("Select None", true);
if (!model.latest.hasStatus() || model.latest.status == pb.Status.DONE) {
_status.render("");
} else {
_status.render(model.latest.status.name);
}
_renderEnv(model.latest);
var items = [noBaseline]..addAll(model.savedReports.keys);
var selected = model.table.baseline;
if (selected == null) selected = noBaseline;
_menu.render(items, model.table.baseline);
_renderResponses(model.table, model.latest);
_jsonView.render(model.latest);
}
void _renderEnv(pb.Report r) {
String newPlatform = r.env.platform.toString();
if (newPlatform == _renderedPlatform) return;
_envElt.text = newPlatform;
_renderedPlatform = newPlatform;
}
/// Renders a table with one row for each benchmark.
void _renderResponses(Table table, pb.Report r) {
var rowIt = table.rows.iterator;
// Update existing rows (we assume the table never shrinks)
for (var view in rowViews) {
var hasNext = rowIt.moveNext();
assert(hasNext);
view.render(rowIt.current, r, _selectionChanges);
}
// Add any new rows
while (rowIt.moveNext()) {
var row = new _ResponseView()
..render(rowIt.current, r, _selectionChanges);
_responseTable.append(row.elt);
rowViews.add(row);
}
}
}
/// A single row in the benchmark table.
///
/// Displays how many samples were collected and the median and max samples.
/// Also displays a baseline sample for comparison.
class _ResponseView {
final elt = new TableRowElement();
final _selected = new _Checkbox<pb.Request>();
final _summary = new _Label(new TableCellElement());
final _baseline = new _SampleView();
final _median = new _SampleView();
final _max = new _SampleView();
final _count = new _Label(new TableCellElement()..style.textAlign = "right");
final _units = new _Label(new TableCellElement());
_ResponseView() {
elt.children.addAll([
_selected.elt,
_summary.elt,
_baseline.elt,
_median.elt,
_max.elt,
_count.elt,
_units.elt
]);
}
void render(
Row row, pb.Report r, EventSink<SelectEvent<pb.Request>> rowSelected) {
var b = row.benchmark;
var response = row.findResponse(r);
_selected.render(row.selected, item: row.request, sink: rowSelected);
_summary.render(b.summary);
_baseline.render(b.measureSample(row.baseline));
_median.render(b.measureSample(b.medianSample(response)));
_max.render(b.measureSample(b.maxSample(response)));
_count.render(response == null ? "0" : "${response.samples.length}");
_units.render(row.benchmark.measureSampleUnits);
}
}
/// A table cell holding the measurement for one sample.
class _SampleView {
final elt = new TableCellElement()..style.textAlign = "right";
double _rendered;
void render(double value) {
if (_rendered == value) return;
elt.text = _render(value);
_rendered = value;
}
static String _render(double value) {
if (value == 0.0) return "*";
return value.toStringAsFixed(0);
}
}
/// Renders the benchmark report as JSON so it can be copied to a file.
class _JsonView {
final DivElement elt;
String _rendered;
_JsonView(this.elt);
void render(pb.Report r) {
// Don't show JSON while benchmarks are in progress.
String json = "";
if (r.status == pb.Status.DONE) {
json = encodeReport(r);
}
if (json == _rendered) return;
elt.children.clear();
if (json == "") return;
elt.children.addAll([
new HeadingElement.h2()..text = "Report data as JSON:",
new PreElement()..text = json
]);
_rendered = json;
}
}
/// A menu of selectable text items.
class _Menu {
final SelectElement elt;
final _changes = new StreamController<String>.broadcast();
final _options = new List<_MenuOption>();
_Menu(this.elt) {
elt.onChange.listen((e) => _changes.add(elt.value));
}
Stream<String> get onChange => _changes.stream;
void render(List<String> items, String selected) {
var it = items.iterator;
// Update existing items
for (var opt in _options) {
var hasNext = it.moveNext();
assert(hasNext); // assume menu never shrinks
opt.render(it.current, it.current == selected);
}
// Add any new items
while (it.moveNext()) {
var opt = new _MenuOption();
opt.render(it.current, it.current == selected);
elt.append(opt.elt);
_options.add(opt);
}
}
}
class _MenuOption {
final elt = new OptionElement();
String _renderedItem;
bool _renderedSelected;
void render(String item, selected) {
if (_renderedItem != item) {
elt.text = item;
elt.value = item;
_renderedItem = item;
}
if (_renderedSelected != selected) {
elt.selected = selected;
_renderedSelected = selected;
}
}
}
class _Label {
final HtmlElement elt;
String _rendered;
_Label(this.elt);
void render(String text) {
if (_rendered == text) return;
elt.text = text;
_rendered = text;
}
}
class _Button {
final ButtonElement elt;
final _clicks = new StreamController.broadcast();
String _renderedLabel;
bool _renderedEnabled;
_Button(this.elt) {
elt.onClick.listen((e) => _clicks.add(true));
}
Stream get onClick => _clicks.stream;
void render(String label, bool enabled) {
if (label != _renderedLabel) {
elt.text = label;
_renderedLabel = label;
}
if (_renderedEnabled != enabled) {
elt.disabled = !enabled;
_renderedEnabled = enabled;
}
}
}
class _Checkbox<T> {
final elt = new CheckboxInputElement();
bool _renderedChecked;
EventSink<SelectEvent<T>> _sink;
T _item;
_Checkbox() {
elt.onChange.listen((e) {
if (_sink != null) {
_sink.add(new SelectEvent<T>(elt.checked, _item));
}
});
}
void render(bool checked, {EventSink<SelectEvent<T>> sink, T item}) {
if (_renderedChecked != checked) {
elt.checked = checked;
_renderedChecked = checked;
}
_item = item;
_sink = sink;
}
}