mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 01:59:38 +00:00
Avoid Stack Overflows during VirtualTree expansion
R=rmacnak@google.com Review-Url: https://codereview.chromium.org/2968813002 .
This commit is contained in:
parent
788516a71d
commit
6be07a30bc
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
|
@ -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>
|
Loading…
Reference in a new issue