Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(632)

Unified Diff: runtime/observatory/lib/src/app/view_model.dart

Issue 1833453004: Dramatically increase the performance of Observatory's profile UI (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: runtime/observatory/lib/src/app/view_model.dart
diff --git a/runtime/observatory/lib/src/app/view_model.dart b/runtime/observatory/lib/src/app/view_model.dart
index 94edb496bac325876f8b946aac51a476a80b54c7..2f63db8a505e76d8efb84627389b5c84bd485dbd 100644
--- a/runtime/observatory/lib/src/app/view_model.dart
+++ b/runtime/observatory/lib/src/app/view_model.dart
@@ -4,6 +4,386 @@
part of app;
+abstract class VirtualTreeRow {
+ // Number of ems each subtree is indented.
+ static const subtreeIndent = 2;
+
+ static const redColor = '#F44336';
+ static const blueColor = '#3F51B5';
+ static const purpleColor = '#673AB7';
+ static const greenColor = '#4CAF50';
+ static const orangeColor = '#FF9800';
+ static const lightGrayColor = '#FAFAFA';
+
+ List backgroundColors = const [
+ purpleColor,
+ redColor,
+ greenColor,
+ blueColor,
+ orangeColor,
+ ];
+
+ final VirtualTree tree;
+ final List<VirtualTreeRow> children = [];
+ final List<StreamSubscription> _listeners = [];
+ final int depth;
+ bool _expanded = false;
+
+ VirtualTreeRow(this.tree, this.depth);
+
+ bool get expanded => _expanded;
+
+ set expanded(bool expanded) {
+ var changed = _expanded != expanded;
+ _expanded = expanded;
+ if (!changed) {
+ return;
+ }
+ if (_expanded) {
+ _expand();
+ } else {
+ _collapse();
+ }
+ }
+
+ Element makeColorBar() {
+ var element = new SpanElement();
+ element.style.paddingLeft = '2px';
+ element.style.paddingRight = '2px';
+ var flexBasis = '2px';
+ element.style.flexBasis = flexBasis;
rmacnak 2016/03/24 23:20:36 Odd for this one to have a variable when the rest
Cutch 2016/03/25 03:04:08 Done here and elsewhere.
+ element.style.height = '${tree.rowHeight}px';
+ element.style.minHeight = '${tree.rowHeight}px';
+ if (depth > 0) {
+ var colorIndex = (depth - 1) % backgroundColors.length;
+ element.style.backgroundColor = backgroundColors[colorIndex];
+ }
+ return element;
+ }
+
+ Element makeExpander() {
+ SpanElement element = new SpanElement();
+ var flexBasis = '2em';
+ element.style.flexBasis = flexBasis;
+ if (!hasChildren()) {
+ element.style.visibility = 'hidden';
+ } else {
+ element.style.visibility = 'visible';
+ element.children.add(expanded ?
+ new Element.tag('icon-expand-more') :
+ new Element.tag('icon-chevron-right'));
+ }
+ _listeners.add(element.onClick.listen((e) {
+ e.stopPropagation();
+ toggle();
+ }));
+ return element;
+ }
+
+ Element makeIndenter() {
+ SpanElement element = new SpanElement();
+ var flexBasis = '${subtreeIndent * depth}em';
+ element.style.flexBasis = flexBasis;
+ return element;
+ }
+
+ Element makeText(String text, {String toolTip, String flexBasis: '7em'}) {
+ SpanElement element = new SpanElement();
+ element.text = text;
+ if (toolTip != null) {
+ element.title = toolTip;
+ }
+ if (flexBasis != null) {
+ element.style.flexBasis = flexBasis;
+ }
+ return element;
+ }
+
+ Element makeGap([int ems = 1]) {
+ SpanElement element = new SpanElement();
+ var flexBasis = '${ems}em';
+ element.style.flexBasis = flexBasis;
+ return element;
+ }
+
+ void _cleanupListeners() {
+ for (var listener in _listeners) {
+ listener.cancel();
+ }
+ _listeners.clear();
+ }
+
+ void _expand() {
+ tree._onExpand(this);
+ }
+
+ void _collapse() {
+ if (children.length == 0) {
+ // Nothing to do.
+ return;
+ }
+ for (var i = 0; i < children.length; i++) {
+ if (children[i].expanded) {
+ children[i]._collapse();
+ }
+ }
+ _expanded = false;
+ tree._onCollapse(this);
+ }
+
+ void toggle() {
+ expanded = !expanded;
+ }
+
+ void _render(DivElement rowDiv) {
+ rowDiv.style.display = 'flex';
+ rowDiv.style.alignItems = 'center';
+ _cleanupListeners();
+ onShow();
+ onRender(rowDiv);
+ }
+
+ /// Called when you should render into [rowDiv].
+ void onRender(DivElement rowDiv);
+
+ // Called when this row is visible.
+ void onShow();
+
+ // Return true if this node can be expanded.
+ bool hasChildren() {
+ return false;
+ }
+
+ // Called when this row is not visible.
+ void onHide() {
+ _cleanupListeners();
+ }
+}
+
+class VirtualTree {
+ final int rowHeight;
+ final List<VirtualTreeRow> rows = [];
+ final DivElement root;
+ final Stopwatch _clock = new Stopwatch();
+
+ DivElement _treeHeightElement;
+ DivElement _tree;
+
+ StreamSubscription _scrollSubscription;
+ StreamSubscription _resizeSubscription;
+ Timer _sweeperTimer;
+
+ // Height of [root] in pixels.
+ int viewHeight;
+
+ // Number of pixels view can be scrolled before a redraw occurs.
+ int redrawThresholdPixels;
+
+ // Number of rows visible at any given time.
+ int numVisibleRows;
+ // Number of rows above the current view that are in the dom.
+ int extraRowsAbove;
+ // Number of rows below the current view that are in the dom.
+ int extraRowsBelow;
+
+ // The time of the last scroll event.
+ int lastScrollTimeMilliseconds;
+
+ // The scroll top of the last scroll event.
+ int lastPaintScrollTop;
+
+ // The starting row of the last paint.
+ int lastPaintStartingRow;
+
+ static const scrollStopThresholdMilliseconds = 100;
+
+ VirtualTree(this.rowHeight, this.root) {
+ _clock.start();
+ _install();
+ _resize();
+ _paint(0);
+ }
+
+ void uninstall() => _uninstall();
+
+ void refresh() {
+ _resize();
+ _paint(lastPaintStartingRow);
+ }
+
+ // Clear the tree.
+ void clear() {
+ rows.clear();
+ _resize();
+ }
+
+ void _onExpand(VirtualTreeRow parent) {
+ int index = rows.indexOf(parent);
+ if (index == -1) {
+ return;
+ }
+ rows.insertAll(index + 1, parent.children);
+ refresh();
+ }
+
+ void _onCollapse(VirtualTreeRow parent) {
+ int index = rows.indexOf(parent);
+ if (index == -1) {
+ return;
+ }
+ int start = index + 1;
+ int end = start + parent.children.length;
+ rows.removeRange(start, end);
+ refresh();
+ }
+
+ void _resize() {
+ viewHeight = root.offsetHeight;
+ numVisibleRows = (viewHeight ~/ rowHeight) + 1;
+ extraRowsAbove = numVisibleRows ~/ 2;
+ extraRowsBelow = numVisibleRows - extraRowsAbove;
+ redrawThresholdPixels =
+ math.min(extraRowsAbove, extraRowsBelow) * rowHeight;
+ _treeHeightElement.style.height = '${_treeHeight()}px';
+ }
+
+ int _treeHeight() {
+ return rows.length * rowHeight;
+ }
+
+ int _now() => _clock.elapsedMilliseconds;
+
+ int _millisecondsSinceLastScroll() {
+ int now = _now();
+ if (lastScrollTimeMilliseconds == null) {
+ return now;
+ }
+ return now - lastScrollTimeMilliseconds;
+ }
+
+ int _pixelsFromLastScroll(int currentScrollTop) {
+ if (lastPaintScrollTop == null) {
+ return currentScrollTop;
+ }
+
+ return (currentScrollTop - lastPaintScrollTop).abs();
+ }
+
+ int _pixelToRow(int pixelY) {
+ int result = pixelY ~/ rowHeight;
+ return result;
+ }
+
+ void _install() {
+ // This element controls the height of the tree's scrollable region.
+ // It is one pixel wide and the height is set to rowHeight * numRows.
+ _treeHeightElement = new DivElement();
+ _treeHeightElement.style.position = 'absolute';
+ _treeHeightElement.style.top = '0';
+ _treeHeightElement.style.left = '0';
+ _treeHeightElement.style.width = '1px';
+
+ // This element holds the visible tree rows and the height controlling
+ // element. It takes the full width and height of its parent element.
+ _tree = new DivElement();
+ _tree.children.add(_treeHeightElement);
+ _tree.style.width = '100%';
+ _tree.style.height = '100%';
+ _tree.style.position = 'relative';
+ _tree.style.overflow = 'auto';
+
+ // Listen for scroll events on the tree.
+ _scrollSubscription = _tree.onScroll.listen(_onScroll);
+
+ root.children.add(_tree);
+
+ // Listen for resize events.
+ _resizeSubscription = window.onResize.listen((_) {
+ _resize();
+ int row =
+ lastPaintStartingRow != null ? lastPaintStartingRow : 0;
+ _paint(row);
+ });
+
+ // Regularly sweep non-visible rows.
+ _sweeperTimer =
+ new Timer.periodic(const Duration(milliseconds: 300), _sweepRows);
+ }
+
+ void _uninstall() {
+ root.children.clear();
+ _scrollSubscription?.cancel();
+ _scrollSubscription = null;
+ _resizeSubscription?.cancel();
+ _resizeSubscription = null;
+ _sweeperTimer?.cancel();
+ _sweeperTimer = null;
+ }
+
+ void _onScroll(Event scrollEvent) {
+ Element target = scrollEvent.target;
+
+ int scrollTop = target.scrollTop;
+
+ if (_pixelsFromLastScroll(scrollTop) > redrawThresholdPixels) {
+ int startingRow = math.max(_pixelToRow(scrollTop), 0);
+ _paint(startingRow);
+ lastPaintScrollTop = scrollTop;
+ }
+ lastScrollTimeMilliseconds = _now();
+ scrollEvent.preventDefault();
+ }
+
+ void _sweepRows(Timer timer) {
+ // It hasn't been long enough since the last scroll.
+ if (_millisecondsSinceLastScroll() <= scrollStopThresholdMilliseconds) {
+ return;
+ }
+ // Find all rows marked with the garbage class.
+ var garbageRows = _tree.querySelectorAll('.garbage');
+ // Remove them from the tree.
+ for (var row in garbageRows) {
+ _tree.children.remove(row);
+ }
+ }
+
+ void _paint(int startingRow) {
+ lastPaintStartingRow = startingRow;
+
+ // Hide all existing rows and mark them for collection. The first child
+ // is the height control element, so we skip that.
+ for (int i = 1; i < _tree.children.length; i++) {
+ _tree.children[i].style.display = 'none';
+ _tree.children[i].classes.add('garbage');
+ }
+
+ int endingRow =
+ math.min(rows.length, startingRow + numVisibleRows + extraRowsBelow);
+
+ startingRow =
+ math.max(0, startingRow - extraRowsAbove);
+
+ print('PAINT $startingRow $endingRow');
+
+ // Create visible rows and insert them into a document fragment.
+ DocumentFragment fragment = new DocumentFragment();
+ for (int i = startingRow; i < endingRow; i++) {
+ DivElement row = new DivElement();
+ row.style.position = 'absolute';
+ row.style.height = '${rowHeight}px';
+ row.style.maxHeight = '${rowHeight}px';
+ row.style.margin = '0';
+ row.style.width = '100%';
+ row.style.top = '${(i * rowHeight)}px';
+ // Render the row.
+ rows[i]._render(row);
+ fragment.children.add(row);
+ }
+ // Append the fragment to the DOM.
+ _tree.append(fragment);
+ }
+}
+
abstract class TableTreeRow extends Observable {
static const arrowRight = '\u2192';
static const arrowDownRight = '\u21b3';

Powered by Google App Engine
This is Rietveld 408576698