From ea9f9f2e09ecfaf6b7e45bd963cd80c91eff9710 Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 21 Mar 2026 02:19:18 -0700 Subject: [PATCH 1/8] perf(flex): skip speculative metrics for simple rows --- webf/lib/src/rendering/flex.dart | 378 +++++++++++++++++++++---------- 1 file changed, 259 insertions(+), 119 deletions(-) diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index eccf74326c..241c14de12 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -204,6 +204,20 @@ class _FlexFactorTotals { double flexShrink; } +class _FlexResolutionInputs { + const _FlexResolutionInputs({ + required this.contentBoxLogicalWidth, + required this.contentBoxLogicalHeight, + required this.maxMainSize, + required this.isMainSizeDefinite, + }); + + final double? contentBoxLogicalWidth; + final double? contentBoxLogicalHeight; + final double? maxMainSize; + final bool isMainSizeDefinite; +} + class _FlexContainerInvariants { const _FlexContainerInvariants({ required this.isHorizontalFlexDirection, @@ -1587,6 +1601,229 @@ class RenderFlexLayout extends RenderLayoutBox { return List.generate(items.length, (int i) => items[i].child, growable: false); } + _FlexResolutionInputs _computeFlexResolutionInputs() { + final bool isHorizontal = _isHorizontalFlexDirection; + final double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; + final double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; + + double? containerWidth; + if (contentBoxLogicalWidth != null) { + containerWidth = contentBoxLogicalWidth; + } else if (contentConstraints!.hasTightWidth) { + containerWidth = contentConstraints!.maxWidth; + } + + double? containerHeight; + if (containerWidth == null) { + if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { + containerWidth = contentConstraints!.maxWidth; + } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { + containerWidth = constraints.maxWidth; + } + } + if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { + containerWidth = + (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); + } + if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { + containerHeight = contentConstraints!.maxHeight; + } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { + containerHeight = constraints.maxHeight; + } + if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { + containerHeight = + (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); + } + if (contentBoxLogicalHeight != null) { + containerHeight = contentBoxLogicalHeight; + } else if (contentConstraints!.hasTightHeight) { + containerHeight = contentConstraints!.maxHeight; + } + + final double? maxMainSize = isHorizontal ? containerWidth : containerHeight; + final bool isMainSizeDefinite = isHorizontal + ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth || + ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || + (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) + : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight || + ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || + (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + + return _FlexResolutionInputs( + contentBoxLogicalWidth: contentBoxLogicalWidth, + contentBoxLogicalHeight: contentBoxLogicalHeight, + maxMainSize: maxMainSize, + isMainSizeDefinite: isMainSizeDefinite, + ); + } + + _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, + {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { + double mainAxisMargin = 0.0; + if (effectiveChild != null) { + final RenderStyle s = effectiveChild.renderStyle; + final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; + final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; + mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; + } + + final RenderBoxModel? marginBoxModel = + child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); + final RenderStyle? marginStyle = marginBoxModel?.renderStyle; + final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; + final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; + final bool marginTopAuto = marginStyle?.marginTop.isAuto ?? false; + final bool marginBottomAuto = marginStyle?.marginBottom.isAuto ?? false; + final bool hasAutoMainAxisMargin = _isHorizontalFlexDirection + ? (marginLeftAuto || marginRightAuto) + : (marginTopAuto || marginBottomAuto); + final bool hasAutoCrossAxisMargin = _isHorizontalFlexDirection + ? (marginTopAuto || marginBottomAuto) + : (marginLeftAuto || marginRightAuto); + + final double flexGrow = _getFlexGrow(child); + final double flexShrink = _getFlexShrink(child); + + return _RunChild( + child, + originalMainSize, + 0, + false, + effectiveChild: effectiveChild, + alignSelf: _getAlignSelf(child), + flexGrow: flexGrow, + flexShrink: flexShrink, + usedFlexBasis: usedFlexBasis, + mainAxisMargin: mainAxisMargin, + mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, + mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, + crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, + crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, + hasAutoMainAxisMargin: hasAutoMainAxisMargin, + hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, + marginLeftAuto: marginLeftAuto, + marginRightAuto: marginRightAuto, + marginTopAuto: marginTopAuto, + marginBottomAuto: marginBottomAuto, + isReplaced: effectiveChild?.renderStyle.isSelfRenderReplaced() ?? false, + aspectRatio: effectiveChild?.renderStyle.aspectRatio, + ); + } + + void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { + RenderBoxModel? box = child is RenderBoxModel + ? child + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + if (box == null) return; + + bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + + if (hasPercentageMaxWidth || hasPercentageMaxHeight) { + _childrenOldConstraints[box] = appliedConstraints; + } + } + + bool _shouldTryEarlyNoFlexNoStretchNoBaselineFastPath(RenderBox child, BoxConstraints childConstraints) { + if (child is RenderPositionPlaceholder) { + return false; + } + + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline || + renderStyle.alignItems == AlignItems.stretch) { + return false; + } + + final AlignSelf alignSelf = _getAlignSelf(child); + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline || + alignSelf == AlignSelf.stretch) { + return false; + } + + return childConstraints.hasTightWidth; + } + + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { + if (!_isHorizontalFlexDirection) return null; + if (renderStyle.flexWrap != FlexWrap.nowrap) return null; + + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + final double mainAxisGap = _getMainAxisGap(); + double runMainAxisExtent = 0.0; + double runCrossAxisExtent = 0.0; + double totalFlexGrow = 0.0; + double totalFlexShrink = 0.0; + final List<_RunChild> runChildren = <_RunChild>[]; + + for (final RenderBox child in children) { + final BoxConstraints childConstraints; + if (child is RenderBoxModel) { + childConstraints = child.getConstraints(); + } else if (child is RenderConstrainedBox) { + childConstraints = child.additionalConstraints; + } else { + childConstraints = constraints; + } + + if (!_shouldTryEarlyNoFlexNoStretchNoBaselineFastPath(child, childConstraints)) { + return null; + } + + child.layout(childConstraints, parentUsesSize: true); + _cacheOriginalConstraintsIfNeeded(child, childConstraints); + + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + childParentData?.runIndex = 0; + + final double childMainSize = _getMainSize(child); + _childrenIntrinsicMainSizes[child] = childMainSize; + + if (runChildren.isNotEmpty) { + runMainAxisExtent += mainAxisGap; + } + runMainAxisExtent += _getMainAxisExtent(child); + runCrossAxisExtent = math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); + + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final _RunChild runChild = _createRunChildMetadata( + child, + childMainSize, + effectiveChild: effectiveChild, + usedFlexBasis: effectiveChild != null ? _getUsedFlexBasis(child) : null, + ); + runChildren.add(runChild); + + if (runChild.flexGrow > 0) { + totalFlexGrow += runChild.flexGrow; + } + if (runChild.flexShrink > 0) { + totalFlexShrink += runChild.flexShrink; + } + } + + final List<_RunMetrics> runMetrics = <_RunMetrics>[ + _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, totalFlexShrink, 0, runChildren, 0) + ]; + + _flexLineBoxMetrics = runMetrics; + + if (!_tryNoFlexNoStretchNoBaselineFastPath( + runMetrics, + maxMainSize: inputs.maxMainSize, + isMainSizeDefinite: inputs.isMainSizeDefinite, + contentBoxLogicalWidth: inputs.contentBoxLogicalWidth, + contentBoxLogicalHeight: inputs.contentBoxLogicalHeight, + )) { + return null; + } + + return runMetrics; + } + @override void performLayout() { try { @@ -1742,6 +1979,15 @@ class RenderFlexLayout extends RenderLayoutBox { return; } + final List<_RunMetrics>? earlyFastPathMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + if (earlyFastPathMetrics != null) { + _setContainerSize(earlyFastPathMetrics); + _setChildrenOffset(earlyFastPathMetrics); + _setMaxScrollableSize(earlyFastPathMetrics); + calculateBaseline(); + return; + } + if (!kReleaseMode) { developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', arguments: {'renderObject': describeIdentity(this)}); @@ -2296,66 +2542,23 @@ class RenderFlexLayout extends RenderLayoutBox { } // Use clamped intrinsic main size as the hypothetical size for line metrics. - double originalMainSize = baseMainSize; - double mainAxisMargin = 0.0; - if (effectiveChild != null) { - final RenderStyle s = effectiveChild.renderStyle; - final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; - final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; - mainAxisMargin = isHorizontal ? marginHorizontal : marginVertical; - } - - final RenderBoxModel? marginBoxModel = - child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); - final RenderStyle? marginStyle = marginBoxModel?.renderStyle; - final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; - final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; - final bool marginTopAuto = marginStyle?.marginTop.isAuto ?? false; - final bool marginBottomAuto = marginStyle?.marginBottom.isAuto ?? false; - final bool hasAutoMainAxisMargin = isHorizontal - ? (marginLeftAuto || marginRightAuto) - : (marginTopAuto || marginBottomAuto); - final bool hasAutoCrossAxisMargin = isHorizontal - ? (marginTopAuto || marginBottomAuto) - : (marginLeftAuto || marginRightAuto); - - final double flexGrow = _getFlexGrow(child); - final double flexShrink = _getFlexShrink(child); - - runChildren.add(_RunChild( + final _RunChild runChild = _createRunChildMetadata( child, - originalMainSize, - 0, - false, + baseMainSize, effectiveChild: effectiveChild, - alignSelf: alignSelf, - flexGrow: flexGrow, - flexShrink: flexShrink, usedFlexBasis: usedFlexBasis, - mainAxisMargin: mainAxisMargin, - mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, - mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, - crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, - crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, - hasAutoMainAxisMargin: hasAutoMainAxisMargin, - hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, - marginLeftAuto: marginLeftAuto, - marginRightAuto: marginRightAuto, - marginTopAuto: marginTopAuto, - marginBottomAuto: marginBottomAuto, - isReplaced: effectiveChild?.renderStyle.isSelfRenderReplaced() ?? false, - aspectRatio: effectiveChild?.renderStyle.aspectRatio, - )); + ); + runChildren.add(runChild); childParentData!.runIndex = runMetrics.length; assert(child.parentData == childParentData); - if (flexGrow > 0) { - totalFlexGrow += flexGrow; + if (runChild.flexGrow > 0) { + totalFlexGrow += runChild.flexGrow; } - if (flexShrink > 0) { - totalFlexShrink += flexShrink; + if (runChild.flexShrink > 0) { + totalFlexShrink += runChild.flexShrink; } } @@ -2815,74 +3018,11 @@ class RenderFlexLayout extends RenderLayoutBox { final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; final bool hasStretchedChildren = canAttemptFastPath ? _hasStretchedChildrenInCrossAxis(runMetrics) : true; - double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; - double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; - - // Container's width specified by style or inherited from parent. - // Use null to indicate an indefinite size; do not default to 0, - // which would incorrectly create free space for flex resolution. - double? containerWidth; - if (contentBoxLogicalWidth != null) { - containerWidth = contentBoxLogicalWidth; - } else if (contentConstraints!.hasTightWidth) { - containerWidth = contentConstraints!.maxWidth; - } - - // Container's height specified by style or inherited from parent. - // Use null to indicate an indefinite size; do not default to 0, - // which would incorrectly create free space for flex resolution. - double? containerHeight; - - // If not tight or explicit, consider bounded max size as a definite available size - // for resolving flexible lengths. This allows flex-shrink to operate when the - // container has a finite max-height/width (e.g., max-height: 17px in column). - if (containerWidth == null) { - if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { - containerWidth = contentConstraints!.maxWidth; - } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = constraints.maxWidth; - } - } - // Prefer the actually imposed outer constraints when they are tighter than - // the content constraints (e.g., a flex item whose max inline-size is 172px - // but whose internal contentMaxConstraintsWidth is larger). This ensures - // the flex algorithm sees the correct available main size and will shrink - // items when content overflows. - if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = - (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); - } - if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { - containerHeight = contentConstraints!.maxHeight; - } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = constraints.maxHeight; - } - if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = - (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); - } - if (contentBoxLogicalHeight != null) { - containerHeight = contentBoxLogicalHeight; - } else if (contentConstraints!.hasTightHeight) { - containerHeight = contentConstraints!.maxHeight; - } - - double? maxMainSize = isHorizontal ? containerWidth : containerHeight; - - // Flexbox has several additional cases where a length can be considered definite. - // https://www.w3.org/TR/css-flexbox-1/#definite-sizes - // Treat the main size as definite if either: - // - The flex container has a specified content-box size in the main axis, or - // - The layout constraints on the main axis are tight (e.g., fixed by parent). - bool isMainSizeDefinite = isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth || - ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || - (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight || - ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || - (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + final double? contentBoxLogicalWidth = inputs.contentBoxLogicalWidth; + final double? contentBoxLogicalHeight = inputs.contentBoxLogicalHeight; + double? maxMainSize = inputs.maxMainSize; + final bool isMainSizeDefinite = inputs.isMainSizeDefinite; if (canAttemptFastPath && !hasStretchedChildren) { if (_tryNoFlexNoStretchNoBaselineFastPath( From d85670428b4dafed001ee61c177eee28ccf06a1c Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 21 Mar 2026 11:51:37 -0700 Subject: [PATCH 2/8] perf(flex): cache safe flow measurements --- webf/lib/foundation.dart | 1 + webf/lib/src/foundation/debug_flags.dart | 6 + .../src/foundation/perf_debug_identity.dart | 147 +++ webf/lib/src/rendering/box_model.dart | 30 +- webf/lib/src/rendering/event_listener.dart | 10 +- webf/lib/src/rendering/flex.dart | 1010 ++++++++++++----- webf/lib/src/rendering/text.dart | 5 + webf/lib/src/rendering/widget.dart | 29 +- .../src/rendering/widget_element_child.dart | 28 +- 9 files changed, 973 insertions(+), 293 deletions(-) create mode 100644 webf/lib/src/foundation/perf_debug_identity.dart diff --git a/webf/lib/foundation.dart b/webf/lib/foundation.dart index 7fd2b36e82..ca4d64d65b 100644 --- a/webf/lib/foundation.dart +++ b/webf/lib/foundation.dart @@ -27,6 +27,7 @@ export 'src/foundation/logger.dart'; export 'src/foundation/form_data/form_data.dart'; export 'src/foundation/loading_state_registry.dart'; export 'src/foundation/debug_flags.dart'; +export 'src/foundation/perf_debug_identity.dart'; export 'src/foundation/positioned_layout_logging.dart'; export 'src/foundation/flex_layout_logging.dart'; export 'src/foundation/widget_logging.dart'; diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index ffe8b814c5..7598b7bbb6 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -117,6 +117,12 @@ class DebugFlags { static int cssGridProfilingMinMs = 2; // Removed: Use FlexLog filters to enable flex logs. + static bool enableFlexFastPathProfiling = + const bool.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH', defaultValue: false); + static int flexFastPathProfilingSummaryEvery = + const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); + static int flexFastPathProfilingMaxDetailLogs = + const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); /// Debug flag to enable inline layout visualization. /// When true, paints debug information for line boxes, margins, padding, etc. diff --git a/webf/lib/src/foundation/perf_debug_identity.dart b/webf/lib/src/foundation/perf_debug_identity.dart new file mode 100644 index 0000000000..129530ae05 --- /dev/null +++ b/webf/lib/src/foundation/perf_debug_identity.dart @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2024-present The OpenWebF Company. All rights reserved. + * Licensed under GNU GPL with Enterprise exception. + */ + +String perfDescribeElementNode(dynamic element, {int maxClasses = 2}) { + if (element == null) return 'unknown'; + + final StringBuffer buffer = StringBuffer(_perfReadTagName(element)); + final String? id = _perfReadStringProperty(element, 'id'); + if (id != null && id.isNotEmpty) { + buffer + ..write('#') + ..write(_perfSanitizeToken(id, maxLength: 32)); + } + + for (final String className + in _perfReadClasses(element, maxCount: maxClasses)) { + buffer + ..write('.') + ..write(className); + } + + return buffer.toString(); +} + +String perfDescribeElementPath( + dynamic element, { + int maxDepth = 4, + int maxClassesPerSegment = 1, +}) { + if (element == null) return 'unknown'; + + final List segments = []; + dynamic cursor = element; + bool truncated = false; + + while (cursor != null) { + if (segments.length >= maxDepth) { + truncated = true; + break; + } + segments + .add(perfDescribeElementNode(cursor, maxClasses: maxClassesPerSegment)); + cursor = _perfReadParentElement(cursor); + } + + final String path = segments.reversed.join('>'); + return truncated ? '...>$path' : path; +} + +String perfFormatMilliseconds(int microseconds, {int fractionDigits = 1}) { + return (microseconds / 1000.0).toStringAsFixed(fractionDigits); +} + +List _perfReadClasses(dynamic element, {required int maxCount}) { + final List classes = []; + if (maxCount <= 0) return classes; + + try { + final dynamic raw = element.classList; + if (raw is Iterable) { + for (final dynamic value in raw) { + if (value is! String || value.isEmpty) continue; + final String sanitized = _perfSanitizeToken(value, maxLength: 24); + if (sanitized.isEmpty || classes.contains(sanitized)) continue; + classes.add(sanitized); + if (classes.length >= maxCount) return classes; + } + } + } catch (_) {} + + try { + final dynamic raw = element.className; + if (raw is String && raw.isNotEmpty) { + for (final String value in raw.split(RegExp(r'\s+'))) { + if (value.isEmpty) continue; + final String sanitized = _perfSanitizeToken(value, maxLength: 24); + if (sanitized.isEmpty || classes.contains(sanitized)) continue; + classes.add(sanitized); + if (classes.length >= maxCount) return classes; + } + } + } catch (_) {} + + return classes; +} + +dynamic _perfReadParentElement(dynamic element) { + try { + return element.parentElement; + } catch (_) { + return null; + } +} + +String _perfReadTagName(dynamic element) { + final String? tagName = _perfReadStringProperty(element, 'tagName'); + if (tagName != null && tagName.isNotEmpty) { + return _perfSanitizeToken(tagName.toLowerCase(), maxLength: 20); + } + return _perfSanitizeToken(element.runtimeType.toString().toLowerCase(), + maxLength: 20); +} + +String? _perfReadStringProperty(dynamic element, String propertyName) { + try { + final dynamic value; + switch (propertyName) { + case 'id': + value = element.id; + break; + case 'tagName': + value = element.tagName; + break; + default: + return null; + } + if (value is String) { + return value; + } + } catch (_) {} + return null; +} + +String _perfSanitizeToken(String value, {required int maxLength}) { + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < value.length; i++) { + final int codeUnit = value.codeUnitAt(i); + final bool isDigit = codeUnit >= 48 && codeUnit <= 57; + final bool isUpper = codeUnit >= 65 && codeUnit <= 90; + final bool isLower = codeUnit >= 97 && codeUnit <= 122; + final bool isSafePunctuation = + codeUnit == 45 || codeUnit == 95 || codeUnit == 58; + if (isDigit || isUpper || isLower || isSafePunctuation) { + buffer.writeCharCode(codeUnit); + } else { + buffer.write('_'); + } + if (buffer.length >= maxLength) break; + } + + if (buffer.isEmpty) { + return 'x'; + } + return buffer.toString(); +} diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 48c6373728..a1e6377485 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -411,6 +411,7 @@ abstract class RenderBoxModel extends RenderBox // A flag to detect the size of this renderBox had changed during this layout. bool isSelfSizeChanged = false; + bool _lastLaidOutAsRelayoutBoundary = false; /// Mark children needs layout when drop child as Flutter did /// @@ -423,6 +424,8 @@ abstract class RenderBoxModel extends RenderBox @override void layout(Constraints constraints, {bool parentUsesSize = false}) { + _lastLaidOutAsRelayoutBoundary = + !parentUsesSize || sizedByParent || constraints.isTight || parent == null; renderBoxInLayoutHashCodes.add(hashCode); renderBoxModelInLayoutStack.add(this); @@ -835,12 +838,22 @@ abstract class RenderBoxModel extends RenderBox // Box size equals to RenderBox.size to avoid flutter complain when read size property. Size? _boxSize; + RenderObject? _relayoutParentOnSizeChange; Size? get boxSize { - assert(_boxSize != null, 'box does not have laid out.'); return _boxSize; } + void setRelayoutParentOnSizeChange(RenderObject? parent) { + _relayoutParentOnSizeChange = parent; + } + + @protected + RenderObject? get relayoutParentOnSizeChange => _relayoutParentOnSizeChange; + + @protected + bool get lastLaidOutAsRelayoutBoundary => _lastLaidOutAsRelayoutBoundary; + @override set size(Size value) { _boxSize = value; @@ -853,6 +866,21 @@ abstract class RenderBoxModel extends RenderBox super.size = value; } + @override + void markNeedsLayout() { + final RenderObject? relayoutParent = _relayoutParentOnSizeChange; + super.markNeedsLayout(); + + // Some wrapper parents mirror child.boxSize while laying the child out + // with parentUsesSize: false to keep a local relayout boundary. + if (relayoutParent != null && + lastLaidOutAsRelayoutBoundary && + identical(parent, relayoutParent) && + relayoutParent.attached) { + relayoutParent.markNeedsLayout(); + } + } + Size getBoxSize(Size contentSize) { _contentSize = contentConstraints!.constrain(contentSize); Size paddingBoxSize = renderStyle.wrapPaddingSize(_contentSize!); diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 60ebe760a7..768606a0b8 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -58,7 +58,15 @@ class RenderEventListener extends RenderBoxModel @override void markNeedsLayout() { super.markNeedsLayout(); - parent?.markNeedsLayout(); + + // Most of the engine still expects RenderEventListener to bubble layout + // dirtiness to its parent. Boundary-sensitive callers opt out by + // registering an explicit relayout parent via RenderBoxModel. + if (relayoutParentOnSizeChange == null && + lastLaidOutAsRelayoutBoundary && + parent != null) { + parent!.markNeedsLayout(); + } } @override diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 241c14de12..f52340f47e 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -18,6 +18,162 @@ import 'package:webf/css.dart'; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; +enum _FlexFastPathRejectReason { + verticalDirection, + wrappedContainer, + positionedPlaceholderChild, + containerAlignItemsBaseline, + containerAlignItemsStretch, + childAlignSelfBaseline, + childAlignSelfStretch, + childNonTightWidth, + wouldGrow, + wouldShrink, +} + +String _flexFastPathRejectReasonLabel(_FlexFastPathRejectReason reason) { + switch (reason) { + case _FlexFastPathRejectReason.verticalDirection: + return 'verticalDirection'; + case _FlexFastPathRejectReason.wrappedContainer: + return 'wrappedContainer'; + case _FlexFastPathRejectReason.positionedPlaceholderChild: + return 'positionedPlaceholderChild'; + case _FlexFastPathRejectReason.containerAlignItemsBaseline: + return 'containerAlignItemsBaseline'; + case _FlexFastPathRejectReason.containerAlignItemsStretch: + return 'containerAlignItemsStretch'; + case _FlexFastPathRejectReason.childAlignSelfBaseline: + return 'childAlignSelfBaseline'; + case _FlexFastPathRejectReason.childAlignSelfStretch: + return 'childAlignSelfStretch'; + case _FlexFastPathRejectReason.childNonTightWidth: + return 'childNonTightWidth'; + case _FlexFastPathRejectReason.wouldGrow: + return 'wouldGrow'; + case _FlexFastPathRejectReason.wouldShrink: + return 'wouldShrink'; + } +} + +typedef _FlexFastPathRejectCallback = void Function( + _FlexFastPathRejectReason reason, { + Map? details, +}); + +class _FlexFastPathProfiler { + static int _attempts = 0; + static int _hits = 0; + static int _detailLogs = 0; + static final Map<_FlexFastPathRejectReason, int> _rejectCounts = + <_FlexFastPathRejectReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexFastPathProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexFastPathProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexFastPathProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static void recordHit(String path, {required int childCount}) { + if (!enabled) return; + _attempts++; + _hits++; + _maybeLogSummary(); + } + + static void recordReject( + String path, + _FlexFastPathRejectReason reason, { + String? childLabel, + int? childIndex, + BoxConstraints? childConstraints, + Map? details, + }) { + if (!enabled) return; + _attempts++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexFastPath][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexFastPathRejectReasonLabel(reason)); + if (childIndex != null) { + message + ..write(' childIndex=') + ..write(childIndex); + } + if (childLabel != null) { + message + ..write(' child=') + ..write(childLabel); + } + if (childConstraints != null) { + message + ..write(' constraints=') + ..write(childConstraints); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + + _maybeLogSummary(); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_attempts == 0 || _attempts % _summaryEvery != 0) return; + + final int rejects = _attempts - _hits; + final double hitRate = _attempts == 0 ? 0.0 : (_hits / _attempts) * 100.0; + final List> rejectEntries = + _rejectCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + final String rejectSummary = rejectEntries.isEmpty + ? 'none' + : rejectEntries + .map((MapEntry<_FlexFastPathRejectReason, int> entry) => + '${_flexFastPathRejectReasonLabel(entry.key)}=${entry.value}') + .join(', '); + + renderingLogger.info( + '[FlexFastPath][summary] attempts=$_attempts hits=$_hits ' + 'hitRate=${hitRate.toStringAsFixed(1)}% rejects=$rejects reasons=$rejectSummary', + ); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + +class _FlexIntrinsicMeasurementCacheEntry { + const _FlexIntrinsicMeasurementCacheEntry({ + required this.constraints, + required this.size, + required this.intrinsicMainSize, + }); + + final BoxConstraints constraints; + final Size size; + final double intrinsicMainSize; +} + // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { @@ -532,6 +688,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache original constraints of children on the first layout. Expando _childrenOldConstraints = Expando('childrenOldConstraints'); + Expando<_FlexIntrinsicMeasurementCacheEntry> _childrenIntrinsicMeasureCache = + Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + Expando? _transientChildSizeOverrides; _FlexContainerInvariants? _layoutInvariants; @@ -543,6 +702,9 @@ class RenderFlexLayout extends RenderLayoutBox { _flexLineBoxMetrics.clear(); _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); _childrenOldConstraints = Expando('childrenOldConstraints'); + _childrenIntrinsicMeasureCache = + Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + _transientChildSizeOverrides = null; } @override @@ -1726,32 +1888,232 @@ class RenderFlexLayout extends RenderLayoutBox { } } - bool _shouldTryEarlyNoFlexNoStretchNoBaselineFastPath(RenderBox child, BoxConstraints childConstraints) { - if (child is RenderPositionPlaceholder) { - return false; + RenderFlowLayout? _getCacheableIntrinsicMeasureFlowChild(RenderBox child) { + if (child is RenderFlowLayout) { + if (child.renderStyle.isSelfAnonymousFlowLayout()) { + return null; + } + return child; } + return null; + } + bool _hasBaselineAlignmentForChild(RenderBox child) { if (renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline || - renderStyle.alignItems == AlignItems.stretch) { + renderStyle.alignItems == AlignItems.lastBaseline) { + return true; + } + final AlignSelf alignSelf = _getAlignSelf(child); + return alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline; + } + + bool _subtreeHasPendingIntrinsicMeasureInvalidation(RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + return true; + } + if (root is RenderBoxModel && root.needsRelayout) { + return true; + } + + if (root is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return true; + } + child = (root as dynamic).childAfter(child) as RenderBox?; + } return false; } + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + return _subtreeHasPendingIntrinsicMeasureInvalidation(child); + } + } + + return false; + } + + _FlexIntrinsicMeasurementCacheEntry? _getReusableIntrinsicMeasurement( + RenderBox child, + BoxConstraints childConstraints, + ) { + if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + return null; + } + if (_hasBaselineAlignmentForChild(child)) { + return null; + } + + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild(child); + if (flowChild == null || flowChild.needsRelayout) { + return null; + } + if (child is RenderBoxModel && child.needsRelayout) { + return null; + } + if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return null; + } + + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + _childrenIntrinsicMeasureCache[child]; + if (cacheEntry == null || cacheEntry.constraints != childConstraints) { + return null; + } + return cacheEntry; + } + + void _storeIntrinsicMeasurementCache( + RenderBox child, + BoxConstraints childConstraints, + Size childSize, + double intrinsicMainSize, + ) { + if (_getCacheableIntrinsicMeasureFlowChild(child) == null) { + return; + } + _childrenIntrinsicMeasureCache[child] = _FlexIntrinsicMeasurementCacheEntry( + constraints: childConstraints, + size: Size.copy(childSize), + intrinsicMainSize: intrinsicMainSize, + ); + } + + bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { + // Match Flutter's relayout-boundary model when the flex container only + // needs the wrapper's cached boxSize, not RenderBox.size dependency wiring. + // RenderEventListener falls back to legacy bubbling only when its parent + // did not establish a size dependency. + return child is RenderEventListener; + } + + void _layoutChildForFlex(RenderBox child, BoxConstraints childConstraints) { + if (child is RenderBoxModel) { + child.setRelayoutParentOnSizeChange( + _shouldAvoidParentUsesSizeForFlexChild(child) ? this : null, + ); + } + child.layout( + childConstraints, + parentUsesSize: !_shouldAvoidParentUsesSizeForFlexChild(child), + ); + } + + _FlexFastPathRejectReason? _getEarlyNoFlexNoStretchNoBaselineRejectReason( + RenderBox child, BoxConstraints childConstraints) { + if (child is RenderPositionPlaceholder) { + return _FlexFastPathRejectReason.positionedPlaceholderChild; + } + + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline) { + return _FlexFastPathRejectReason.containerAlignItemsBaseline; + } + if (renderStyle.alignItems == AlignItems.stretch) { + return _FlexFastPathRejectReason.containerAlignItemsStretch; + } + final AlignSelf alignSelf = _getAlignSelf(child); if (alignSelf == AlignSelf.baseline || - alignSelf == AlignSelf.lastBaseline || - alignSelf == AlignSelf.stretch) { - return false; + alignSelf == AlignSelf.lastBaseline) { + return _FlexFastPathRejectReason.childAlignSelfBaseline; + } + if (alignSelf == AlignSelf.stretch) { + return _FlexFastPathRejectReason.childAlignSelfStretch; } - return childConstraints.hasTightWidth; + return null; + } + + String _describeFastPathContainer() { + return perfDescribeElementPath( + renderStyle.target, + maxDepth: 5, + maxClassesPerSegment: 1, + ); + } + + String _describeFastPathChild(RenderBox child) { + RenderBoxModel? effectiveChild; + if (child is RenderBoxModel) { + effectiveChild = child; + } else if (child is RenderEventListener) { + effectiveChild = child.child as RenderBoxModel?; + } else if (child is RenderPositionPlaceholder) { + effectiveChild = child.positioned; + } + + if (effectiveChild != null) { + return perfDescribeElementNode( + effectiveChild.renderStyle.target, + maxClasses: 1, + ); + } + + return child.runtimeType.toString(); + } + + void _recordEarlyFastPathReject( + _FlexFastPathRejectReason reason, { + RenderBox? child, + int? childIndex, + BoxConstraints? childConstraints, + Map? details, + }) { + if (!_FlexFastPathProfiler.enabled) return; + _FlexFastPathProfiler.recordReject( + _describeFastPathContainer(), + reason, + childLabel: child == null ? null : _describeFastPathChild(child), + childIndex: childIndex, + childConstraints: childConstraints, + details: details, + ); + } + + void _recordEarlyFastPathHit(int childCount) { + if (!_FlexFastPathProfiler.enabled) return; + _FlexFastPathProfiler.recordHit( + _describeFastPathContainer(), + childCount: childCount, + ); + } + + bool _canAttemptFullEarlyFastPath(List<_RunMetrics> runMetrics) { + for (final _RunMetrics metrics in runMetrics) { + for (final _RunChild runChild in metrics.runChildren) { + if (!runChild.child.constraints.hasTightWidth) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.childNonTightWidth, + child: runChild.child, + childConstraints: runChild.child.constraints, + ); + return false; + } + } + } + return true; } List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { - if (!_isHorizontalFlexDirection) return null; - if (renderStyle.flexWrap != FlexWrap.nowrap) return null; + if (!_isHorizontalFlexDirection) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.verticalDirection, + details: {'flexDirection': renderStyle.flexDirection}, + ); + return null; + } + if (renderStyle.flexWrap != FlexWrap.nowrap) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.wrappedContainer, + details: {'flexWrap': renderStyle.flexWrap}, + ); + return null; + } - final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); final double mainAxisGap = _getMainAxisGap(); double runMainAxisExtent = 0.0; double runCrossAxisExtent = 0.0; @@ -1759,7 +2121,8 @@ class RenderFlexLayout extends RenderLayoutBox { double totalFlexShrink = 0.0; final List<_RunChild> runChildren = <_RunChild>[]; - for (final RenderBox child in children) { + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; final BoxConstraints childConstraints; if (child is RenderBoxModel) { childConstraints = child.getConstraints(); @@ -1769,11 +2132,20 @@ class RenderFlexLayout extends RenderLayoutBox { childConstraints = constraints; } - if (!_shouldTryEarlyNoFlexNoStretchNoBaselineFastPath(child, childConstraints)) { + final _FlexFastPathRejectReason? rejectReason = + _getEarlyNoFlexNoStretchNoBaselineRejectReason( + child, childConstraints); + if (rejectReason != null) { + _recordEarlyFastPathReject( + rejectReason, + child: child, + childIndex: childIndex, + childConstraints: childConstraints, + ); return null; } - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); _cacheOriginalConstraintsIfNeeded(child, childConstraints); final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; @@ -1810,17 +2182,6 @@ class RenderFlexLayout extends RenderLayoutBox { ]; _flexLineBoxMetrics = runMetrics; - - if (!_tryNoFlexNoStretchNoBaselineFastPath( - runMetrics, - maxMainSize: inputs.maxMainSize, - isMainSizeDefinite: inputs.isMainSizeDefinite, - contentBoxLogicalWidth: inputs.contentBoxLogicalWidth, - contentBoxLogicalHeight: inputs.contentBoxLogicalHeight, - )) { - return null; - } - return runMetrics; } @@ -1979,25 +2340,46 @@ class RenderFlexLayout extends RenderLayoutBox { return; } - final List<_RunMetrics>? earlyFastPathMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); - if (earlyFastPathMetrics != null) { - _setContainerSize(earlyFastPathMetrics); - _setChildrenOffset(earlyFastPathMetrics); - _setMaxScrollableSize(earlyFastPathMetrics); - calculateBaseline(); - return; - } - - if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', - arguments: {'renderObject': describeIdentity(this)}); + List<_RunMetrics>? runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + if (runMetrics != null) { + final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + if (_tryNoFlexNoStretchNoBaselineFastPath( + runMetrics, + maxMainSize: inputs.maxMainSize, + isMainSizeDefinite: inputs.isMainSizeDefinite, + contentBoxLogicalWidth: inputs.contentBoxLogicalWidth, + contentBoxLogicalHeight: inputs.contentBoxLogicalHeight, + onReject: ( + _FlexFastPathRejectReason reason, { + Map? details, + }) { + _recordEarlyFastPathReject(reason, details: details); + }, + )) { + _recordEarlyFastPathHit(children.length); + _setContainerSize(runMetrics); + _setChildrenOffset(runMetrics); + _setMaxScrollableSize(runMetrics); + calculateBaseline(); + return; + } + } + runMetrics = null; } - // Layout children to compute metrics of flex lines. - List<_RunMetrics> runMetrics = _computeRunMetrics(children); + if (runMetrics == null) { + // Layout children to compute metrics of flex lines. + if (!kReleaseMode) { + developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', + arguments: {'renderObject': describeIdentity(this)}); + } - if (!kReleaseMode) { - developer.Timeline.finishSync(); + runMetrics = _computeRunMetrics(children); + if (!kReleaseMode) { + developer.Timeline.finishSync(); + } } // Set flex container size. @@ -2315,251 +2697,269 @@ class RenderFlexLayout extends RenderLayoutBox { List<_RunChild> runChildren = <_RunChild>[]; // PASS 1+2: Intrinsic layout + compute run metrics in one pass. - for (RenderBox child in children) { - final BoxConstraints childConstraints = _getIntrinsicConstraints(child); - child.layout(childConstraints, parentUsesSize: true); - - if (child is RenderBoxModel) { - child.clearOverrideContentSize(); - } + _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); + try { + for (RenderBox child in children) { + final BoxConstraints childConstraints = _getIntrinsicConstraints(child); + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + _getReusableIntrinsicMeasurement(child, childConstraints); + + final Size childSize; + double intrinsicMain; + if (cacheEntry != null) { + childSize = cacheEntry.size; + intrinsicMain = cacheEntry.intrinsicMainSize; + _transientChildSizeOverrides![child] = childSize; + } else { + _layoutChildForFlex(child, childConstraints); - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + if (child is RenderBoxModel) { + child.clearOverrideContentSize(); + } - // Use intrinsic size for run calculations - final Size childSize = child.size; - double intrinsicMain = isHorizontal ? childSize.width : childSize.height; - - // CSS Flexbox §9.2: For flex-basis:auto with an auto main-size, the flex base size - // should come from the item's max-content contribution in the main axis, not from - // the block formatting context's "fill-available" used size. Our intrinsic pass can - // mistakenly inherit a container-bounded width for block-level items that establish - // an inline formatting context (IFC), causing the base size to equal the container - // width. Detect that case and prefer the IFC's max-intrinsic width instead. - if (isHorizontal && child is RenderFlowLayout) { - final RenderFlowLayout flowChild = child; - final CSSRenderStyle cs = flowChild.renderStyle; - final bool autoMain = cs.width.isAuto; - final bool hasDefiniteBasis = _getFlexBasis(flowChild) != null; - if (autoMain && !hasDefiniteBasis) { - double? candidate; - if (flowChild.inlineFormattingContext != null) { - // Paragraph max-intrinsic width approximates the max-content contribution. - final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; - // Convert content-width to border-box width by adding horizontal padding + borders. - final double paddingBorderH = - cs.paddingLeft.computedValue + - cs.paddingRight.computedValue + - cs.effectiveBorderLeftWidth.computedValue + - cs.effectiveBorderRightWidth.computedValue; - candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; - } else { - // Fallback: use max intrinsic width (already includes padding/border). - final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); - if (maxIntrinsic.isFinite) { - candidate = maxIntrinsic; + childSize = Size.copy(_getChildSize(child)!); + _transientChildSizeOverrides![child] = childSize; + intrinsicMain = isHorizontal ? childSize.width : childSize.height; + + // CSS Flexbox §9.2: For flex-basis:auto with an auto main-size, the flex base size + // should come from the item's max-content contribution in the main axis, not from + // the block formatting context's "fill-available" used size. Our intrinsic pass can + // mistakenly inherit a container-bounded width for block-level items that establish + // an inline formatting context (IFC), causing the base size to equal the container + // width. Detect that case and prefer the IFC's max-intrinsic width instead. + if (isHorizontal && child is RenderFlowLayout) { + final RenderFlowLayout flowChild = child; + final CSSRenderStyle cs = flowChild.renderStyle; + final bool autoMain = cs.width.isAuto; + final bool hasDefiniteBasis = _getFlexBasis(flowChild) != null; + if (autoMain && !hasDefiniteBasis) { + double? candidate; + if (flowChild.inlineFormattingContext != null) { + // Paragraph max-intrinsic width approximates the max-content contribution. + final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + // Convert content-width to border-box width by adding horizontal padding + borders. + final double paddingBorderH = + cs.paddingLeft.computedValue + + cs.paddingRight.computedValue + + cs.effectiveBorderLeftWidth.computedValue + + cs.effectiveBorderRightWidth.computedValue; + candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; + } else { + // Fallback: use max intrinsic width (already includes padding/border). + final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); + if (maxIntrinsic.isFinite) { + candidate = maxIntrinsic; + } + } + // If the currently measured intrinsic width is larger (e.g., filled to container), + // prefer the content-based candidate to avoid unintended expansion. + if (candidate != null && candidate > 0 && candidate < intrinsicMain) { + intrinsicMain = candidate; + } } } - // If the currently measured intrinsic width is larger (e.g., filled to container), - // prefer the content-based candidate to avoid unintended expansion. - if (candidate != null && candidate > 0 && candidate < intrinsicMain) { - intrinsicMain = candidate; - } - } - } - // Clamp intrinsic main size by child's min/max constraints before flexing, - // so percentage max-width/height act as caps on the base size per spec. - if (child is RenderBoxModel) { - final CSSRenderStyle cs = child.renderStyle; - // Determine min/max along the main axis - double? minMain; - double? maxMain; - if (isHorizontal) { - if (cs.minWidth.isNotAuto) minMain = cs.minWidth.computedValue; - if (!cs.maxWidth.isNone) maxMain = cs.maxWidth.computedValue; - } else { - if (cs.minHeight.isNotAuto) minMain = cs.minHeight.computedValue; - if (!cs.maxHeight.isNone) maxMain = cs.maxHeight.computedValue; - } - - // intrinsicMain is the border-box main size. In WebF (border-box model), - // min-width/max-width are already specified for the border box. Do not - // add padding/border again when clamping. - if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { - intrinsicMain = maxMain; - } - if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { - intrinsicMain = minMain; - } - } + // Clamp intrinsic main size by child's min/max constraints before flexing, + // so percentage max-width/height act as caps on the base size per spec. + if (child is RenderBoxModel) { + final CSSRenderStyle cs = child.renderStyle; + // Determine min/max along the main axis + double? minMain; + double? maxMain; + if (isHorizontal) { + if (cs.minWidth.isNotAuto) minMain = cs.minWidth.computedValue; + if (!cs.maxWidth.isNone) maxMain = cs.maxWidth.computedValue; + } else { + if (cs.minHeight.isNotAuto) minMain = cs.minHeight.computedValue; + if (!cs.maxHeight.isNone) maxMain = cs.maxHeight.computedValue; + } - // If a flex item has percentage max-size and is truly empty, its base size should be - // its padding+border box (do not expand to the percentage constraint). - if (child is RenderBoxModel) { - bool hasPctMaxMain = isHorizontal - ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE - : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height - .isAuto; - if (hasPctMaxMain && hasAutoMain) { - double paddingBorderMain = isHorizontal - ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue + - child.renderStyle.paddingLeft.computedValue + - child.renderStyle.paddingRight.computedValue) - : (child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue + - child.renderStyle.paddingTop.computedValue + - child.renderStyle.paddingBottom.computedValue); + // intrinsicMain is the border-box main size. In WebF (border-box model), + // min-width/max-width are already specified for the border box. Do not + // add padding/border again when clamping. + if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { + intrinsicMain = maxMain; + } + if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { + intrinsicMain = minMain; + } + } - // Check if this is an empty element (no content) using DOM-based detection - bool isEmptyElement = false; - Element domElement = child.renderStyle.target; - isEmptyElement = !domElement.hasChildren(); + // If a flex item has percentage max-size and is truly empty, its base size should be + // its padding+border box (do not expand to the percentage constraint). + if (child is RenderBoxModel) { + bool hasPctMaxMain = isHorizontal + ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE + : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height + .isAuto; + if (hasPctMaxMain && hasAutoMain) { + double paddingBorderMain = isHorizontal + ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + + child.renderStyle.effectiveBorderRightWidth.computedValue + + child.renderStyle.paddingLeft.computedValue + + child.renderStyle.paddingRight.computedValue) + : (child.renderStyle.effectiveBorderTopWidth.computedValue + + child.renderStyle.effectiveBorderBottomWidth.computedValue + + child.renderStyle.paddingTop.computedValue + + child.renderStyle.paddingBottom.computedValue); + + // Check if this is an empty element (no content) using DOM-based detection + bool isEmptyElement = false; + Element domElement = child.renderStyle.target; + isEmptyElement = !domElement.hasChildren(); + + // For empty elements, force intrinsic size to padding+border + if (isEmptyElement) { + intrinsicMain = paddingBorderMain; + } + } + } - // For empty elements, force intrinsic size to padding+border - if (isEmptyElement) { - intrinsicMain = paddingBorderMain; + // Enforce automatic minimum main size (min-size:auto) so preserved sizes + // never fall below min-content contributions in the main axis. + if (child is RenderBoxModel) { + final double autoMinMain = _getMinMainAxisSize(child); + if (intrinsicMain < autoMinMain) { + intrinsicMain = autoMinMain; + } } - } - } - // Enforce automatic minimum main size (min-size:auto) so preserved sizes - // never fall below min-content contributions in the main axis. - if (child is RenderBoxModel) { - final double autoMinMain = _getMinMainAxisSize(child); - if (intrinsicMain < autoMinMain) { - intrinsicMain = autoMinMain; + _storeIntrinsicMeasurementCache(child, childConstraints, childSize, intrinsicMain); } - } - - _childrenIntrinsicMainSizes[child] = intrinsicMain; - - Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); - - double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); - double childCrossAxisExtent = _getCrossAxisExtent(child); - // Include gap spacing in flex line limit check - double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; - bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; - // calculate flex line - if (isWrap && - runChildren.isNotEmpty && - isExceedFlexLineLimit) { - runMetrics.add(_RunMetrics( - runMainAxisExtent, - runCrossAxisExtent, - totalFlexGrow, - totalFlexShrink, - maxSizeAboveBaseline, - runChildren, - 0)); - runChildren = <_RunChild>[]; - runMainAxisExtent = 0.0; - runCrossAxisExtent = 0.0; - maxSizeAboveBaseline = 0.0; - maxSizeBelowBaseline = 0.0; - - totalFlexGrow = 0; - totalFlexShrink = 0; - } - // Add gap spacing between items (not before the first item) - if (runChildren.isNotEmpty) { - runMainAxisExtent += mainAxisGap; - } - runMainAxisExtent += childMainAxisExtent; - runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); - // Vertical align is only valid for inline box. - // Baseline alignment in column direction behave the same as flex-start. - AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || - alignSelf == AlignSelf.lastBaseline || - renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline; - if (isHorizontal && isBaselineAlign) { - // Distance from top to baseline of child - double childAscent = _getChildAscent(child); - double childMarginTop = 0; - double childMarginBottom = 0; - if (child is RenderBoxModel) { - childMarginTop = child.renderStyle.marginTop.computedValue; - childMarginBottom = child.renderStyle.marginBottom.computedValue; - } - if (DebugFlags.debugLogFlexBaselineEnabled) { - final Size? ic = intrinsicChildSize; - renderingLogger.finer('[FlexBaseline] PASS2 child=' - '${child.runtimeType}#${child.hashCode} ' - 'intrinsicSize=${ic?.width.toStringAsFixed(2)}x${ic?.height.toStringAsFixed(2)} ' - 'ascent=${childAscent.toStringAsFixed(2)} ' - 'mT=${childMarginTop.toStringAsFixed(2)} mB=${childMarginBottom.toStringAsFixed(2)}'); + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + + _childrenIntrinsicMainSizes[child] = intrinsicMain; + + Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); + + double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); + double childCrossAxisExtent = _getCrossAxisExtent(child); + // Include gap spacing in flex line limit check + double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; + bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; + // calculate flex line + if (isWrap && + runChildren.isNotEmpty && + isExceedFlexLineLimit) { + runMetrics.add(_RunMetrics( + runMainAxisExtent, + runCrossAxisExtent, + totalFlexGrow, + totalFlexShrink, + maxSizeAboveBaseline, + runChildren, + 0)); + runChildren = <_RunChild>[]; + runMainAxisExtent = 0.0; + runCrossAxisExtent = 0.0; + maxSizeAboveBaseline = 0.0; + maxSizeBelowBaseline = 0.0; + + totalFlexGrow = 0; + totalFlexShrink = 0; } - maxSizeAboveBaseline = math.max( - childAscent, - maxSizeAboveBaseline, - ); - maxSizeBelowBaseline = math.max( - childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, - maxSizeBelowBaseline, - ); - runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; - if (DebugFlags.debugLogFlexBaselineEnabled) { - renderingLogger.finer('[FlexBaseline] RUN update: maxAbove=' - '${maxSizeAboveBaseline.toStringAsFixed(2)} ' - 'maxBelow=${maxSizeBelowBaseline.toStringAsFixed(2)} ' - 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); + // Add gap spacing between items (not before the first item) + if (runChildren.isNotEmpty) { + runMainAxisExtent += mainAxisGap; } - } else { + runMainAxisExtent += childMainAxisExtent; runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); - } - // Per CSS Flexbox §9.7, keep two sizes: - // - flex base size: from flex-basis if definite, otherwise the intrinsic - // content-based size BEFORE min/max clamping. - // - hypothetical main size: the base size clamped by min/max. - // We store the base size in runChild.originalMainSize so remaining free - // space and shrink/grow weighting use the correct base, and keep the - // clamped value in _childrenIntrinsicMainSizes for line metrics. - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; - final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; - - double baseMainSize; - if (usedFlexBasis != null) { - // Used basis is already border-box (>= padding+border) for non-zero bases. - // For flex-basis: 0% in the main axis, use a flex base size of 0 so equal-flex - // items share free space evenly regardless of padding/border, while the - // non-flex portion (padding/border) is accounted for separately in totalSpace. - final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; - if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { - baseMainSize = 0; + // Vertical align is only valid for inline box. + // Baseline alignment in column direction behave the same as flex-start. + AlignSelf alignSelf = _getAlignSelf(child); + bool isBaselineAlign = + alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline || + renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline; + if (isHorizontal && isBaselineAlign) { + // Distance from top to baseline of child + double childAscent = _getChildAscent(child); + double childMarginTop = 0; + double childMarginBottom = 0; + if (child is RenderBoxModel) { + childMarginTop = child.renderStyle.marginTop.computedValue; + childMarginBottom = child.renderStyle.marginBottom.computedValue; + } + if (DebugFlags.debugLogFlexBaselineEnabled) { + final Size? ic = intrinsicChildSize; + renderingLogger.finer('[FlexBaseline] PASS2 child=' + '${child.runtimeType}#${child.hashCode} ' + 'intrinsicSize=${ic?.width.toStringAsFixed(2)}x${ic?.height.toStringAsFixed(2)} ' + 'ascent=${childAscent.toStringAsFixed(2)} ' + 'mT=${childMarginTop.toStringAsFixed(2)} mB=${childMarginBottom.toStringAsFixed(2)}'); + } + maxSizeAboveBaseline = math.max( + childAscent, + maxSizeAboveBaseline, + ); + maxSizeBelowBaseline = math.max( + childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, + maxSizeBelowBaseline, + ); + runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; + if (DebugFlags.debugLogFlexBaselineEnabled) { + renderingLogger.finer('[FlexBaseline] RUN update: maxAbove=' + '${maxSizeAboveBaseline.toStringAsFixed(2)} ' + 'maxBelow=${maxSizeBelowBaseline.toStringAsFixed(2)} ' + 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); + } } else { - baseMainSize = usedFlexBasis; + runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); } - } else { - // childSize is the intrinsic measurement from PASS 1 (pre-clamp). - baseMainSize = isHorizontal ? childSize.width : childSize.height; - } - // Use clamped intrinsic main size as the hypothetical size for line metrics. - final _RunChild runChild = _createRunChildMetadata( - child, - baseMainSize, - effectiveChild: effectiveChild, - usedFlexBasis: usedFlexBasis, - ); - runChildren.add(runChild); + // Per CSS Flexbox §9.7, keep two sizes: + // - flex base size: from flex-basis if definite, otherwise the intrinsic + // content-based size BEFORE min/max clamping. + // - hypothetical main size: the base size clamped by min/max. + // We store the base size in runChild.originalMainSize so remaining free + // space and shrink/grow weighting use the correct base, and keep the + // clamped value in _childrenIntrinsicMainSizes for line metrics. + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; + + double baseMainSize; + if (usedFlexBasis != null) { + // Used basis is already border-box (>= padding+border) for non-zero bases. + // For flex-basis: 0% in the main axis, use a flex base size of 0 so equal-flex + // items share free space evenly regardless of padding/border, while the + // non-flex portion (padding/border) is accounted for separately in totalSpace. + final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; + if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { + baseMainSize = 0; + } else { + baseMainSize = usedFlexBasis; + } + } else { + // childSize is the intrinsic measurement from PASS 1 (pre-clamp). + baseMainSize = isHorizontal ? childSize.width : childSize.height; + } - childParentData!.runIndex = runMetrics.length; + // Use clamped intrinsic main size as the hypothetical size for line metrics. + final _RunChild runChild = _createRunChildMetadata( + child, + baseMainSize, + effectiveChild: effectiveChild, + usedFlexBasis: usedFlexBasis, + ); + runChildren.add(runChild); - assert(child.parentData == childParentData); + childParentData!.runIndex = runMetrics.length; - if (runChild.flexGrow > 0) { - totalFlexGrow += runChild.flexGrow; - } - if (runChild.flexShrink > 0) { - totalFlexShrink += runChild.flexShrink; + assert(child.parentData == childParentData); + + if (runChild.flexGrow > 0) { + totalFlexGrow += runChild.flexGrow; + } + if (runChild.flexShrink > 0) { + totalFlexShrink += runChild.flexShrink; + } } + } finally { + _transientChildSizeOverrides = null; } if (runChildren.isNotEmpty) { @@ -2884,6 +3284,7 @@ class RenderFlexLayout extends RenderLayoutBox { required bool isMainSizeDefinite, required double? contentBoxLogicalWidth, required double? contentBoxLogicalHeight, + _FlexFastPathRejectCallback? onReject, }) { final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); @@ -2930,7 +3331,33 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (willGrow || willShrink) return false; + if (willShrink) { + onReject?.call( + _FlexFastPathRejectReason.wouldShrink, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexShrink': metrics.totalFlexShrink.toStringAsFixed(2), + }, + ); + return false; + } + if (willGrow) { + onReject?.call( + _FlexFastPathRejectReason.wouldGrow, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexGrow': metrics.totalFlexGrow.toStringAsFixed(2), + 'boundedOnly': boundedOnly, + 'isMainSizeDefinite': isMainSizeDefinite, + 'containerStyleMin': containerStyleMin.toStringAsFixed(2), + }, + ); + return false; + } } // No flexing/no stretching/no baseline alignment: relayout only items that actually need it. @@ -2945,7 +3372,7 @@ class RenderFlexLayout extends RenderLayoutBox { final RenderBoxModel? effectiveChild = runChild.effectiveChild; if (effectiveChild == null) continue; - final double childOldMainSize = isHorizontal ? child.size.width : child.size.height; + final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; bool needsLayout = effectiveChild.needsRelayout; @@ -2967,7 +3394,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (!needsLayout && !isHorizontal) { final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; if (childCrossAuto && availCross.isFinite) { - final double measuredBorderW = effectiveChild.size.width; + final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; } @@ -2985,7 +3412,7 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenCount, preserveMainAxisSize: desiredPreservedMain, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); didRelayout = true; } @@ -3195,7 +3622,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Non-RenderBoxModel child: nothing to tighten in phase 1. continue; } - double childOldMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double childOldMainSize = _getMainSize(child); // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; @@ -3237,7 +3664,7 @@ class RenderFlexLayout extends RenderLayoutBox { final double availCross = contentConstraints?.maxWidth ?? double.infinity; if (childCrossAuto && noStretch && availCross.isFinite) { // Compare against the border-box width measured during intrinsic pass. - final double measuredBorderW = effectiveChild.size.width; + final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; } @@ -3256,7 +3683,7 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenList.length, preserveMainAxisSize: desiredPreservedMain, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); } // After Phase 1, recompute the run cross extent based on the items’ natural @@ -3285,7 +3712,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (childStretchedCrossSize == null) continue; // If the current cross size already matches the stretched result, skip. - final double currentCross = _isHorizontalFlexDirection ? child.size.height : child.size.width; + final double currentCross = _isHorizontalFlexDirection + ? _getChildSize(child)!.height + : _getChildSize(child)!.width; if ((childStretchedCrossSize - currentCross).abs() < 0.5) continue; // Apply stretch by relayout with tightened cross-axis constraint. @@ -3295,7 +3724,7 @@ class RenderFlexLayout extends RenderLayoutBox { childStretchedCrossSize, runChildrenList.length, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); } // Finally, recompute run main & cross extents using the final sizes. @@ -3322,8 +3751,9 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { RenderBox child = runChild.child; - double childMainSize = isHorizontal ? child.size.width : child.size.height; - double childCrossSize = isHorizontal ? child.size.height : child.size.width; + final Size childSize = _getChildSize(child)!; + double childMainSize = isHorizontal ? childSize.width : childSize.height; + double childCrossSize = isHorizontal ? childSize.height : childSize.width; double childCrossMargin = 0; if (child is RenderBoxModel) { childCrossMargin = isHorizontal @@ -3627,7 +4057,7 @@ class RenderFlexLayout extends RenderLayoutBox { // but only when it is less than the container's available cross size to avoid // regressing centering cases (e.g., column-wrap with align-self:center). if (maxContentCB <= minContentCB + 0.5) { - final double priorBorderW = child.size.width; + final double priorBorderW = _getChildSize(child)!.width; final double priorContentW = math.max(0.0, priorBorderW - (child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal)); if (priorContentW.isFinite && priorContentW > minContentCB) { @@ -3681,10 +4111,10 @@ class RenderFlexLayout extends RenderLayoutBox { // stretch cases and avoids regressions. double fixedW; if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? child.size.height; + final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? _getChildSize(child)!.height; fixedW = usedBorderBoxH * child.renderStyle.aspectRatio!; } else { - fixedW = child.size.width; + fixedW = _getChildSize(child)!.width; } final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; final double containerContentW = containerCrossMax.isFinite @@ -4076,7 +4506,7 @@ class RenderFlexLayout extends RenderLayoutBox { double runMainExtent = 0; for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - double runChildMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double runChildMainSize = _getMainSize(child); // Should add main axis margin of child to the main axis auto size of parent. if (child is RenderBoxModel) { double childMarginTop = child.renderStyle.marginTop.computedValue; @@ -4116,7 +4546,8 @@ class RenderFlexLayout extends RenderLayoutBox { double runCrossExtent = 0; for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - final double runChildCrossSize = _isHorizontalFlexDirection ? child.size.height : child.size.width; + final Size childSize = _getChildSize(child)!; + final double runChildCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; runCrossExtent = math.max(runCrossExtent, runChildCrossSize); } runCrossSize.add(runCrossExtent); @@ -4163,7 +4594,7 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - Size childScrollableSize = child.size; + Size childScrollableSize = _getChildSize(child)!; double childOffsetX = 0; double childOffsetY = 0; @@ -4200,8 +4631,9 @@ class RenderFlexLayout extends RenderLayoutBox { } } - final double childBoxMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; - final double childBoxCrossSize = _isHorizontalFlexDirection ? child.size.height : child.size.width; + final Size childSize = _getChildSize(child)!; + final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; final double childMainOffset = _isHorizontalFlexDirection ? childOffsetX : childOffsetY; final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection @@ -4224,7 +4656,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). - double childMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double childMainSize = _getMainSize(child); if (child is RenderBoxModel) { if (_isHorizontalFlexDirection) { childMainSize += child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; @@ -5200,11 +5632,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Get child size through boxSize to avoid flutter error when parentUsesSize is set to false. Size? _getChildSize(RenderBox? child, {bool shouldUseIntrinsicMainSize = false}) { Size? childSize; - if (child is RenderBoxModel) { + if (child != null) { + childSize = _transientChildSizeOverrides?[child]; + } + + if (childSize == null && child is RenderBoxModel) { childSize = child.boxSize; - } else if (child is RenderPositionPlaceholder) { + } else if (childSize == null && child is RenderPositionPlaceholder) { childSize = child.boxSize; - } else if (child != null && child.hasSize) { + } else if (childSize == null && child != null && child.hasSize) { // child is WidgetElement. childSize = child.size; } diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index a91e0949be..c6e4b791c0 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -21,10 +21,14 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin String _data; TextPainter? _textPainter; TextSpan? _cachedSpan; + bool _hasPendingTextLayoutUpdate = false; + + bool get hasPendingTextLayoutUpdate => _hasPendingTextLayoutUpdate; set data(String value) { if (_data == value) return; _data = value; + _hasPendingTextLayoutUpdate = true; // Text content changed. Since text boxes are measured and painted by the // parent's inline formatting context, notify the parent to relayout so the // paragraph gets rebuilt with the new text content. @@ -225,6 +229,7 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin @override void performLayout() { + _hasPendingTextLayoutUpdate = false; if (_data.isEmpty) { size = constraints.constrain(Size.zero); return; diff --git a/webf/lib/src/rendering/widget.dart b/webf/lib/src/rendering/widget.dart index 5c20cd9dfe..e3933e8a3f 100644 --- a/webf/lib/src/rendering/widget.dart +++ b/webf/lib/src/rendering/widget.dart @@ -22,6 +22,7 @@ class RenderWidget extends RenderBoxModel // Cache sticky children to calculate the base offset of sticky children final Set stickyChildren = {}; + RenderBoxModel? _relayoutNotifyingChild; @override BoxSizeType get widthSizeType { @@ -53,6 +54,16 @@ class RenderWidget extends RenderBoxModel return null; } + void _updateRelayoutNotifyingChild(RenderBox? child) { + final RenderBoxModel? next = child is RenderBoxModel ? child : null; + if (identical(_relayoutNotifyingChild, next)) { + return; + } + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(null); + _relayoutNotifyingChild = next; + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(this); + } + void _layoutChild(RenderBox child) { // Ensure logical content sizes are computed from CSS before deriving constraints // so that explicit width/height (e.g. h-8) can be honored. @@ -188,9 +199,22 @@ class RenderWidget extends RenderBoxModel // Deflate padding constraints. // childConstraints = renderStyle.deflatePaddingConstraints(childConstraints); - child.layout(childConstraints, parentUsesSize: true); + _updateRelayoutNotifyingChild(child); - Size childSize = child.size; + Size childSize; + if (child is RenderBoxModel) { + child.layout(childConstraints, parentUsesSize: false); + final Size? childBoxSize = child.boxSize; + if (childBoxSize != null) { + childSize = childBoxSize; + } else { + child.layout(childConstraints, parentUsesSize: true); + childSize = child.size; + } + } else { + child.layout(childConstraints, parentUsesSize: true); + childSize = child.size; + } setMaxScrollableSize(childSize); size = getBoxSize(childSize); @@ -610,6 +634,7 @@ class RenderWidget extends RenderBoxModel if (nonPositionedChildren.isNotEmpty) { _layoutChild(nonPositionedChildren.first); } else { + _updateRelayoutNotifyingChild(null); performResize(); } diff --git a/webf/lib/src/rendering/widget_element_child.dart b/webf/lib/src/rendering/widget_element_child.dart index 9b50de403c..c204735e29 100644 --- a/webf/lib/src/rendering/widget_element_child.dart +++ b/webf/lib/src/rendering/widget_element_child.dart @@ -52,6 +52,7 @@ class WebFWidgetElementChild extends SingleChildRenderObjectWidget { /// WebF HTML elements through the [findWidgetElementChild] method. class RenderWidgetElementChild extends RenderProxyBox { BoxConstraints? _effectiveChildConstraints; + RenderBoxModel? _relayoutNotifyingChild; /// The last constraints actually used to lay out [child]. /// @@ -59,6 +60,16 @@ class RenderWidgetElementChild extends RenderProxyBox { /// we collapse to intrinsic sizing before laying out the WebF subtree. BoxConstraints get effectiveChildConstraints => _effectiveChildConstraints ?? constraints; + void _updateRelayoutNotifyingChild(RenderBox? child) { + final RenderBoxModel? next = child is RenderBoxModel ? child : null; + if (identical(_relayoutNotifyingChild, next)) { + return; + } + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(null); + _relayoutNotifyingChild = next; + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(this); + } + @override void performLayout() { final BoxConstraints incoming = constraints; @@ -88,6 +99,7 @@ class RenderWidgetElementChild extends RenderProxyBox { } _effectiveChildConstraints = effective; + _updateRelayoutNotifyingChild(c); if (c is RenderBoxModel) { // Ensure CSS sizing queries resolve constraints against the *current* @@ -103,9 +115,21 @@ class RenderWidgetElementChild extends RenderProxyBox { } if (c != null) { - c.layout(effective, parentUsesSize: true); - size = c.size; + if (c is RenderBoxModel) { + c.layout(effective, parentUsesSize: false); + final Size? childBoxSize = c.boxSize; + if (childBoxSize != null) { + size = childBoxSize; + } else { + c.layout(effective, parentUsesSize: true); + size = c.size; + } + } else { + c.layout(effective, parentUsesSize: true); + size = c.size; + } } else { + _updateRelayoutNotifyingChild(null); size = computeSizeForNoChild(incoming); } } From 62e58e642cc076f5cd02569057c82795b0f3bfed Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 21 Mar 2026 18:58:37 -0700 Subject: [PATCH 3/8] perf(flex): reuse wrapped flow measurements --- webf/lib/src/rendering/box_model.dart | 8 +++++- webf/lib/src/rendering/flex.dart | 35 ++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index a1e6377485..8751ad691a 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -392,6 +392,9 @@ abstract class RenderBoxModel extends RenderBox // Whether it needs relayout due to percentage calculation. bool needsRelayout = false; + bool _hasPendingLayoutInvalidation = true; + + bool get hasPendingLayoutInvalidation => _hasPendingLayoutInvalidation; // Mark parent as needs relayout used in cases such as // child has percentage length and parent's size can not be calculated by style @@ -407,6 +410,7 @@ abstract class RenderBoxModel extends RenderBox void markNeedsRelayout() { needsRelayout = true; + _hasPendingLayoutInvalidation = true; } // A flag to detect the size of this renderBox had changed during this layout. @@ -856,7 +860,7 @@ abstract class RenderBoxModel extends RenderBox @override set size(Size value) { - _boxSize = value; + _boxSize = Size.copy(value); Size? previousSize = hasSize ? super.size : null; if (previousSize != null && previousSize != value) { @@ -869,6 +873,7 @@ abstract class RenderBoxModel extends RenderBox @override void markNeedsLayout() { final RenderObject? relayoutParent = _relayoutParentOnSizeChange; + _hasPendingLayoutInvalidation = true; super.markNeedsLayout(); // Some wrapper parents mirror child.boxSize while laying the child out @@ -1011,6 +1016,7 @@ abstract class RenderBoxModel extends RenderBox this.contentConstraints = contentConstraints; clearOverflowLayout(); isSelfSizeChanged = false; + _hasPendingLayoutInvalidation = false; // Reset cached CSS baselines before a new layout pass. They will be // updated by subclasses that can establish inline formatting context diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index f52340f47e..1214bf6a01 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -690,6 +690,8 @@ class RenderFlexLayout extends RenderLayoutBox { Expando _childrenOldConstraints = Expando('childrenOldConstraints'); Expando<_FlexIntrinsicMeasurementCacheEntry> _childrenIntrinsicMeasureCache = Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + Expando _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); Expando? _transientChildSizeOverrides; _FlexContainerInvariants? _layoutInvariants; @@ -704,6 +706,8 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); _transientChildSizeOverrides = null; } @@ -1895,6 +1899,13 @@ class RenderFlexLayout extends RenderLayoutBox { } return child; } + if (child is RenderEventListener) { + final RenderBox? wrapped = child.child; + if (wrapped is RenderFlowLayout && + !wrapped.renderStyle.isSelfAnonymousFlowLayout()) { + return wrapped; + } + } return null; } @@ -1911,7 +1922,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { return true; } - if (root is RenderBoxModel && root.needsRelayout) { + if (root is RenderBoxModel && + (root.needsRelayout || root.hasPendingLayoutInvalidation)) { return true; } @@ -1982,6 +1994,14 @@ class RenderFlexLayout extends RenderLayoutBox { ); } + bool _shouldRequirePostMeasureLayout(RenderBox child) { + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild(child); + if (flowChild == null) { + return false; + } + return child is RenderEventListener || flowChild.renderStyle.isSelfAnonymousFlowLayout(); + } + bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { // Match Flutter's relayout-boundary model when the flex container only // needs the wrapper's cached boxSize, not RenderBox.size dependency wiring. @@ -2000,6 +2020,7 @@ class RenderFlexLayout extends RenderLayoutBox { childConstraints, parentUsesSize: !_shouldAvoidParentUsesSizeForFlexChild(child), ); + _childrenRequirePostMeasureLayout[child] = false; } _FlexFastPathRejectReason? _getEarlyNoFlexNoStretchNoBaselineRejectReason( @@ -2331,6 +2352,9 @@ class RenderFlexLayout extends RenderLayoutBox { // 3. Set flex container size according to children size and its own size styles. // 4. Align children according to alignment properties. void _layoutFlexItems(List children) { + _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); + // If no child exists, stop layout. if (children.isEmpty) { _setContainerSizeWithNoChild(); @@ -2710,6 +2734,9 @@ class RenderFlexLayout extends RenderLayoutBox { childSize = cacheEntry.size; intrinsicMain = cacheEntry.intrinsicMainSize; _transientChildSizeOverrides![child] = childSize; + if (_shouldRequirePostMeasureLayout(child)) { + _childrenRequirePostMeasureLayout[child] = true; + } } else { _layoutChildForFlex(child, childConstraints); @@ -3375,7 +3402,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; - bool needsLayout = effectiveChild.needsRelayout; + bool needsLayout = effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true); if (!needsLayout && desiredPreservedMain != null && desiredPreservedMain != childOldMainSize) { needsLayout = true; } @@ -3636,7 +3664,8 @@ class RenderFlexLayout extends RenderLayoutBox { } bool needsLayout = (childFlexedMainSize != null) || - (effectiveChild.needsRelayout); + (effectiveChild.needsRelayout) || + (_childrenRequirePostMeasureLayout[child] == true); if (!needsLayout && desiredPreservedMain != null && (desiredPreservedMain != childOldMainSize)) { needsLayout = true; } From 88688b6fb7e7d51de76c2f7280ebfa50cc18c31a Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 21 Mar 2026 19:02:16 -0700 Subject: [PATCH 4/8] perf(flex): reuse safe anonymous measurements --- webf/lib/src/rendering/flex.dart | 63 ++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 1214bf6a01..fe2093b4c3 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -1892,9 +1892,12 @@ class RenderFlexLayout extends RenderLayoutBox { } } - RenderFlowLayout? _getCacheableIntrinsicMeasureFlowChild(RenderBox child) { + RenderFlowLayout? _getCacheableIntrinsicMeasureFlowChild( + RenderBox child, { + bool allowAnonymous = false, + }) { if (child is RenderFlowLayout) { - if (child.renderStyle.isSelfAnonymousFlowLayout()) { + if (!allowAnonymous && child.renderStyle.isSelfAnonymousFlowLayout()) { return null; } return child; @@ -1902,13 +1905,43 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderEventListener) { final RenderBox? wrapped = child.child; if (wrapped is RenderFlowLayout && - !wrapped.renderStyle.isSelfAnonymousFlowLayout()) { + (allowAnonymous || !wrapped.renderStyle.isSelfAnonymousFlowLayout())) { return wrapped; } } return null; } + bool _canUseAnonymousMetricsOnlyCache(List children) { + if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + return false; + } + + bool hasAnonymousFlowChild = false; + for (final RenderBox child in children) { + if (_getEarlyNoFlexNoStretchNoBaselineRejectReason( + child, + const BoxConstraints(), + ) != + null) { + return false; + } + if (!isFlexNone(child)) { + return false; + } + + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (flowChild != null && flowChild.renderStyle.isSelfAnonymousFlowLayout()) { + hasAnonymousFlowChild = true; + } + } + + return hasAnonymousFlowChild; + } + bool _hasBaselineAlignmentForChild(RenderBox child) { if (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline) { @@ -1951,6 +1984,9 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexIntrinsicMeasurementCacheEntry? _getReusableIntrinsicMeasurement( RenderBox child, BoxConstraints childConstraints, + { + bool allowAnonymous = false, + } ) { if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { return null; @@ -1959,7 +1995,10 @@ class RenderFlexLayout extends RenderLayoutBox { return null; } - final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild(child); + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: allowAnonymous, + ); if (flowChild == null || flowChild.needsRelayout) { return null; } @@ -1984,7 +2023,8 @@ class RenderFlexLayout extends RenderLayoutBox { Size childSize, double intrinsicMainSize, ) { - if (_getCacheableIntrinsicMeasureFlowChild(child) == null) { + if (_getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true) == + null) { return; } _childrenIntrinsicMeasureCache[child] = _FlexIntrinsicMeasurementCacheEntry( @@ -1995,7 +2035,10 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _shouldRequirePostMeasureLayout(RenderBox child) { - final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild(child); + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); if (flowChild == null) { return false; } @@ -2721,12 +2764,18 @@ class RenderFlexLayout extends RenderLayoutBox { List<_RunChild> runChildren = <_RunChild>[]; // PASS 1+2: Intrinsic layout + compute run metrics in one pass. + final bool allowAnonymousMetricsOnlyCache = + _canUseAnonymousMetricsOnlyCache(children); _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); try { for (RenderBox child in children) { final BoxConstraints childConstraints = _getIntrinsicConstraints(child); final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = - _getReusableIntrinsicMeasurement(child, childConstraints); + _getReusableIntrinsicMeasurement( + child, + childConstraints, + allowAnonymous: allowAnonymousMetricsOnlyCache, + ); final Size childSize; double intrinsicMain; From 03ac84dce61b5a9628d5f05adfdc31cc77c30bf5 Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 22 Mar 2026 04:05:23 -0700 Subject: [PATCH 5/8] perf(flex): profile anonymous flow metrics reuse --- webf/lib/src/foundation/debug_flags.dart | 8 + webf/lib/src/rendering/flex.dart | 347 +++++++++++++++++++++-- 2 files changed, 330 insertions(+), 25 deletions(-) diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index 7598b7bbb6..7857ce2c15 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -123,6 +123,14 @@ class DebugFlags { const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); static int flexFastPathProfilingMaxDetailLogs = const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); + static bool enableFlexAnonymousMetricsProfiling = + const bool.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS', defaultValue: false); + static int flexAnonymousMetricsProfilingSummaryEvery = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', defaultValue: 50); + static int flexAnonymousMetricsProfilingMaxDetailLogs = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', defaultValue: 20); + static String flexAnonymousMetricsProfilingWatchedPathContains = + const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', defaultValue: ''); /// Debug flag to enable inline layout visualization. /// When true, paints debug information for line boxes, margins, padding, etc. diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index fe2093b4c3..d85a7c5f1b 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -15,6 +15,7 @@ import 'package:webf/dom.dart'; import 'package:webf/foundation.dart'; import 'package:webf/rendering.dart'; import 'package:webf/css.dart'; +import 'package:webf/src/html/forms.dart' show ButtonElement; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; @@ -56,6 +57,42 @@ String _flexFastPathRejectReasonLabel(_FlexFastPathRejectReason reason) { } } +enum _FlexAnonymousMetricsRejectReason { + verticalDirection, + wrappedContainer, + positionedPlaceholderChild, + containerAlignItemsBaseline, + containerAlignItemsStretch, + childAlignSelfBaseline, + childAlignSelfStretch, + childNotFlexNone, + noAnonymousFlowChild, +} + +String _flexAnonymousMetricsRejectReasonLabel( + _FlexAnonymousMetricsRejectReason reason) { + switch (reason) { + case _FlexAnonymousMetricsRejectReason.verticalDirection: + return 'verticalDirection'; + case _FlexAnonymousMetricsRejectReason.wrappedContainer: + return 'wrappedContainer'; + case _FlexAnonymousMetricsRejectReason.positionedPlaceholderChild: + return 'positionedPlaceholderChild'; + case _FlexAnonymousMetricsRejectReason.containerAlignItemsBaseline: + return 'containerAlignItemsBaseline'; + case _FlexAnonymousMetricsRejectReason.containerAlignItemsStretch: + return 'containerAlignItemsStretch'; + case _FlexAnonymousMetricsRejectReason.childAlignSelfBaseline: + return 'childAlignSelfBaseline'; + case _FlexAnonymousMetricsRejectReason.childAlignSelfStretch: + return 'childAlignSelfStretch'; + case _FlexAnonymousMetricsRejectReason.childNotFlexNone: + return 'childNotFlexNone'; + case _FlexAnonymousMetricsRejectReason.noAnonymousFlowChild: + return 'noAnonymousFlowChild'; + } +} + typedef _FlexFastPathRejectCallback = void Function( _FlexFastPathRejectReason reason, { Map? details, @@ -162,6 +199,160 @@ class _FlexFastPathProfiler { } } +class _FlexAnonymousMetricsProfiler { + static int _rowsEvaluated = 0; + static int _rowsEligible = 0; + static int _childCacheHits = 0; + static int _childCacheMisses = 0; + static int _detailLogs = 0; + static final Map<_FlexAnonymousMetricsRejectReason, int> _rejectCounts = + <_FlexAnonymousMetricsRejectReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexAnonymousMetricsProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexAnonymousMetricsProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static String get _watchedPathSubstring => + DebugFlags.flexAnonymousMetricsProfilingWatchedPathContains.trim(); + + static bool _shouldForceDetail(String path) { + final String watched = _watchedPathSubstring; + return watched.isNotEmpty && path.contains(watched); + } + + static void recordEligibleRow( + String path, { + required int childCount, + required int anonymousChildCount, + }) { + if (!enabled) return; + _rowsEvaluated++; + _rowsEligible++; + + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][eligible] path=$path childCount=$childCount ' + 'anonymousChildCount=$anonymousChildCount', + ); + _maybeLogSummary(); + } + + static void recordRejectedRow( + String path, + _FlexAnonymousMetricsRejectReason reason, { + String? childLabel, + int? childIndex, + Map? details, + }) { + if (!enabled) return; + _rowsEvaluated++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + + final StringBuffer message = StringBuffer() + ..write('[FlexAnonymousMetrics][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexAnonymousMetricsRejectReasonLabel(reason)); + if (childIndex != null) { + message + ..write(' childIndex=') + ..write(childIndex); + } + if (childLabel != null) { + message + ..write(' child=') + ..write(childLabel); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + _maybeLogDetail(path, message.toString()); + _maybeLogSummary(); + } + + static void recordChildCacheHit( + String path, { + required String childLabel, + required int childIndex, + }) { + if (!enabled) return; + _childCacheHits++; + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][cacheHit] path=$path childIndex=$childIndex child=$childLabel', + ); + _maybeLogSummary(); + } + + static void recordChildCacheMiss( + String path, { + required String childLabel, + required int childIndex, + }) { + if (!enabled) return; + _childCacheMisses++; + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][cacheMiss] path=$path childIndex=$childIndex child=$childLabel', + ); + _maybeLogSummary(); + } + + static void _maybeLogDetail(String path, String message) { + final bool forceDetail = _shouldForceDetail(path); + if (!forceDetail) { + if (_detailLogs >= _maxDetailLogs) return; + _detailLogs++; + } + renderingLogger.info(message); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_rowsEvaluated == 0 || _rowsEvaluated % _summaryEvery != 0) return; + + final int rowsRejected = _rowsEvaluated - _rowsEligible; + final double rowHitRate = + _rowsEvaluated == 0 ? 0.0 : (_rowsEligible / _rowsEvaluated) * 100.0; + final int childAttempts = _childCacheHits + _childCacheMisses; + final double childHitRate = + childAttempts == 0 ? 0.0 : (_childCacheHits / childAttempts) * 100.0; + final List> rejectEntries = + _rejectCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + final String rejectSummary = rejectEntries.isEmpty + ? 'none' + : rejectEntries + .map((MapEntry<_FlexAnonymousMetricsRejectReason, int> entry) => + '${_flexAnonymousMetricsRejectReasonLabel(entry.key)}=${entry.value}') + .join(', '); + + renderingLogger.info( + '[FlexAnonymousMetrics][summary] rows=$_rowsEvaluated eligible=$_rowsEligible ' + 'eligibleRate=${rowHitRate.toStringAsFixed(1)}% rejected=$rowsRejected ' + 'childCacheHits=$_childCacheHits childCacheMisses=$_childCacheMisses ' + 'childHitRate=${childHitRate.toStringAsFixed(1)}% reasons=$rejectSummary', + ); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + class _FlexIntrinsicMeasurementCacheEntry { const _FlexIntrinsicMeasurementCacheEntry({ required this.constraints, @@ -1897,49 +2088,65 @@ class RenderFlexLayout extends RenderLayoutBox { bool allowAnonymous = false, }) { if (child is RenderFlowLayout) { - if (!allowAnonymous && child.renderStyle.isSelfAnonymousFlowLayout()) { - return null; + if (child.renderStyle.isSelfAnonymousFlowLayout()) { + if (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(child)) { + return null; + } } return child; } if (child is RenderEventListener) { final RenderBox? wrapped = child.child; - if (wrapped is RenderFlowLayout && - (allowAnonymous || !wrapped.renderStyle.isSelfAnonymousFlowLayout())) { + if (wrapped is RenderFlowLayout) { + if (wrapped.renderStyle.isSelfAnonymousFlowLayout() && + (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(wrapped))) { + return null; + } return wrapped; } } return null; } - bool _canUseAnonymousMetricsOnlyCache(List children) { - if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { - return false; - } + bool _canReuseAnonymousFlowMeasurement(RenderFlowLayout flowChild) { + final Element? parentElement = flowChild.renderStyle.target.parentElement; + // Button-owned anonymous wrappers still regress :hover/:active snapshots + // when their intrinsic measurement is reused across flex passes. + return parentElement is! ButtonElement; + } - bool hasAnonymousFlowChild = false; - for (final RenderBox child in children) { - if (_getEarlyNoFlexNoStretchNoBaselineRejectReason( - child, - const BoxConstraints(), - ) != - null) { - return false; + bool _canUseAnonymousMetricsOnlyCache(List children) { + int anonymousFlowChildCount = 0; + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; + if (_isAnonymousIntrinsicMeasureChild(child)) { + anonymousFlowChildCount++; } - if (!isFlexNone(child)) { + + final _FlexAnonymousMetricsRejectReason? rejectReason = + _getAnonymousMetricsRejectReason(child); + if (rejectReason != null) { + _recordAnonymousMetricsReject( + rejectReason, + child: child, + childIndex: childIndex, + ); return false; } + } - final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( - child, - allowAnonymous: true, + if (anonymousFlowChildCount == 0) { + _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason.noAnonymousFlowChild, ); - if (flowChild != null && flowChild.renderStyle.isSelfAnonymousFlowLayout()) { - hasAnonymousFlowChild = true; - } + return false; } - return hasAnonymousFlowChild; + _recordAnonymousMetricsEligible( + childCount: children.length, + anonymousChildCount: anonymousFlowChildCount, + ); + return true; } bool _hasBaselineAlignmentForChild(RenderBox child) { @@ -2120,6 +2327,34 @@ class RenderFlexLayout extends RenderLayoutBox { return child.runtimeType.toString(); } + bool _isAnonymousIntrinsicMeasureChild(RenderBox child) { + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + return flowChild != null && flowChild.renderStyle.isSelfAnonymousFlowLayout(); + } + + _FlexAnonymousMetricsRejectReason? _getAnonymousMetricsRejectReason( + RenderBox child) { + if (child is RenderPositionPlaceholder) { + return _FlexAnonymousMetricsRejectReason.positionedPlaceholderChild; + } + + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline) { + return _FlexAnonymousMetricsRejectReason.containerAlignItemsBaseline; + } + + final AlignSelf alignSelf = _getAlignSelf(child); + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline) { + return _FlexAnonymousMetricsRejectReason.childAlignSelfBaseline; + } + + return null; + } + void _recordEarlyFastPathReject( _FlexFastPathRejectReason reason, { RenderBox? child, @@ -2146,6 +2381,57 @@ class RenderFlexLayout extends RenderLayoutBox { ); } + void _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason reason, { + RenderBox? child, + int? childIndex, + Map? details, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + _FlexAnonymousMetricsProfiler.recordRejectedRow( + _describeFastPathContainer(), + reason, + childLabel: child == null ? null : _describeFastPathChild(child), + childIndex: childIndex, + details: details, + ); + } + + void _recordAnonymousMetricsEligible({ + required int childCount, + required int anonymousChildCount, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + _FlexAnonymousMetricsProfiler.recordEligibleRow( + _describeFastPathContainer(), + childCount: childCount, + anonymousChildCount: anonymousChildCount, + ); + } + + void _recordAnonymousMetricsChildCache( + RenderBox child, { + required int childIndex, + required bool hit, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + final String path = _describeFastPathContainer(); + final String childLabel = _describeFastPathChild(child); + if (hit) { + _FlexAnonymousMetricsProfiler.recordChildCacheHit( + path, + childLabel: childLabel, + childIndex: childIndex, + ); + } else { + _FlexAnonymousMetricsProfiler.recordChildCacheMiss( + path, + childLabel: childLabel, + childIndex: childIndex, + ); + } + } + bool _canAttemptFullEarlyFastPath(List<_RunMetrics> runMetrics) { for (final _RunMetrics metrics in runMetrics) { for (final _RunChild runChild in metrics.runChildren) { @@ -2768,14 +3054,25 @@ class RenderFlexLayout extends RenderLayoutBox { _canUseAnonymousMetricsOnlyCache(children); _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); try { - for (RenderBox child in children) { + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; final BoxConstraints childConstraints = _getIntrinsicConstraints(child); + final bool isAnonymousMetricsChild = + allowAnonymousMetricsOnlyCache && + _isAnonymousIntrinsicMeasureChild(child); final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = _getReusableIntrinsicMeasurement( child, childConstraints, allowAnonymous: allowAnonymousMetricsOnlyCache, ); + if (isAnonymousMetricsChild) { + _recordAnonymousMetricsChildCache( + child, + childIndex: childIndex, + hit: cacheEntry != null, + ); + } final Size childSize; double intrinsicMain; From 1cfd53ae0d99bc7c79bbc47ef24e0cf95f259dd6 Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 22 Mar 2026 07:09:27 -0700 Subject: [PATCH 6/8] perf(flex): reuse safe text-heavy flow measurements --- webf/lib/src/css/border.dart | 12 + webf/lib/src/css/display.dart | 1 + webf/lib/src/css/font_face.dart | 5 + webf/lib/src/css/padding.dart | 1 + webf/lib/src/css/render_style.dart | 8 + webf/lib/src/css/sizing.dart | 1 + webf/lib/src/css/text.dart | 8 +- webf/lib/src/rendering/box_model.dart | 41 ++- webf/lib/src/rendering/event_listener.dart | 1 + webf/lib/src/rendering/flex.dart | 346 ++++++++++++++++++--- webf/lib/src/rendering/layout_box.dart | 4 + webf/lib/src/rendering/text.dart | 4 + webf/lib/src/rendering/widget.dart | 3 + 13 files changed, 389 insertions(+), 46 deletions(-) diff --git a/webf/lib/src/css/border.dart b/webf/lib/src/css/border.dart index 42a0c277a2..451fd12797 100644 --- a/webf/lib/src/css/border.dart +++ b/webf/lib/src/css/border.dart @@ -154,6 +154,7 @@ mixin CSSBorderMixin on RenderStyle { set borderTopWidth(CSSLengthValue? value) { if (value == _borderTopWidth) return; _borderTopWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -170,6 +171,7 @@ mixin CSSBorderMixin on RenderStyle { set borderRightWidth(CSSLengthValue? value) { if (value == _borderRightWidth) return; _borderRightWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -186,6 +188,7 @@ mixin CSSBorderMixin on RenderStyle { set borderBottomWidth(CSSLengthValue? value) { if (value == _borderBottomWidth) return; _borderBottomWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -202,6 +205,7 @@ mixin CSSBorderMixin on RenderStyle { set borderLeftWidth(CSSLengthValue? value) { if (value == _borderLeftWidth) return; _borderLeftWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -266,6 +270,8 @@ mixin CSSBorderMixin on RenderStyle { set borderTopStyle(CSSBorderStyleType? value) { if (value == _borderTopStyle) return; _borderTopStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -277,6 +283,8 @@ mixin CSSBorderMixin on RenderStyle { set borderRightStyle(CSSBorderStyleType? value) { if (value == _borderRightStyle) return; _borderRightStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -288,6 +296,8 @@ mixin CSSBorderMixin on RenderStyle { set borderBottomStyle(CSSBorderStyleType? value) { if (value == _borderBottomStyle) return; _borderBottomStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -299,6 +309,8 @@ mixin CSSBorderMixin on RenderStyle { set borderLeftStyle(CSSBorderStyleType? value) { if (value == _borderLeftStyle) return; _borderLeftStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } diff --git a/webf/lib/src/css/display.dart b/webf/lib/src/css/display.dart index 75678dbd74..32b3364430 100644 --- a/webf/lib/src/css/display.dart +++ b/webf/lib/src/css/display.dart @@ -33,6 +33,7 @@ mixin CSSDisplayMixin on RenderStyle { set display(CSSDisplay? value) { if (_display != value) { _display = value; + markNeedsIntrinsicMeasurement('display'); markNeedsLayout(); // CSS display affects accessibility visibility (e.g., display:none) attachedRenderBoxModel?.markNeedsSemanticsUpdate(); diff --git a/webf/lib/src/css/font_face.dart b/webf/lib/src/css/font_face.dart index 91a0501973..50e2826321 100644 --- a/webf/lib/src/css/font_face.dart +++ b/webf/lib/src/css/font_face.dart @@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart'; import 'package:webf/css.dart'; import 'package:webf/foundation.dart'; import 'package:webf/launcher.dart'; +import 'package:webf/rendering.dart'; import 'dart:convert'; import 'package:webf/src/foundation/logger.dart'; @@ -72,6 +73,9 @@ class CSSFontFace { static void _markRenderSubtreeNeedsLayout(RenderObject root) { root.visitChildren(_markRenderSubtreeNeedsLayout); + if (root is RenderBoxModel) { + root.markNeedsIntrinsicMeasurementUpdate('fontFaceSubtree'); + } root.markNeedsLayout(); root.markNeedsPaint(); } @@ -357,6 +361,7 @@ class CSSFontFace { } finally { // Remove from loading map when done _loadingFonts.remove(descriptorKey); + renderStyle.markNeedsIntrinsicMeasurement('fontFaceLoad'); renderStyle.markNeedsLayout(); } } diff --git a/webf/lib/src/css/padding.dart b/webf/lib/src/css/padding.dart index aa6e773bf2..1fc57cf4bb 100644 --- a/webf/lib/src/css/padding.dart +++ b/webf/lib/src/css/padding.dart @@ -116,6 +116,7 @@ mixin CSSPaddingMixin on RenderStyle { CSSLengthValue get paddingTop => _normalizePaddingLength(_paddingTop) ?? CSSLengthValue.zero; void _markSelfAndParentNeedsLayout() { + markNeedsIntrinsicMeasurement('padding'); markNeedsLayout(); // Sizing may affect parent size, mark parent as needsLayout in case // renderBoxModel has tight constraints which will prevent parent from marking. diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index e35c595141..aa430b96b3 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -1284,6 +1284,14 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { widgetRenderBox?.clearIntersectionChangeListeners(); } + @pragma('vm:prefer-inline') + void markNeedsIntrinsicMeasurement([String reason = 'renderStyle']) { + everyAttachedWidgetRenderBox((element, renderObject) { + renderObject.markNeedsIntrinsicMeasurementUpdate(reason); + return true; + }); + } + @pragma('vm:prefer-inline') void markNeedsLayout() { everyAttachedWidgetRenderBox((element, renderObject) { diff --git a/webf/lib/src/css/sizing.dart b/webf/lib/src/css/sizing.dart index 027acbda32..c79462cd54 100644 --- a/webf/lib/src/css/sizing.dart +++ b/webf/lib/src/css/sizing.dart @@ -222,6 +222,7 @@ mixin CSSSizingMixin on RenderStyle { void _markSelfAndParentNeedsLayout() { if (!hasRenderBox()) return; + markNeedsIntrinsicMeasurement('sizing'); markNeedsLayout(); // Sizing may affect parent size, mark parent as needsLayout in case diff --git a/webf/lib/src/css/text.dart b/webf/lib/src/css/text.dart index 5a89b67ff9..f2a993e27c 100644 --- a/webf/lib/src/css/text.dart +++ b/webf/lib/src/css/text.dart @@ -265,6 +265,7 @@ mixin CSSTextMixin on RenderStyle { void updateFontRelativeLength() { if (_fontRelativeProperties.isEmpty) return; + markNeedsIntrinsicMeasurement('fontRelativeLength'); markNeedsLayout(); if (isSelfBoxModelSizeTight()) { markParentNeedsLayout(); @@ -278,6 +279,7 @@ mixin CSSTextMixin on RenderStyle { void updateRootFontRelativeLength() { if (_rootFontRelativeProperties.isEmpty) return; + markNeedsIntrinsicMeasurement('rootFontRelativeLength'); markNeedsLayout(); if (isSelfBoxModelSizeTight()) { markParentNeedsLayout(); @@ -597,6 +599,7 @@ mixin CSSTextMixin on RenderStyle { // text and layout (line-height, white-space) changes. void _markNestChildrenTextAndLayoutNeedsLayout(RenderStyle renderStyle, String styleProperty) { if (renderStyle.isSelfRenderLayoutBox()) { + renderStyle.markNeedsIntrinsicMeasurement('textLayout:$styleProperty'); renderStyle.markNeedsLayout(); visitor(RenderObject child) { @@ -620,6 +623,7 @@ mixin CSSTextMixin on RenderStyle { void _markTextNeedsLayout() { visitor(RenderObject child) { if (child is RenderTextBox) { + child.renderStyle.markNeedsIntrinsicMeasurement('textDirect'); child.renderStyle.markNeedsLayout(); } else { child.visitChildren(visitor); @@ -635,7 +639,9 @@ mixin CSSTextMixin on RenderStyle { void _markChildrenTextNeedsLayout(RenderStyle renderStyle, String styleProperty) { visitor(dom.Node child) { if (child is dom.TextNode) { - child.parentElement!.attachedRenderer?.markNeedsLayout(); + final RenderStyle parentStyle = child.parentElement!.renderStyle; + parentStyle.markNeedsIntrinsicMeasurement('textInherited:$styleProperty'); + parentStyle.markNeedsLayout(); } if (child is dom.Element && child.style[styleProperty].isEmpty) { diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 8751ad691a..526bcd84c1 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -28,6 +28,7 @@ List renderBoxInLayoutHashCodes = []; // tree instance in cases where the same DOM element is mounted into multiple // Flutter subtrees simultaneously (e.g. CupertinoContextMenu preview/modal). final List renderBoxModelInLayoutStack = []; +int renderBoxModelLayoutPassId = 0; class RenderLayoutParentData extends ContainerBoxParentData { // Row index of child when wrapping @@ -392,9 +393,14 @@ abstract class RenderBoxModel extends RenderBox // Whether it needs relayout due to percentage calculation. bool needsRelayout = false; - bool _hasPendingLayoutInvalidation = true; + bool _hasPendingIntrinsicMeasurementInvalidation = true; + String? _debugIntrinsicMeasurementDirtyReason = 'initial'; + int _clearIntrinsicMeasurementInvalidationAfterLayoutPass = 1; - bool get hasPendingLayoutInvalidation => _hasPendingLayoutInvalidation; + bool get hasPendingIntrinsicMeasurementInvalidation => + _hasPendingIntrinsicMeasurementInvalidation; + String? get debugIntrinsicMeasurementDirtyReason => + _debugIntrinsicMeasurementDirtyReason; // Mark parent as needs relayout used in cases such as // child has percentage length and parent's size can not be calculated by style @@ -410,7 +416,29 @@ abstract class RenderBoxModel extends RenderBox void markNeedsRelayout() { needsRelayout = true; - _hasPendingLayoutInvalidation = true; + } + + void markNeedsIntrinsicMeasurementUpdate([String reason = 'unspecified']) { + _hasPendingIntrinsicMeasurementInvalidation = true; + _debugIntrinsicMeasurementDirtyReason = reason; + _clearIntrinsicMeasurementInvalidationAfterLayoutPass = + renderBoxModelLayoutPassId + 1; + } + + void updateIntrinsicMeasurementInvalidationForCurrentLayoutPass() { + if (_hasPendingIntrinsicMeasurementInvalidation && + renderBoxModelLayoutPassId > + _clearIntrinsicMeasurementInvalidationAfterLayoutPass) { + _hasPendingIntrinsicMeasurementInvalidation = false; + _debugIntrinsicMeasurementDirtyReason = null; + } + } + + void clearIntrinsicMeasurementInvalidationAfterMeasurement() { + _hasPendingIntrinsicMeasurementInvalidation = false; + _debugIntrinsicMeasurementDirtyReason = null; + _clearIntrinsicMeasurementInvalidationAfterLayoutPass = + renderBoxModelLayoutPassId; } // A flag to detect the size of this renderBox had changed during this layout. @@ -421,6 +449,7 @@ abstract class RenderBoxModel extends RenderBox /// @override void dropChild(RenderObject child) { + markNeedsIntrinsicMeasurementUpdate('dropChild'); super.dropChild(child); } @@ -430,6 +459,9 @@ abstract class RenderBoxModel extends RenderBox void layout(Constraints constraints, {bool parentUsesSize = false}) { _lastLaidOutAsRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent == null; + if (renderBoxModelInLayoutStack.isEmpty) { + renderBoxModelLayoutPassId++; + } renderBoxInLayoutHashCodes.add(hashCode); renderBoxModelInLayoutStack.add(this); @@ -873,7 +905,6 @@ abstract class RenderBoxModel extends RenderBox @override void markNeedsLayout() { final RenderObject? relayoutParent = _relayoutParentOnSizeChange; - _hasPendingLayoutInvalidation = true; super.markNeedsLayout(); // Some wrapper parents mirror child.boxSize while laying the child out @@ -1016,7 +1047,7 @@ abstract class RenderBoxModel extends RenderBox this.contentConstraints = contentConstraints; clearOverflowLayout(); isSelfSizeChanged = false; - _hasPendingLayoutInvalidation = false; + updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); // Reset cached CSS baselines before a new layout pass. They will be // updated by subclasses that can establish inline formatting context diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 768606a0b8..9e5405f254 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -169,6 +169,7 @@ class RenderEventListener extends RenderBoxModel @override void performLayout() { + updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); size = (child?..layout(constraints, parentUsesSize: true))?.size ?? computeSizeForNoChild(constraints); diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index d85a7c5f1b..a54d64e2a4 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -16,6 +16,7 @@ import 'package:webf/foundation.dart'; import 'package:webf/rendering.dart'; import 'package:webf/css.dart'; import 'package:webf/src/html/forms.dart' show ButtonElement; +import 'package:webf/src/html/semantics_text.dart' show SpanElement; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; @@ -66,7 +67,8 @@ enum _FlexAnonymousMetricsRejectReason { childAlignSelfBaseline, childAlignSelfStretch, childNotFlexNone, - noAnonymousFlowChild, + noMetricsOnlyFlowChild, + mixedMetricsOnlyChildren, } String _flexAnonymousMetricsRejectReasonLabel( @@ -88,8 +90,34 @@ String _flexAnonymousMetricsRejectReasonLabel( return 'childAlignSelfStretch'; case _FlexAnonymousMetricsRejectReason.childNotFlexNone: return 'childNotFlexNone'; - case _FlexAnonymousMetricsRejectReason.noAnonymousFlowChild: - return 'noAnonymousFlowChild'; + case _FlexAnonymousMetricsRejectReason.noMetricsOnlyFlowChild: + return 'noMetricsOnlyFlowChild'; + case _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren: + return 'mixedMetricsOnlyChildren'; + } +} + +enum _FlexAnonymousMetricsMissReason { + flowNeedsRelayout, + childNeedsRelayout, + subtreeIntrinsicDirty, + missingCacheEntry, + constraintsMismatch, +} + +String _flexAnonymousMetricsMissReasonLabel( + _FlexAnonymousMetricsMissReason reason) { + switch (reason) { + case _FlexAnonymousMetricsMissReason.flowNeedsRelayout: + return 'flowNeedsRelayout'; + case _FlexAnonymousMetricsMissReason.childNeedsRelayout: + return 'childNeedsRelayout'; + case _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty: + return 'subtreeIntrinsicDirty'; + case _FlexAnonymousMetricsMissReason.missingCacheEntry: + return 'missingCacheEntry'; + case _FlexAnonymousMetricsMissReason.constraintsMismatch: + return 'constraintsMismatch'; } } @@ -205,8 +233,11 @@ class _FlexAnonymousMetricsProfiler { static int _childCacheHits = 0; static int _childCacheMisses = 0; static int _detailLogs = 0; + static int _lastSummaryRows = -1; static final Map<_FlexAnonymousMetricsRejectReason, int> _rejectCounts = <_FlexAnonymousMetricsRejectReason, int>{}; + static final Map<_FlexAnonymousMetricsMissReason, int> _missCounts = + <_FlexAnonymousMetricsMissReason, int>{}; static bool get enabled => DebugFlags.enableFlexAnonymousMetricsProfiling; @@ -231,7 +262,7 @@ class _FlexAnonymousMetricsProfiler { static void recordEligibleRow( String path, { required int childCount, - required int anonymousChildCount, + required int candidateChildCount, }) { if (!enabled) return; _rowsEvaluated++; @@ -240,7 +271,7 @@ class _FlexAnonymousMetricsProfiler { _maybeLogDetail( path, '[FlexAnonymousMetrics][eligible] path=$path childCount=$childCount ' - 'anonymousChildCount=$anonymousChildCount', + 'candidateChildCount=$candidateChildCount', ); _maybeLogSummary(); } @@ -298,12 +329,17 @@ class _FlexAnonymousMetricsProfiler { String path, { required String childLabel, required int childIndex, + required _FlexAnonymousMetricsMissReason reason, + Map? details, }) { if (!enabled) return; _childCacheMisses++; + _missCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); _maybeLogDetail( path, - '[FlexAnonymousMetrics][cacheMiss] path=$path childIndex=$childIndex child=$childLabel', + '[FlexAnonymousMetrics][cacheMiss] path=$path childIndex=$childIndex child=$childLabel ' + 'reason=${_flexAnonymousMetricsMissReasonLabel(reason)}' + '${details != null && details.isNotEmpty ? ' details=${_formatDetails(details)}' : ''}', ); _maybeLogSummary(); } @@ -320,6 +356,8 @@ class _FlexAnonymousMetricsProfiler { static void _maybeLogSummary() { if (!enabled) return; if (_rowsEvaluated == 0 || _rowsEvaluated % _summaryEvery != 0) return; + if (_lastSummaryRows == _rowsEvaluated) return; + _lastSummaryRows = _rowsEvaluated; final int rowsRejected = _rowsEvaluated - _rowsEligible; final double rowHitRate = @@ -337,12 +375,22 @@ class _FlexAnonymousMetricsProfiler { .map((MapEntry<_FlexAnonymousMetricsRejectReason, int> entry) => '${_flexAnonymousMetricsRejectReasonLabel(entry.key)}=${entry.value}') .join(', '); + final List> missEntries = + _missCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + final String missSummary = missEntries.isEmpty + ? 'none' + : missEntries + .map((MapEntry<_FlexAnonymousMetricsMissReason, int> entry) => + '${_flexAnonymousMetricsMissReasonLabel(entry.key)}=${entry.value}') + .join(', '); renderingLogger.info( '[FlexAnonymousMetrics][summary] rows=$_rowsEvaluated eligible=$_rowsEligible ' 'eligibleRate=${rowHitRate.toStringAsFixed(1)}% rejected=$rowsRejected ' 'childCacheHits=$_childCacheHits childCacheMisses=$_childCacheMisses ' - 'childHitRate=${childHitRate.toStringAsFixed(1)}% reasons=$rejectSummary', + 'childHitRate=${childHitRate.toStringAsFixed(1)}% reasons=$rejectSummary ' + 'misses=$missSummary', ); } @@ -365,6 +413,18 @@ class _FlexIntrinsicMeasurementCacheEntry { final double intrinsicMainSize; } +class _FlexIntrinsicMeasurementLookupResult { + const _FlexIntrinsicMeasurementLookupResult({ + this.entry, + this.missReason, + this.missDetails, + }); + + final _FlexIntrinsicMeasurementCacheEntry? entry; + final _FlexAnonymousMetricsMissReason? missReason; + final Map? missDetails; +} + // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { @@ -2109,18 +2169,24 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _canReuseAnonymousFlowMeasurement(RenderFlowLayout flowChild) { - final Element? parentElement = flowChild.renderStyle.target.parentElement; + Element? parentElement = flowChild.renderStyle.target.parentElement; // Button-owned anonymous wrappers still regress :hover/:active snapshots // when their intrinsic measurement is reused across flex passes. - return parentElement is! ButtonElement; + while (parentElement != null) { + if (parentElement is ButtonElement) { + return false; + } + parentElement = parentElement.parentElement; + } + return true; } bool _canUseAnonymousMetricsOnlyCache(List children) { - int anonymousFlowChildCount = 0; + int metricsOnlyChildCount = 0; for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; - if (_isAnonymousIntrinsicMeasureChild(child)) { - anonymousFlowChildCount++; + if (_isMetricsOnlyIntrinsicMeasureChild(child)) { + metricsOnlyChildCount++; } final _FlexAnonymousMetricsRejectReason? rejectReason = @@ -2135,16 +2201,26 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (anonymousFlowChildCount == 0) { + if (metricsOnlyChildCount == 0) { + _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason.noMetricsOnlyFlowChild, + ); + return false; + } + if (metricsOnlyChildCount != children.length) { _recordAnonymousMetricsReject( - _FlexAnonymousMetricsRejectReason.noAnonymousFlowChild, + _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren, + details: { + 'childCount': children.length, + 'candidateChildCount': metricsOnlyChildCount, + }, ); return false; } _recordAnonymousMetricsEligible( childCount: children.length, - anonymousChildCount: anonymousFlowChildCount, + candidateChildCount: metricsOnlyChildCount, ); return true; } @@ -2163,7 +2239,7 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } if (root is RenderBoxModel && - (root.needsRelayout || root.hasPendingLayoutInvalidation)) { + root.hasPendingIntrinsicMeasurementInvalidation) { return true; } @@ -2188,40 +2264,143 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } - _FlexIntrinsicMeasurementCacheEntry? _getReusableIntrinsicMeasurement( + bool _hasWrappingFlexAncestor() { + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderFlexLayout) { + final FlexWrap wrap = ancestor.renderStyle.flexWrap; + if (wrap == FlexWrap.wrap || wrap == FlexWrap.wrapReverse) { + return true; + } + } + ancestor = ancestor.parent; + } + return false; + } + + Map? _describeFirstPendingIntrinsicMeasureInvalidation( + RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + return { + 'dirtyNode': 'RenderTextBox', + 'dirtyType': root.runtimeType, + 'dirtyReason': 'pendingTextLayoutUpdate', + }; + } + if (root is RenderBoxModel && + root.hasPendingIntrinsicMeasurementInvalidation) { + return { + 'dirtyNode': _describeFastPathChild(root), + 'dirtyType': root.runtimeType, + 'dirtyReason': root.debugIntrinsicMeasurementDirtyReason ?? 'unknown', + }; + } + + if (root is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + final Map? details = + _describeFirstPendingIntrinsicMeasureInvalidation(child); + if (details != null) return details; + child = (root as dynamic).childAfter(child) as RenderBox?; + } + return null; + } + + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + return _describeFirstPendingIntrinsicMeasureInvalidation(child); + } + } + + return null; + } + + void _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + root.clearPendingTextLayoutUpdateAfterMeasurement(); + } + if (root is RenderBoxModel) { + root.clearIntrinsicMeasurementInvalidationAfterMeasurement(); + } + + if (root + is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + child = (root as dynamic).childAfter(child) as RenderBox?; + } + return; + } + + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + } + } + } + + _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, BoxConstraints childConstraints, { bool allowAnonymous = false, } ) { - if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { - return null; + if (!allowAnonymous) { + return const _FlexIntrinsicMeasurementLookupResult(); } - if (_hasBaselineAlignmentForChild(child)) { - return null; - } - final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: allowAnonymous, ); - if (flowChild == null || flowChild.needsRelayout) { - return null; + final bool allowMetricsOnlyReuse = + flowChild != null && + _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + if ((!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) && + !allowMetricsOnlyReuse) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (_hasBaselineAlignmentForChild(child)) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (flowChild == null) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (flowChild.needsRelayout) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.flowNeedsRelayout, + ); } if (child is RenderBoxModel && child.needsRelayout) { - return null; + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, + ); } if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { - return null; + return _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty, + missDetails: _describeFirstPendingIntrinsicMeasureInvalidation(child), + ); } final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = _childrenIntrinsicMeasureCache[child]; - if (cacheEntry == null || cacheEntry.constraints != childConstraints) { - return null; + if (cacheEntry == null) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.missingCacheEntry, + ); } - return cacheEntry; + if (cacheEntry.constraints != childConstraints) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, + ); + } + return _FlexIntrinsicMeasurementLookupResult(entry: cacheEntry); } void _storeIntrinsicMeasurementCache( @@ -2327,12 +2506,83 @@ class RenderFlexLayout extends RenderLayoutBox { return child.runtimeType.toString(); } - bool _isAnonymousIntrinsicMeasureChild(RenderBox child) { + bool _isMetricsOnlyIntrinsicMeasureChild(RenderBox child) { final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, ); - return flowChild != null && flowChild.renderStyle.isSelfAnonymousFlowLayout(); + return flowChild != null && _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + } + + bool _isMetricsOnlyIntrinsicMeasureFlowChild(RenderFlowLayout flowChild) { + if (!_canReuseAnonymousFlowMeasurement(flowChild)) { + return false; + } + if (flowChild.renderStyle.isSelfAnonymousFlowLayout()) { + return true; + } + if (flowChild.renderStyle.target is SpanElement) { + return false; + } + return _flowSubtreeContainsReusableTextHeavyContent(flowChild); + } + + bool _flowSubtreeContainsReusableTextHeavyContent(RenderBox root) { + if (root is RenderPositionPlaceholder) { + return false; + } + + RenderBox effectiveRoot = root; + if (root is RenderEventListener) { + final RenderBox? wrapped = root.child; + if (wrapped == null) { + return false; + } + effectiveRoot = wrapped; + } + + if (effectiveRoot is RenderTextBox) { + return effectiveRoot.data.trim().isNotEmpty; + } + + if (effectiveRoot is RenderBoxModel) { + final CSSPositionType position = effectiveRoot.renderStyle.position; + if (position == CSSPositionType.absolute || + position == CSSPositionType.fixed) { + return false; + } + } + + if (effectiveRoot is RenderFlowLayout) { + if (effectiveRoot.renderStyle.isSelfAnonymousFlowLayout()) { + return _canReuseAnonymousFlowMeasurement(effectiveRoot); + } + if (effectiveRoot.establishIFC || + effectiveRoot.inlineFormattingContext != null) { + return true; + } + } + + if (effectiveRoot + is ContainerRenderObjectMixin>) { + RenderBox? child = (effectiveRoot as dynamic).firstChild as RenderBox?; + while (child != null) { + if (_flowSubtreeContainsReusableTextHeavyContent(child)) { + return true; + } + child = (effectiveRoot as dynamic).childAfter(child) as RenderBox?; + } + return false; + } + + if (effectiveRoot is RenderObjectWithChildMixin) { + final RenderBox? child = (effectiveRoot as dynamic).child as RenderBox?; + if (child != null) { + return _flowSubtreeContainsReusableTextHeavyContent(child); + } + } + + return false; } _FlexAnonymousMetricsRejectReason? _getAnonymousMetricsRejectReason( @@ -2399,13 +2649,13 @@ class RenderFlexLayout extends RenderLayoutBox { void _recordAnonymousMetricsEligible({ required int childCount, - required int anonymousChildCount, + required int candidateChildCount, }) { if (!_FlexAnonymousMetricsProfiler.enabled) return; _FlexAnonymousMetricsProfiler.recordEligibleRow( _describeFastPathContainer(), childCount: childCount, - anonymousChildCount: anonymousChildCount, + candidateChildCount: candidateChildCount, ); } @@ -2413,6 +2663,8 @@ class RenderFlexLayout extends RenderLayoutBox { RenderBox child, { required int childIndex, required bool hit, + _FlexAnonymousMetricsMissReason? missReason, + Map? missDetails, }) { if (!_FlexAnonymousMetricsProfiler.enabled) return; final String path = _describeFastPathContainer(); @@ -2428,6 +2680,8 @@ class RenderFlexLayout extends RenderLayoutBox { path, childLabel: childLabel, childIndex: childIndex, + reason: missReason ?? _FlexAnonymousMetricsMissReason.missingCacheEntry, + details: missDetails, ); } } @@ -3057,20 +3311,24 @@ class RenderFlexLayout extends RenderLayoutBox { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; final BoxConstraints childConstraints = _getIntrinsicConstraints(child); - final bool isAnonymousMetricsChild = + final bool isMetricsOnlyMeasureChild = allowAnonymousMetricsOnlyCache && - _isAnonymousIntrinsicMeasureChild(child); - final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = - _getReusableIntrinsicMeasurement( + _isMetricsOnlyIntrinsicMeasureChild(child); + final _FlexIntrinsicMeasurementLookupResult cacheLookup = + _lookupReusableIntrinsicMeasurement( child, childConstraints, allowAnonymous: allowAnonymousMetricsOnlyCache, ); - if (isAnonymousMetricsChild) { + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + cacheLookup.entry; + if (isMetricsOnlyMeasureChild) { _recordAnonymousMetricsChildCache( child, childIndex: childIndex, hit: cacheEntry != null, + missReason: cacheLookup.missReason, + missDetails: cacheLookup.missDetails, ); } @@ -3080,11 +3338,19 @@ class RenderFlexLayout extends RenderLayoutBox { childSize = cacheEntry.size; intrinsicMain = cacheEntry.intrinsicMainSize; _transientChildSizeOverrides![child] = childSize; - if (_shouldRequirePostMeasureLayout(child)) { + if (_shouldRequirePostMeasureLayout(child) || + isMetricsOnlyMeasureChild) { _childrenRequirePostMeasureLayout[child] = true; } } else { _layoutChildForFlex(child, childConstraints); + if (isMetricsOnlyMeasureChild && + renderStyle.flexWrap == FlexWrap.nowrap && + !_hasWrappingFlexAncestor()) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + child, + ); + } if (child is RenderBoxModel) { child.clearOverrideContentSize(); diff --git a/webf/lib/src/rendering/layout_box.dart b/webf/lib/src/rendering/layout_box.dart index d9fba73d0f..235b1f8231 100644 --- a/webf/lib/src/rendering/layout_box.dart +++ b/webf/lib/src/rendering/layout_box.dart @@ -507,24 +507,28 @@ abstract class RenderLayoutBox extends RenderBoxModel void insert(RenderBox child, {RenderBox? after}) { super.insert(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListInsert'); } @override void remove(RenderBox child) { super.remove(child); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemove'); } @override void removeAll() { super.removeAll(); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemoveAll'); } @override void move(RenderBox child, {RenderBox? after}) { super.move(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListMove'); } @override diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index c6e4b791c0..1d71c362bc 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -25,6 +25,10 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin bool get hasPendingTextLayoutUpdate => _hasPendingTextLayoutUpdate; + void clearPendingTextLayoutUpdateAfterMeasurement() { + _hasPendingTextLayoutUpdate = false; + } + set data(String value) { if (_data == value) return; _data = value; diff --git a/webf/lib/src/rendering/widget.dart b/webf/lib/src/rendering/widget.dart index e3933e8a3f..c64fbe1b36 100644 --- a/webf/lib/src/rendering/widget.dart +++ b/webf/lib/src/rendering/widget.dart @@ -585,18 +585,21 @@ class RenderWidget extends RenderBoxModel void insert(RenderBox child, {RenderBox? after}) { super.insert(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListInsert'); } @override void remove(RenderBox child) { super.remove(child); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemove'); } @override void move(RenderBox child, {RenderBox? after}) { super.move(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListMove'); } @override From 3d40c5846c88d2acbc19c9c197506699d5cb9ec3 Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 22 Mar 2026 10:59:32 -0700 Subject: [PATCH 7/8] fix(flex): use aligned offsets for scroll overflow --- .../flex-grow-chat-layout-issue-520.ts | 16 +++++- webf/lib/src/rendering/flex.dart | 49 ++++++++++++++----- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts b/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts index 0e9151ac27..0f7b35decb 100644 --- a/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts +++ b/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts @@ -102,6 +102,17 @@ describe('flex-grow column chat layout (issue #520)', () => { await snapshot(); + const lastItem = list.querySelectorAll('.list-item')[13] as HTMLElement; + const listRect = list.getBoundingClientRect(); + const lastRect = lastItem.getBoundingClientRect(); + const itemStyle = getComputedStyle(lastItem); + const listStyle = getComputedStyle(list); + const bottomOverflowAtTop = lastRect.bottom - listRect.bottom; + const expectedScrollGap = + bottomOverflowAtTop + + parseFloat(itemStyle.marginBottom || '0') + + parseFloat(listStyle.paddingBottom || '0'); + // Programmatic assertions to ensure expected layout behavior const chatH = chat.offsetHeight; const headerH = header.offsetHeight; @@ -119,6 +130,9 @@ describe('flex-grow column chat layout (issue #520)', () => { expect(firstItem.offsetHeight).toBe(secondItem.offsetHeight); // Items should not be stretched to container height expect(firstItem.offsetHeight).toBeLessThan(listH); + // Centered overflow should not leave extra blank scroll range after the + // last item's margin box and the container's bottom padding. + expect(Math.round(list.scrollHeight - list.clientHeight)) + .toBe(Math.round(expectedScrollGap)); }); }); - diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index a54d64e2a4..38ec4b0916 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -5218,6 +5218,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Set the size of scrollable overflow area for flex layout. // https://drafts.csswg.org/css-overflow-3/#scrollable void _setMaxScrollableSize(List<_RunMetrics> runMetrics) { + final double physicalMainAxisStartBorder = _isHorizontalFlexDirection + ? renderStyle.effectiveBorderLeftWidth.computedValue + : renderStyle.effectiveBorderTopWidth.computedValue; + final double physicalMainAxisEndPadding = _isHorizontalFlexDirection + ? renderStyle.paddingRight.computedValue + : renderStyle.paddingBottom.computedValue; // Scrollable main size collection of each line. List scrollableMainSizeOfLines = []; // Scrollable cross size collection of each line. @@ -5238,6 +5244,7 @@ class RenderFlexLayout extends RenderLayoutBox { Size childScrollableSize = _getChildSize(child)!; double childOffsetX = 0; double childOffsetY = 0; + double childTransformMainOverflow = 0; if (child is RenderBoxModel) { final RenderStyle childRenderStyle = child.renderStyle; @@ -5269,26 +5276,46 @@ class RenderFlexLayout extends RenderLayoutBox { if (transformOffset != null) { childOffsetX += transformOffset.dx; childOffsetY += transformOffset.dy; + childTransformMainOverflow = math.max( + 0, + _isHorizontalFlexDirection ? transformOffset.dx : transformOffset.dy, + ); } } final Size childSize = _getChildSize(child)!; final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; - final double childMainOffset = _isHorizontalFlexDirection ? childOffsetX : childOffsetY; final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection - ? childScrollableSize.width + childOffsetX - : childScrollableSize.height + childOffsetY; + ? childScrollableSize.width + childTransformMainOverflow + : childScrollableSize.height + childTransformMainOverflow; final double childScrollableCrossExtent = _isHorizontalFlexDirection ? childScrollableSize.height + childOffsetY : childScrollableSize.width + childOffsetX; - // The child's extent must cover at least its offset border-box. - // child.scrollableSize may only cover padding-box, but offsets (margin/relative/transform) - // still need to be preserved so negative offsets don't create phantom trailing scroll range. - final double childScrollableMain = preSiblingsMainSize + - math.max(childBoxMainSize + childMainOffset, childScrollableMainExtent); + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; + final double childMainPosition = _isHorizontalFlexDirection + ? (childParentData?.offset.dx ?? preSiblingsMainSize) + : (childParentData?.offset.dy ?? preSiblingsMainSize); + double childPhysicalMainEndMargin = 0; + if (child is RenderBoxModel) { + childPhysicalMainEndMargin = _isHorizontalFlexDirection + ? child.renderStyle.marginRight.computedValue + : child.renderStyle.marginBottom.computedValue; + } + + // Use the actual laid-out main-axis position so scrollable overflow follows + // post-alignment geometry (e.g. justify-content:center on overflowing columns) + // instead of the pre-alignment stacked size. This prevents blank trailing + // scroll range after children are shifted by negative leading space. + final double childScrollableMain = math.max( + 0, + childMainPosition - physicalMainAxisStartBorder, + ) + + math.max(childBoxMainSize, childScrollableMainExtent) + + childPhysicalMainEndMargin; final double childScrollableCross = math.max( childBoxCrossSize + childCrossOffset, childScrollableCrossExtent); @@ -5335,10 +5362,10 @@ class RenderFlexLayout extends RenderLayoutBox { bool isScrollContainer = renderStyle.effectiveOverflowX != CSSOverflowType.visible || renderStyle.effectiveOverflowY != CSSOverflowType.visible; - // Padding in the end direction of axis should be included in scroll container. + // Child positions already include physical start padding. Only the trailing + // padding needs to be added here. double maxScrollableMainSizeOfChildren = maxScrollableMainSizeOfLines + - _flowAwareMainAxisPadding() + - (isScrollContainer ? _flowAwareMainAxisPadding(isEnd: true) : 0); + (isScrollContainer ? physicalMainAxisEndPadding : 0); // Max scrollable cross size of all lines. double maxScrollableCrossSizeOfLines = scrollableCrossSizeOfLines.isEmpty From c6c07bd2be1c91c7a6e0bee8e4fc1a901dd17970 Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 22 Mar 2026 11:25:36 -0700 Subject: [PATCH 8/8] perf(flex): prune intrinsic measurement checks --- .../css-flexbox/relayout-align-to-stretch.ts | 8 +---- webf/lib/src/rendering/box_model.dart | 27 +++++++++++++++ webf/lib/src/rendering/flex.dart | 33 ++++++++++++++----- webf/lib/src/rendering/text.dart | 12 +++++++ 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts b/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts index bc51d68a21..2d9c196764 100644 --- a/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts +++ b/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts @@ -16,7 +16,7 @@ describe('relayout-align', () => { class: 'flexbox align-items-flex-start', style: { display: 'flex', - '-webkit-align-items': 'flex-start', + 'align-items': 'flex-start', height: '100px', position: 'relative', 'box-sizing': 'border-box', @@ -37,7 +37,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-auto', style: { - '-webkit-align-self': 'auto', 'align-self': 'auto', border: '5px solid green', width: '50px', @@ -49,7 +48,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-flex-start', style: { - '-webkit-align-self': 'flex-start', 'align-self': 'flex-start', border: '5px solid green', width: '50px', @@ -61,7 +59,6 @@ describe('relayout-align', () => { 'data-offset-y': '90', class: 'align-self-flex-end', style: { - '-webkit-align-self': 'flex-end', 'align-self': 'flex-end', border: '5px solid green', width: '50px', @@ -73,7 +70,6 @@ describe('relayout-align', () => { 'data-offset-y': '45', class: 'align-self-center', style: { - '-webkit-align-self': 'center', 'align-self': 'center', border: '5px solid green', width: '50px', @@ -85,7 +81,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-baseline', style: { - '-webkit-align-self': 'baseline', 'align-self': 'baseline', border: '5px solid green', width: '50px', @@ -97,7 +92,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-stretch', style: { - '-webkit-align-self': 'stretch', 'align-self': 'stretch', border: '5px solid green', width: '50px', diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 526bcd84c1..e2c483ecf1 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -394,11 +394,14 @@ abstract class RenderBoxModel extends RenderBox // Whether it needs relayout due to percentage calculation. bool needsRelayout = false; bool _hasPendingIntrinsicMeasurementInvalidation = true; + bool _hasPendingSubtreeIntrinsicMeasurementInvalidation = true; String? _debugIntrinsicMeasurementDirtyReason = 'initial'; int _clearIntrinsicMeasurementInvalidationAfterLayoutPass = 1; bool get hasPendingIntrinsicMeasurementInvalidation => _hasPendingIntrinsicMeasurementInvalidation; + bool get hasPendingSubtreeIntrinsicMeasurementInvalidation => + _hasPendingSubtreeIntrinsicMeasurementInvalidation; String? get debugIntrinsicMeasurementDirtyReason => _debugIntrinsicMeasurementDirtyReason; @@ -423,6 +426,29 @@ abstract class RenderBoxModel extends RenderBox _debugIntrinsicMeasurementDirtyReason = reason; _clearIntrinsicMeasurementInvalidationAfterLayoutPass = renderBoxModelLayoutPassId + 1; + _markNeedsSubtreeIntrinsicMeasurementUpdate(reason); + } + + void markNeedsSubtreeIntrinsicMeasurementUpdate( + [String reason = 'descendant']) { + _markNeedsSubtreeIntrinsicMeasurementUpdate(reason); + } + + void _markNeedsSubtreeIntrinsicMeasurementUpdate(String reason) { + if (_hasPendingSubtreeIntrinsicMeasurementInvalidation) { + return; + } + _hasPendingSubtreeIntrinsicMeasurementInvalidation = true; + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderBoxModel) { + if (ancestor._hasPendingSubtreeIntrinsicMeasurementInvalidation) { + break; + } + ancestor._hasPendingSubtreeIntrinsicMeasurementInvalidation = true; + } + ancestor = ancestor.parent; + } } void updateIntrinsicMeasurementInvalidationForCurrentLayoutPass() { @@ -436,6 +462,7 @@ abstract class RenderBoxModel extends RenderBox void clearIntrinsicMeasurementInvalidationAfterMeasurement() { _hasPendingIntrinsicMeasurementInvalidation = false; + _hasPendingSubtreeIntrinsicMeasurementInvalidation = false; _debugIntrinsicMeasurementDirtyReason = null; _clearIntrinsicMeasurementInvalidationAfterLayoutPass = renderBoxModelLayoutPassId; diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 38ec4b0916..16c505a100 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -944,6 +944,7 @@ class RenderFlexLayout extends RenderLayoutBox { Expando _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); Expando? _transientChildSizeOverrides; + Expando? _metricsOnlyIntrinsicMeasureChildEligibilityCache; _FlexContainerInvariants? _layoutInvariants; @@ -960,6 +961,7 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); _transientChildSizeOverrides = null; + _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; } @override @@ -2239,7 +2241,7 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } if (root is RenderBoxModel && - root.hasPendingIntrinsicMeasurementInvalidation) { + root.hasPendingSubtreeIntrinsicMeasurementInvalidation) { return true; } @@ -2381,13 +2383,6 @@ class RenderFlexLayout extends RenderLayoutBox { missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, ); } - if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { - return _FlexIntrinsicMeasurementLookupResult( - missReason: _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty, - missDetails: _describeFirstPendingIntrinsicMeasureInvalidation(child), - ); - } - final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = _childrenIntrinsicMeasureCache[child]; if (cacheEntry == null) { @@ -2400,6 +2395,14 @@ class RenderFlexLayout extends RenderLayoutBox { missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, ); } + if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty, + missDetails: _FlexAnonymousMetricsProfiler.enabled + ? _describeFirstPendingIntrinsicMeasureInvalidation(child) + : null, + ); + } return _FlexIntrinsicMeasurementLookupResult(entry: cacheEntry); } @@ -2507,11 +2510,20 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _isMetricsOnlyIntrinsicMeasureChild(RenderBox child) { + final Expando? eligibilityCache = + _metricsOnlyIntrinsicMeasureChildEligibilityCache; + final int? cachedValue = eligibilityCache?[child]; + if (cachedValue != null) { + return cachedValue == 1; + } final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, ); - return flowChild != null && _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + final bool isEligible = + flowChild != null && _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + eligibilityCache?[child] = isEligible ? 1 : 0; + return isEligible; } bool _isMetricsOnlyIntrinsicMeasureFlowChild(RenderFlowLayout flowChild) { @@ -3304,6 +3316,8 @@ class RenderFlexLayout extends RenderLayoutBox { List<_RunChild> runChildren = <_RunChild>[]; // PASS 1+2: Intrinsic layout + compute run metrics in one pass. + _metricsOnlyIntrinsicMeasureChildEligibilityCache = + Expando('metricsOnlyIntrinsicMeasureChildEligibilityCache'); final bool allowAnonymousMetricsOnlyCache = _canUseAnonymousMetricsOnlyCache(children); _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); @@ -3599,6 +3613,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } finally { _transientChildSizeOverrides = null; + _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; } if (runChildren.isNotEmpty) { diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index 1d71c362bc..6a7b372b06 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -29,10 +29,22 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin _hasPendingTextLayoutUpdate = false; } + void _markAncestorSubtreeIntrinsicMeasurementUpdate() { + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderBoxModel) { + ancestor.markNeedsSubtreeIntrinsicMeasurementUpdate('textData'); + break; + } + ancestor = ancestor.parent; + } + } + set data(String value) { if (_data == value) return; _data = value; _hasPendingTextLayoutUpdate = true; + _markAncestorSubtreeIntrinsicMeasurementUpdate(); // Text content changed. Since text boxes are measured and painted by the // parent's inline formatting context, notify the parent to relayout so the // paragraph gets rebuilt with the new text content.