layout: avoid heap escapes in Stack and Flex

Stack.Layout and Flex.Layout caused a lot of heap allocations / escapes. The
reason was that scratch space for dims and call and was inside Stack/FlexChild.
child.call.Add(gtx.Ops) confused the go escape analysis and caused the entired
children slice to escape to the heap, including all widgets in it. This caused a
lot of heap allocations. Now the scratch space is separate from children, and
for cases len(children) <= 32, we will allocate the scratch space on the stack.
For cases len(children) > 32, only the scratch space gets allocated from the
heap, during append.

Signed-off-by: vsariola <5684185+vsariola@users.noreply.github.com>
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-05-26 18:44:06 +03:00
parent 0a209f7d39
commit 1a17e9ea37
2 changed files with 46 additions and 26 deletions
+24 -14
View File
@@ -30,10 +30,6 @@ type FlexChild struct {
weight float32 weight float32
widget Widget widget Widget
// Scratch space.
call op.CallOp
dims Dimensions
} }
// Spacing determine the spacing mode for a Flex. // Spacing determine the spacing mode for a Flex.
@@ -88,6 +84,20 @@ func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
remaining := mainMax remaining := mainMax
var totalWeight float32 var totalWeight float32
cgtx := gtx cgtx := gtx
// Note: previously the scratch space was inside FlexChild.
// child.call.Add(gtx.Ops) confused the go escape analysis and caused the
// entired children slice to be allocated on the heap, including all widgets
// in it. This produced a lot of object allocations. Now the scratch space
// is separate from children, and for cases len(children) <= 32, we will
// allocate the scratch space on the stack. For cases len(children) > 32,
// only the scratch space gets allocated from the heap, during append.
type scratchSpace struct {
call op.CallOp
dims Dimensions
}
var scratchArray [32]scratchSpace
scratch := scratchArray[:0]
scratch = append(scratch, make([]scratchSpace, len(children))...)
// Lay out Rigid children. // Lay out Rigid children.
for i, child := range children { for i, child := range children {
if child.flex { if child.flex {
@@ -104,8 +114,8 @@ func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
if remaining < 0 { if remaining < 0 {
remaining = 0 remaining = 0
} }
children[i].call = c scratch[i].call = c
children[i].dims = dims scratch[i].dims = dims
} }
if w := f.WeightSum; w != 0 { if w := f.WeightSum; w != 0 {
totalWeight = w totalWeight = w
@@ -139,16 +149,16 @@ func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
if remaining < 0 { if remaining < 0 {
remaining = 0 remaining = 0
} }
children[i].call = c scratch[i].call = c
children[i].dims = dims scratch[i].dims = dims
} }
maxCross := crossMin maxCross := crossMin
var maxBaseline int var maxBaseline int
for _, child := range children { for _, scratchChild := range scratch {
if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross { if c := f.Axis.Convert(scratchChild.dims.Size).Y; c > maxCross {
maxCross = c maxCross = c
} }
if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { if b := scratchChild.dims.Size.Y - scratchChild.dims.Baseline; b > maxBaseline {
maxBaseline = b maxBaseline = b
} }
} }
@@ -169,8 +179,8 @@ func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
mainSize += space / (len(children) * 2) mainSize += space / (len(children) * 2)
} }
} }
for i, child := range children { for i, scratchChild := range scratch {
dims := child.dims dims := scratchChild.dims
b := dims.Size.Y - dims.Baseline b := dims.Size.Y - dims.Baseline
var cross int var cross int
switch f.Alignment { switch f.Alignment {
@@ -185,7 +195,7 @@ func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
} }
pt := f.Axis.Convert(image.Pt(mainSize, cross)) pt := f.Axis.Convert(image.Pt(mainSize, cross))
trans := op.Offset(pt).Push(gtx.Ops) trans := op.Offset(pt).Push(gtx.Ops)
child.call.Add(gtx.Ops) scratchChild.call.Add(gtx.Ops)
trans.Pop() trans.Pop()
mainSize += f.Axis.Convert(dims.Size).X mainSize += f.Axis.Convert(dims.Size).X
if i < len(children)-1 { if i < len(children)-1 {
+22 -12
View File
@@ -20,10 +20,6 @@ type Stack struct {
type StackChild struct { type StackChild struct {
expanded bool expanded bool
widget Widget widget Widget
// Scratch space.
call op.CallOp
dims Dimensions
} }
// Stacked returns a Stack child that is laid out with no minimum // Stacked returns a Stack child that is laid out with no minimum
@@ -52,6 +48,20 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
// First lay out Stacked children. // First lay out Stacked children.
cgtx := gtx cgtx := gtx
cgtx.Constraints.Min = image.Point{} cgtx.Constraints.Min = image.Point{}
// Note: previously the scratch space was inside StackChild.
// child.call.Add(gtx.Ops) confused the go escape analysis and caused the
// entired children slice to be allocated on the heap, including all widgets
// in it. This produced a lot of object allocations. Now the scratch space
// is separate from children, and for cases len(children) <= 32, we will
// allocate the scratch space on the stack. For cases len(children) > 32,
// only the scratch space gets allocated from the heap, during append.
type scratchSpace struct {
call op.CallOp
dims Dimensions
}
var scratchArray [32]scratchSpace
scratch := scratchArray[:0]
scratch = append(scratch, make([]scratchSpace, len(children))...)
for i, w := range children { for i, w := range children {
if w.expanded { if w.expanded {
continue continue
@@ -65,8 +75,8 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
if h := dims.Size.Y; h > maxSZ.Y { if h := dims.Size.Y; h > maxSZ.Y {
maxSZ.Y = h maxSZ.Y = h
} }
children[i].call = call scratch[i].call = call
children[i].dims = dims scratch[i].dims = dims
} }
// Then lay out Expanded children. // Then lay out Expanded children.
for i, w := range children { for i, w := range children {
@@ -83,14 +93,14 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
if h := dims.Size.Y; h > maxSZ.Y { if h := dims.Size.Y; h > maxSZ.Y {
maxSZ.Y = h maxSZ.Y = h
} }
children[i].call = call scratch[i].call = call
children[i].dims = dims scratch[i].dims = dims
} }
maxSZ = gtx.Constraints.Constrain(maxSZ) maxSZ = gtx.Constraints.Constrain(maxSZ)
var baseline int var baseline int
for _, ch := range children { for _, scratchChild := range scratch {
sz := ch.dims.Size sz := scratchChild.dims.Size
var p image.Point var p image.Point
switch s.Alignment { switch s.Alignment {
case N, S, Center: case N, S, Center:
@@ -105,10 +115,10 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
p.Y = maxSZ.Y - sz.Y p.Y = maxSZ.Y - sz.Y
} }
trans := op.Offset(p).Push(gtx.Ops) trans := op.Offset(p).Push(gtx.Ops)
ch.call.Add(gtx.Ops) scratchChild.call.Add(gtx.Ops)
trans.Pop() trans.Pop()
if baseline == 0 { if baseline == 0 {
if b := ch.dims.Baseline; b != 0 { if b := scratchChild.dims.Baseline; b != 0 {
baseline = b + maxSZ.Y - sz.Y - p.Y baseline = b + maxSZ.Y - sz.Y - p.Y
} }
} }