Avoid Stack Overflows during VirtualTree expansion

R=rmacnak@google.com

Review-Url: https://codereview.chromium.org/2968813002 .
This commit is contained in:
Carlo Bernaschina 2017-07-05 17:46:41 -07:00
parent 788516a71d
commit 6be07a30bc
3 changed files with 181 additions and 24 deletions

View file

@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:html';
import 'dart:collection';
import 'dart:math' as Math;
import 'package:observatory/src/elements/containers/virtual_collection.dart';
import 'package:observatory/src/elements/helpers/rendering_scheduler.dart';
@ -89,8 +90,14 @@ class VirtualTreeElement extends HtmlElement implements Renderable {
bool autoExpandWholeTree: false}) {
if (_expanded.add(item)) _r.dirty();
if (autoExpandWholeTree) {
for (final child in _children(item)) {
expand(child, autoExpandWholeTree: true);
// The tree is potentially very deep, simple recursion can produce a
// Stack Overflow
Queue pendingNodes = new Queue();
pendingNodes.addAll(_children(item));
while (pendingNodes.isNotEmpty) {
final item = pendingNodes.removeFirst();
if (_expanded.add(item)) _r.dirty();
pendingNodes.addAll(_children(item));
}
} else if (autoExpandSingleChildNodes) {
var children = _children(item);
@ -106,8 +113,14 @@ class VirtualTreeElement extends HtmlElement implements Renderable {
bool autoCollapseWholeTree: false}) {
if (_expanded.remove(item)) _r.dirty();
if (autoCollapseWholeTree) {
for (final child in _children(item)) {
collapse(child, autoCollapseWholeTree: true);
// The tree is potentially very deep, simple recursion can produce a
// Stack Overflow
Queue pendingNodes = new Queue();
pendingNodes.addAll(_children(item));
while (pendingNodes.isNotEmpty) {
final item = pendingNodes.removeFirst();
if (_expanded.remove(item)) _r.dirty();
pendingNodes.addAll(_children(item));
}
} else if (autoCollapseSingleChildNodes) {
var children = _children(item);
@ -137,29 +150,32 @@ class VirtualTreeElement extends HtmlElement implements Renderable {
if (children.length == 0) {
children = [_collection];
}
Iterable _toList(item) {
if (isExpanded(item)) {
Iterable children = _children(item);
if (children.isNotEmpty) {
return [item]..addAll(children.expand(_toList));
final items = [];
final depths = new List.filled(_items.length, 0, growable: true);
{
final toDo = new Queue();
toDo.addAll(_items);
while (toDo.isNotEmpty) {
final item = toDo.removeFirst();
items.add(item);
if (isExpanded(item)) {
final children = _children(item);
children
.toList(growable: false)
.reversed
.forEach((c) => toDo.addFirst(c));
final depth = depths[items.length - 1];
depths.insertAll(
items.length, new List.filled(children.length, depth + 1));
}
}
return [item];
}
_collection.items = _items.expand(_toList);
var depth = 0;
Iterable _toDepth(item) {
if (isExpanded(item)) {
Iterable children = _children(item);
if (children.isNotEmpty) {
depth++;
return children.expand(_toDepth).toList()..insert(0, --depth);
}
}
return [depth];
}
_depths = _items.expand(_toDepth).toList();
_depths = depths;
_collection.items = items;
}
}

View file

@ -0,0 +1,112 @@
// Copyright (c) 2017, 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.
import 'dart:html';
import 'package:unittest/unittest.dart';
import 'package:observatory/src/elements/containers/virtual_collection.dart';
import 'package:observatory/src/elements/containers/virtual_tree.dart';
main() {
VirtualTreeElement.tag.ensureRegistration();
final cTag = VirtualCollectionElement.tag.name;
var container;
setUp(() {
container = document.body.getElementsByClassName('test_container').first;
});
group('instantiation', () {
test('default', () {
final e = new VirtualTreeElement((_) {}, (_1, _2, _3) {}, (_) {});
expect(e, isNotNull, reason: 'element correctly created');
expect(e.items, isNotNull, reason: 'items not null');
expect(e.items, isEmpty, reason: 'no items');
});
test('items: []', () {
final items = ["1", 2, {}];
final e =
new VirtualTreeElement((_) {}, (_1, _2, _3) {}, (_) {}, items: items);
expect(e, isNotNull, reason: 'element correctly created');
expect(e.items, isNot(same(items)), reason: 'avoid side effect');
expect(e.items, equals(items), reason: 'same items');
});
});
test('elements created after attachment', () async {
final create = (toggle) => new DivElement()..classes = ['test_item'];
final update = (HtmlElement el, item, depth) {
el.text = item.toString();
};
final children = (item) => [];
final items = ["1", 2, {}];
final e = new VirtualTreeElement(create, update, children);
container.append(e);
await e.onRendered.first;
expect(e.children.length, isNonZero, reason: 'has elements');
expect(e.querySelectorAll(cTag).length, same(1));
e.remove();
await e.onRendered.first;
expect(e.children.length, isZero, reason: 'is empty');
});
test('expand single child', () async {
const max_depth = 100000;
final create = (toggle) => new DivElement()..classes = ['test_item'];
final update = (HtmlElement el, item, depth) {
el.text = item.toString();
};
final children = (item) => item >= max_depth ? [] : [item + 1];
final items = [0];
final e = new VirtualTreeElement(create, update, children, items: items);
container.append(e);
await e.onRendered.first;
expect(e.children.length, isNonZero, reason: 'has elements');
final VirtualCollectionElement collection = e.querySelectorAll(cTag).first;
expect(collection.items.length, equals(1), reason: 'begin');
e.expand(0, autoExpandSingleChildNodes: true);
await e.onRendered.first;
expect(collection.items.length, equals(max_depth + 1), reason: 'expanded');
e.collapse(0, autoCollapseSingleChildNodes: true);
await e.onRendered.first;
expect(collection.items.length, equals(1), reason: 'collapsed');
e.remove();
await e.onRendered.first;
expect(e.children.length, isZero, reason: 'is empty');
});
test('expand whole tree', () async {
const max_depth = 100000;
final create = (toggle) => new DivElement()..classes = ['test_item'];
final update = (HtmlElement el, item, depth) {
el.text = item.toString();
};
// We want to generated a tree that doesn't collapse to a chain of items
// while avoiding to generate an exponential number of items
final children = (item) {
if (item < 2 * max_depth) {
if (item % 200 == 0) {
return [item + 1, item + 2];
} else if (item % 2 == 0) {
return [item + 2];
}
}
return [];
};
final items = [0];
final e = new VirtualTreeElement(create, update, children, items: items);
container.append(e);
await e.onRendered.first;
expect(e.children.length, isNonZero, reason: 'has elements');
final VirtualCollectionElement collection = e.querySelectorAll(cTag).first;
expect(collection.items.length, equals(1), reason: 'begin');
e.expand(0, autoExpandWholeTree: true);
await e.onRendered.first;
expect(collection.items.length, equals(max_depth + max_depth / 100 + 1),
reason: 'expanded');
e.collapse(0, autoCollapseWholeTree: true);
await e.onRendered.first;
expect(collection.items.length, equals(1), reason: 'collapsed');
e.remove();
await e.onRendered.first;
expect(e.children.length, isZero, reason: 'is empty');
});
}

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="dart.unittest" content="full-stack-traces">
<style>
.unittest-table { font-family:monospace; border:1px; }
.unittest-pass { background: #6b3;}
.unittest-fail { background: #d55;}
.unittest-error { background: #a11;}
.test_container {
height: 100px;
}
.test_item {
height: 20px;
}
</style>
<script src="/packages/web_components/webcomponents-lite.min.js"></script>
</head>
<body>
<div class="test_container">
</div>
<script type="text/javascript"
src="/root_dart/tools/testing/dart/test_controller.js"></script>
%TEST_SCRIPTS%
</body>
</html>