diff --git a/dart/a2ui_core/CHANGELOG.md b/dart/a2ui_core/CHANGELOG.md index 4f851a8a00..2905584f10 100644 --- a/dart/a2ui_core/CHANGELOG.md +++ b/dart/a2ui_core/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Extracted `ComponentGraphBound` for incremental `SurfaceComponentsModel` + component graph checks and optimized its initial height traversal to linear + time. - Exported `checkPathsAndRecursion`, the check of data-model paths and nesting depth that `MessageProcessor` runs on each message under a validation config. Its new `grownFrom` argument takes an earlier form of diff --git a/dart/a2ui_core/lib/src/core/component_graph_bound.dart b/dart/a2ui_core/lib/src/core/component_graph_bound.dart new file mode 100644 index 0000000000..447af87798 --- /dev/null +++ b/dart/a2ui_core/lib/src/core/component_graph_bound.dart @@ -0,0 +1,174 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +import 'package:meta/meta.dart'; + +import '../validation/component_graph.dart'; +import '../validation/component_refs.dart'; + +/// What a check found about a surface's component graph, which had no +/// self-reference and no cycle: the references each component holds, and an +/// upper bound on the longest chain of references below each. +/// +/// The bound is what lets a batch be checked without walking the whole +/// graph again. A cycle the batch makes passes through one of its +/// components, and only the chains through its components grow, so only +/// those components and the ones above them are revisited. +@internal +class ComponentGraphBound { + ComponentGraphBound._(); + + /// The bound of a graph [checkComponentGraph] passed, holding each id of + /// [graph] with its type and the map its references are read from through + /// [refFields]. + factory ComponentGraphBound.of( + Map)> graph, + Map refFields, + ) { + final bound = ComponentGraphBound._(); + for (final MapEntry(key: String id, value: (String type, source)) + in graph.entries) { + bound._setChildren(id, type, source, refFields); + } + // Children before parents, which ends since the graph has no cycle. A + // component waits on the stack until its children have their heights. + for (final String start in graph.keys) { + if (bound._heights.containsKey(start)) continue; + final stack = <({String id, int childIndex, int height})>[ + (id: start, childIndex: 0, height: 0), + ]; + while (stack.isNotEmpty) { + final ({String id, int childIndex, int height}) frame = stack.last; + final List children = + bound._children[frame.id] ?? const []; + int childIndex = frame.childIndex; + int height = frame.height; + var ready = true; + while (childIndex < children.length) { + final String child = children[childIndex]; + childIndex++; + if (!graph.containsKey(child)) continue; + final int? below = bound._heights[child]; + if (below == null) { + stack[stack.length - 1] = ( + id: frame.id, + childIndex: childIndex - 1, + height: height, + ); + stack.add((id: child, childIndex: 0, height: 0)); + ready = false; + break; + } + if (below >= height) height = below + 1; + } + if (!ready) continue; + bound._heights[frame.id] = height; + if (height > bound._maxHeight) bound._maxHeight = height; + stack.removeLast(); + } + } + return bound; + } + + /// The ids each component references, held or not, in order. + final Map> _children = {}; + + /// The components that reference each id, held or not. + final Map> _parents = {}; + + /// For each component, at least the number of references in the longest + /// chain below it. + final Map _heights = {}; + + /// At least the largest of [_heights]. + int _maxHeight = 0; + + /// The reference fields each type's references were read through. + final Map _fields = {}; + + void _setChildren( + String id, + String type, + Map source, + Map refFields, + ) { + for (final String child in _children[id] ?? const []) { + _parents[child]?.remove(id); + } + final children = []; + for (final ComponentReference reference + in componentReferences(source, refFields[type])) { + final String child = reference.id; + children.add(child); + (_parents[child] ??= {}).add(id); + } + _children[id] = children; + _fields[type] = refFields[type]; + } + + /// Whether the graph with [batch] applied has no self-reference, no cycle + /// and no chain of more than [limit] references, which holds if this + /// bound shows it, updating the bound to that graph. + /// + /// [batch] holds each id with its type and the map its references are + /// read from through [refFields], and [held] tells whether the graph + /// holds an id before the batch. False means the bound cannot show it, + /// not that the graph fails, and leaves the bound unusable. + bool admits( + Map)> batch, + Map refFields, + int limit, + bool Function(String id) held, + ) { + if (_maxHeight > limit) return false; + for (final MapEntry(key: String type, value: ComponentRefFields? fields) + in _fields.entries) { + if (!identical(refFields[type], fields)) return false; + } + bool holds(String id) => batch.containsKey(id) || held(id); + + for (final MapEntry(key: String id, value: (String type, source)) + in batch.entries) { + _setChildren(id, type, source, refFields); + if ((_children[id] ?? const []).contains(id)) return false; + } + // Raises the bound of each component above one whose bound rose, until + // none rises. A cycle would raise its bounds past any limit. + final rising = ListQueue(); + for (final String id in batch.keys) { + var height = 0; + for (final String child in _children[id] ?? const []) { + final int below = _heights[child] ?? 0; + if (holds(child) && below >= height) height = below + 1; + } + _heights[id] = height; + rising.add(id); + } + while (rising.isNotEmpty) { + final String id = rising.removeFirst(); + final int height = _heights[id] ?? 0; + if (height > limit) return false; + if (height > _maxHeight) _maxHeight = height; + for (final String parent in _parents[id] ?? const {}) { + if ((_heights[parent] ?? 0) <= height) { + _heights[parent] = height + 1; + rising.add(parent); + } + } + } + return true; + } +} diff --git a/dart/a2ui_core/lib/src/core/component_model.dart b/dart/a2ui_core/lib/src/core/component_model.dart index 86da03a6e6..64a69f6735 100644 --- a/dart/a2ui_core/lib/src/core/component_model.dart +++ b/dart/a2ui_core/lib/src/core/component_model.dart @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:collection'; - import 'package:meta/meta.dart'; import '../primitives/errors.dart'; @@ -22,6 +20,7 @@ import '../validation/component_graph.dart'; import '../validation/component_refs.dart'; import '../validation/validation_config.dart'; import 'catalog.dart'; +import 'component_graph_bound.dart'; /// Represents the state model for an individual UI component. /// @@ -101,7 +100,7 @@ class SurfaceComponentsModel { /// What the last [checkComponentsUpdate] found about the graph, which /// holds for this model while [_changes] is [_boundAt]. - _GraphBound? _bound; + ComponentGraphBound? _bound; int _boundAt = -1; /// The batch [_bound] describes the graph with, once applied. @@ -324,7 +323,7 @@ class SurfaceComponentsModel { final bool bounded = config.allowDanglingReferences && (config.allowOrphanComponents || config.allowMissingRoot); - final _GraphBound? bound = _bound; + final ComponentGraphBound? bound = _bound; final bool current = bound != null && _boundAt == _changes; // The bound changes with the batch, and holds for the model again only // once the batch is applied. @@ -358,7 +357,7 @@ class SurfaceComponentsModel { return componentReferences(source, refFields[type]); }, config, defaultRootId: defaultRootId); if (bounded) { - _bound = _GraphBound.of(candidate, refFields); + _bound = ComponentGraphBound.of(candidate, refFields); _boundBatch = components; } } @@ -385,148 +384,3 @@ class SurfaceComponentsModel { _onDeleted.dispose(); } } - -/// What a check found about a surface's component graph, which had no -/// self-reference and no cycle: the references each component holds, and an -/// upper bound on the longest chain of references below each. -/// -/// The bound is what lets a batch be checked without walking the whole -/// graph again. A cycle the batch makes passes through one of its -/// components, and only the chains through its components grow, so only -/// those components and the ones above them are revisited. -class _GraphBound { - _GraphBound._(); - - /// The bound of a graph [checkComponentGraph] passed, holding each id of - /// [graph] with its type and the map its references are read from through - /// [refFields]. - factory _GraphBound.of( - Map)> graph, - Map refFields, - ) { - final bound = _GraphBound._(); - for (final MapEntry(key: String id, value: (String type, source)) - in graph.entries) { - bound._setChildren(id, type, source, refFields); - } - // Children before parents, which ends since the graph has no cycle. A - // component waits on the stack until its children have their heights. - for (final String start in graph.keys) { - final stack = [start]; - while (stack.isNotEmpty) { - final String id = stack.last; - if (bound._heights.containsKey(id)) { - stack.removeLast(); - continue; - } - var ready = true; - var height = 0; - for (final String child in bound._children[id]!) { - if (!graph.containsKey(child)) continue; - final int? below = bound._heights[child]; - if (below == null) { - ready = false; - stack.add(child); - } else if (below >= height) { - height = below + 1; - } - } - if (!ready) continue; - bound._heights[id] = height; - if (height > bound._maxHeight) bound._maxHeight = height; - stack.removeLast(); - } - } - return bound; - } - - /// The ids each component references, held or not, in order. - final Map> _children = {}; - - /// The components that reference each id, held or not. - final Map> _parents = {}; - - /// For each component, at least the number of references in the longest - /// chain below it. - final Map _heights = {}; - - /// At least the largest of [_heights]. - int _maxHeight = 0; - - /// The reference fields each type's references were read through. - final Map _fields = {}; - - void _setChildren( - String id, - String type, - Map source, - Map refFields, - ) { - for (final String child in _children[id] ?? const []) { - _parents[child]?.remove(id); - } - final List children = [ - for (final ComponentReference reference - in componentReferences(source, refFields[type])) - reference.id, - ]; - for (final child in children) { - (_parents[child] ??= {}).add(id); - } - _children[id] = children; - _fields[type] = refFields[type]; - } - - /// Whether the graph with [batch] applied has no self-reference, no cycle - /// and no chain of more than [limit] references, which holds if this - /// bound shows it, updating the bound to that graph. - /// - /// [batch] holds each id with its type and the map its references are - /// read from through [refFields], and [held] tells whether the graph - /// holds an id before the batch. False means the bound cannot show it, - /// not that the graph fails, and leaves the bound unusable. - bool admits( - Map)> batch, - Map refFields, - int limit, - bool Function(String id) held, - ) { - if (_maxHeight > limit) return false; - for (final MapEntry(key: String type, value: ComponentRefFields? fields) - in _fields.entries) { - if (!identical(refFields[type], fields)) return false; - } - bool holds(String id) => batch.containsKey(id) || held(id); - - for (final MapEntry(key: String id, value: (String type, source)) - in batch.entries) { - _setChildren(id, type, source, refFields); - if (_children[id]!.contains(id)) return false; - } - // Raises the bound of each component above one whose bound rose, until - // none rises. A cycle would raise its bounds past any limit. - final rising = ListQueue(); - for (final String id in batch.keys) { - var height = 0; - for (final String child in _children[id]!) { - final int below = _heights[child] ?? 0; - if (holds(child) && below >= height) height = below + 1; - } - _heights[id] = height; - rising.add(id); - } - while (rising.isNotEmpty) { - final String id = rising.removeFirst(); - final int height = _heights[id]!; - if (height > limit) return false; - if (height > _maxHeight) _maxHeight = height; - for (final String parent in _parents[id] ?? const {}) { - if ((_heights[parent] ?? 0) <= height) { - _heights[parent] = height + 1; - rising.add(parent); - } - } - } - return true; - } -}