Bug 1209697: Cache flex measuring reflows to avoid exponential behavior

This commit is contained in:
janekptacijarabaci 2018-04-24 20:18:05 +02:00 • committed by Roy Tam
commit 95b1132e04
2 changed files with 114 additions and 39 deletions

View file

@ -33,6 +33,8 @@ typedef nsFlexContainerFrame::FlexItem FlexItem;
typedef nsFlexContainerFrame::FlexLine FlexLine; typedef nsFlexContainerFrame::FlexLine FlexLine;
typedef nsFlexContainerFrame::FlexboxAxisTracker FlexboxAxisTracker; typedef nsFlexContainerFrame::FlexboxAxisTracker FlexboxAxisTracker;
typedef nsFlexContainerFrame::StrutInfo StrutInfo; typedef nsFlexContainerFrame::StrutInfo StrutInfo;
typedef nsFlexContainerFrame::CachedMeasuringReflowResult
CachedMeasuringReflowResult;
static mozilla::LazyLogModule gFlexContainerLog("nsFlexContainerFrame"); static mozilla::LazyLogModule gFlexContainerLog("nsFlexContainerFrame");
@ -1756,6 +1758,91 @@ nsFlexContainerFrame::
} }
} }
/**
* A cached result for a measuring reflow.
*
* Right now we only need to cache the available size, the height and the
* ascent. This can be extended later if needed.
*
* The assumption here is that a given flex item measurement won't change until
* either the available size changes, or the flex container intrinsic size is
* marked as dirty (due to a style or DOM change).
*
* Caching it prevents us from doing exponential reflows in cases of deeply
* nested flex and scroll frames.
*
* We store them in the frame property table for simplicity.
*/
struct nsFlexContainerFrame::CachedMeasuringReflowResult
{
LogicalSize mAvailableSize;
const nscoord mHeight;
const nscoord mAscent;
CachedMeasuringReflowResult(const LogicalSize& aAvailableSize,
nscoord aHeight,
nscoord aAscent)
: mAvailableSize(aAvailableSize)
, mHeight(aHeight)
, mAscent(aAscent)
{}
};
NS_DECLARE_FRAME_PROPERTY_DELETABLE(CachedFlexMeasuringReflow,
CachedMeasuringReflowResult);
const CachedMeasuringReflowResult&
nsFlexContainerFrame::MeasureAscentAndHeightForFlexItem(
FlexItem& aItem,
nsPresContext* aPresContext,
ReflowInput& aChildReflowInput)
{
const auto availableSize = aChildReflowInput.AvailableSize();
const FrameProperties props = aItem.Frame()->Properties();
if (const auto* cachedResult = props.Get(CachedFlexMeasuringReflow())) {
if (cachedResult->mAvailableSize == availableSize) {
return *cachedResult;
}
}
ReflowOutput childDesiredSize(aChildReflowInput);
nsReflowStatus childReflowStatus;
const uint32_t flags = NS_FRAME_NO_MOVE_FRAME;
ReflowChild(aItem.Frame(), aPresContext,
childDesiredSize, aChildReflowInput,
0, 0, flags, childReflowStatus);
aItem.SetHadMeasuringReflow();
// XXXdholbert Once we do pagination / splitting, we'll need to actually
// handle incomplete childReflowStatuses. But for now, we give our kids
// unconstrained available height, which means they should always complete.
MOZ_ASSERT(NS_FRAME_IS_COMPLETE(childReflowStatus),
"We gave flex item unconstrained available height, so it "
"should be complete");
// Tell the child we're done with its initial reflow.
// (Necessary for e.g. GetBaseline() to work below w/out asserting)
FinishReflowChild(aItem.Frame(), aPresContext,
childDesiredSize, &aChildReflowInput, 0, 0, flags);
auto result =
new CachedMeasuringReflowResult(availableSize,
childDesiredSize.Height(),
childDesiredSize.BlockStartAscent());
props.Set(CachedFlexMeasuringReflow(), result);
return *result;
}
/* virtual */ void
nsFlexContainerFrame::MarkIntrinsicISizesDirty()
{
for (nsIFrame* childFrame : mFrames) {
childFrame->Properties().Delete(CachedFlexMeasuringReflow());
}
}
nscoord nscoord
nsFlexContainerFrame:: nsFlexContainerFrame::
MeasureFlexItemContentHeight(nsPresContext* aPresContext, MeasureFlexItemContentHeight(nsPresContext* aPresContext,
@ -1783,27 +1870,15 @@ nsFlexContainerFrame::
childRIForMeasuringHeight.SetVResize(true); childRIForMeasuringHeight.SetVResize(true);
} }
ReflowOutput childDesiredSize(childRIForMeasuringHeight); const CachedMeasuringReflowResult& reflowResult =
nsReflowStatus childReflowStatus; MeasureAscentAndHeightForFlexItem(aFlexItem, aPresContext,
const uint32_t flags = NS_FRAME_NO_MOVE_FRAME; childRIForMeasuringHeight);
ReflowChild(aFlexItem.Frame(), aPresContext,
childDesiredSize, childRIForMeasuringHeight,
0, 0, flags, childReflowStatus);
MOZ_ASSERT(NS_FRAME_IS_COMPLETE(childReflowStatus), aFlexItem.SetAscent(reflowResult.mAscent);
"We gave flex item unconstrained available height, so it "
"should be complete");
FinishReflowChild(aFlexItem.Frame(), aPresContext,
childDesiredSize, &childRIForMeasuringHeight,
0, 0, flags);
aFlexItem.SetHadMeasuringReflow();
aFlexItem.SetAscent(childDesiredSize.BlockStartAscent());
// Subtract border/padding in vertical axis, to get _just_ // Subtract border/padding in vertical axis, to get _just_
// the effective computed value of the "height" property. // the effective computed value of the "height" property.
nscoord childDesiredHeight = childDesiredSize.Height() - nscoord childDesiredHeight = reflowResult.mHeight -
childRIForMeasuringHeight.ComputedPhysicalBorderPadding().TopBottom(); childRIForMeasuringHeight.ComputedPhysicalBorderPadding().TopBottom();
return std::max(0, childDesiredHeight); return std::max(0, childDesiredHeight);
@ -3959,25 +4034,10 @@ nsFlexContainerFrame::SizeItemInCrossAxis(
// whether any of its ancestors are being resized). // whether any of its ancestors are being resized).
aChildReflowInput.SetVResize(true); aChildReflowInput.SetVResize(true);
} }
ReflowOutput childDesiredSize(aChildReflowInput);
nsReflowStatus childReflowStatus;
const uint32_t flags = NS_FRAME_NO_MOVE_FRAME;
ReflowChild(aItem.Frame(), aPresContext,
childDesiredSize, aChildReflowInput,
0, 0, flags, childReflowStatus);
aItem.SetHadMeasuringReflow();
// XXXdholbert Once we do pagination / splitting, we'll need to actually // Potentially reflow the item, and get the sizing info.
// handle incomplete childReflowStatuses. But for now, we give our kids const CachedMeasuringReflowResult& reflowResult =
// unconstrained available height, which means they should always complete. MeasureAscentAndHeightForFlexItem(aItem, aPresContext, aChildReflowInput);
MOZ_ASSERT(NS_FRAME_IS_COMPLETE(childReflowStatus),
"We gave flex item unconstrained available height, so it "
"should be complete");
// Tell the child we're done with its initial reflow.
// (Necessary for e.g. GetBaseline() to work below w/out asserting)
FinishReflowChild(aItem.Frame(), aPresContext,
childDesiredSize, &aChildReflowInput, 0, 0, flags);
// Save the sizing info that we learned from this reflow // Save the sizing info that we learned from this reflow
// ----------------------------------------------------- // -----------------------------------------------------
@ -3989,7 +4049,7 @@ nsFlexContainerFrame::SizeItemInCrossAxis(
// so we don't bother with making aAxisTracker pick the cross-axis component // so we don't bother with making aAxisTracker pick the cross-axis component
// for us.) // for us.)
nscoord crossAxisBorderPadding = aItem.GetBorderPadding().TopBottom(); nscoord crossAxisBorderPadding = aItem.GetBorderPadding().TopBottom();
if (childDesiredSize.Height() < crossAxisBorderPadding) { if (reflowResult.mHeight < crossAxisBorderPadding) {
// Child's requested size isn't large enough for its border/padding! // Child's requested size isn't large enough for its border/padding!
// This is OK for the trivial nsFrame::Reflow() impl, but other frame // This is OK for the trivial nsFrame::Reflow() impl, but other frame
// classes should know better. So, if we get here, the child had better be // classes should know better. So, if we get here, the child had better be
@ -4002,10 +4062,10 @@ nsFlexContainerFrame::SizeItemInCrossAxis(
aItem.SetCrossSize(0); aItem.SetCrossSize(0);
} else { } else {
// (normal case) // (normal case)
aItem.SetCrossSize(childDesiredSize.Height() - crossAxisBorderPadding); aItem.SetCrossSize(reflowResult.mHeight - crossAxisBorderPadding);
} }
aItem.SetAscent(childDesiredSize.BlockStartAscent()); aItem.SetAscent(reflowResult.mAscent);
} }
void void
@ -4295,7 +4355,7 @@ nsFlexContainerFrame::DoFlexLayout(nsPresContext* aPresContext,
LogicalSize availSize = aReflowInput.ComputedSize(wm); LogicalSize availSize = aReflowInput.ComputedSize(wm);
availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
ReflowInput childReflowInput(aPresContext, aReflowInput, ReflowInput childReflowInput(aPresContext, aReflowInput,
item->Frame(), availSize); item->Frame(), availSize);
if (!sizeOverride) { if (!sizeOverride) {
// Directly override the computed main-size, by tweaking reflow state: // Directly override the computed main-size, by tweaking reflow state:
if (aAxisTracker.IsMainAxisHorizontal()) { if (aAxisTracker.IsMainAxisHorizontal()) {

View file

@ -56,6 +56,7 @@ public:
class FlexLine; class FlexLine;
class FlexboxAxisTracker; class FlexboxAxisTracker;
struct StrutInfo; struct StrutInfo;
struct CachedMeasuringReflowResult;
// nsIFrame overrides // nsIFrame overrides
void Init(nsIContent* aContent, void Init(nsIContent* aContent,
@ -66,6 +67,8 @@ public:
const nsRect& aDirtyRect, const nsRect& aDirtyRect,
const nsDisplayListSet& aLists) override; const nsDisplayListSet& aLists) override;
void MarkIntrinsicISizesDirty() override;
virtual void Reflow(nsPresContext* aPresContext, virtual void Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize, ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput, const ReflowInput& aReflowInput,
@ -194,6 +197,18 @@ protected:
const ReflowInput& aParentReflowInput, const ReflowInput& aParentReflowInput,
const FlexboxAxisTracker& aAxisTracker); const FlexboxAxisTracker& aAxisTracker);
/**
* This method gets a cached measuring reflow for a flex item, or does it and
* caches it.
*
* This avoids exponential reflows, see the comment on
* CachedMeasuringReflowResult.
*/
const CachedMeasuringReflowResult& MeasureAscentAndHeightForFlexItem(
FlexItem& aItem,
nsPresContext* aPresContext,
ReflowInput& aChildReflowInput);
/** /**
* This method performs a "measuring" reflow to get the content height of * This method performs a "measuring" reflow to get the content height of
* aFlexItem.Frame() (treating it as if it had auto-height), & returns the * aFlexItem.Frame() (treating it as if it had auto-height), & returns the