Skip to content

Commit

Permalink
Logbus: Don't pre-allocate 80k for each command (earthly#3482)
Browse files Browse the repository at this point in the history
While looking into BK's memory usage, I noticed some odd consumption
(below) when reviewing Earthly's `pprof` info.

It turns out that the previously used `circbuf` library was
[preallocating](https://github.com/armon/circbuf/blob/master/circbuf.go#L27)
~80K for each command. For large builds like ours, this can represent
100s of MBs (or even GBs).

I've added a custom circular buffer implementation that starts with an
empty buffer and grows to a maximum size before writing circularly. This
is massively reducing memory usage for some of our tests, like
`+test-no-qemu`.


![2023-11-09_14-04](https://github.com/earthly/earthly/assets/332408/ac81b474-24cc-409a-972d-d57ee7886d61)
  • Loading branch information
mikejholly authored Nov 10, 2023
1 parent 3b04a98 commit dd6ac1b
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 5 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
git.sr.ht/~nelsam/hel v0.4.6
github.com/adrg/xdg v0.4.0
github.com/alessio/shellescape v1.4.1
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/aws/aws-sdk-go-v2 v1.17.6
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230227212328-9f4511cd144a
github.com/containerd/containerd v1.7.7
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230219212500-1f9a474cc2dc h1:ikxgKfnYm4kXCOohe1uCkVFwZcABDZbVsqginko+GY8=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230219212500-1f9a474cc2dc/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.17.6 h1:Y773UK7OBqhzi5VDXMi1zVGsoj+CVHs2eaC2bDsLwi0=
Expand Down
2 changes: 1 addition & 1 deletion logbus/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"sync/atomic"
"time"

"github.com/armon/circbuf"
"github.com/earthly/cloud-api/logstream"
"github.com/earthly/earthly/util/circbuf"
"github.com/pkg/errors"
)

Expand Down
2 changes: 1 addition & 1 deletion outmon/vertexmon.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"strings"
"time"

"github.com/armon/circbuf"
"github.com/earthly/earthly/conslogging"
"github.com/earthly/earthly/util/circbuf"
"github.com/earthly/earthly/util/progressbar"
"github.com/earthly/earthly/util/vertexmeta"
"github.com/mattn/go-isatty"
Expand Down
75 changes: 75 additions & 0 deletions util/circbuf/circbuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package circbuf

import "errors"

// Buffer represents a dynamic circular (ring) buffer. It will append data
// freely to the underlying buffer until maxSize is hit. Once hit, new writes
// will be written in a circular fashion.
type Buffer struct {
data []byte
offset int
maxSize int
written int
}

// NewBuffer creates and returns a Buffer pointer with a max size.
func NewBuffer(maxSize int) (*Buffer, error) {
if maxSize < 0 {
return nil, errors.New("size must be a positive int")
}
return &Buffer{
maxSize: int(maxSize),
}, nil
}

// Write implements io.Writer.
func (c *Buffer) Write(buf []byte) (int, error) {
l := len(c.data)
n := len(buf)
c.written += n

if n > c.maxSize {
buf = buf[n-c.maxSize:]
}

if l < c.maxSize {
r := c.maxSize - l
if n > r {
c.data = append(c.data, buf[:r]...)
buf = buf[r:]
c.offset = 0
} else {
c.data = append(c.data, buf...)
c.offset = n
return n, nil
}
}

remain := c.maxSize - c.offset
copy(c.data[c.offset:], buf)
if len(buf) > remain {
copy(c.data, buf[remain:])
}

c.offset = (c.offset + len(buf)) % c.maxSize

return n, nil
}

func (c *Buffer) Bytes() []byte {
if len(c.data) < c.maxSize {
return c.data
}
ret := make([]byte, c.maxSize)
copy(ret, c.data[c.offset:])
copy(ret[c.maxSize-c.offset:], c.data[:c.offset])
return ret
}

func (c *Buffer) Size() int {
return len(c.data)
}

func (c *Buffer) TotalWritten() int {
return c.written
}
56 changes: 56 additions & 0 deletions util/circbuf/circbuf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package circbuf

import (
"io"
"testing"

"github.com/stretchr/testify/require"
)

func TestNewError(t *testing.T) {
_, err := NewBuffer(-1)
require.Error(t, err)
}

func TestWriteGrow(t *testing.T) {
b := &Buffer{maxSize: 5}
n, err := io.WriteString(b, "foo")
r := require.New(t)
r.NoError(err)
r.Equal(3, n)
r.Len(b.data, 3)
r.Equal("foo", string(b.Bytes()))
}

func TestWriteOverflow(t *testing.T) {
b := &Buffer{maxSize: 5}
n, err := io.WriteString(b, "foobarbaz")

r := require.New(t)
r.NoError(err)
r.Equal(9, n)
r.Equal("arbaz", string(b.data))
r.Equal("arbaz", string(b.Bytes()))
}

func TestWriteMulti(t *testing.T) {
b := &Buffer{maxSize: 5}
r := require.New(t)

n, err := io.WriteString(b, "mr")
r.NoError(err)
r.Equal(2, n)

n, err = io.WriteString(b, "world")
r.NoError(err)
r.Equal(5, n)

n, err = io.WriteString(b, "wide")
r.NoError(err)
r.Equal(4, n)

r.Equal("edwid", string(b.data))
r.Equal(1, b.offset)

r.Equal("dwide", string(b.Bytes()))
}

0 comments on commit dd6ac1b

Please sign in to comment.