Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/ext/slicesx/slicesx.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ func One[E any](p *E) []E {
return unsafe.Slice(p, 1)
}

// New returns a new slice with at least the given length.
func New[S ~[]E, E any](count int) []E {
// Append will always round up to a size class for us.
s := append(S(nil), make(S, count)...)[:]
return s[:cap(s)]
}

// Get performs a bounds check and returns the value at idx.
//
// If the bounds check fails, returns the zero value and false.
Expand Down
50 changes: 50 additions & 0 deletions internal/ext/synctestx/synctestx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package synctestx

import (
"runtime"
"sync"
)

// Hammer runs f across count goroutines, ensuring that f is called
// simultaneously, simulating a thundering herd. Returns once all spawned
// goroutines have exited.
//
// If count is zero, uses GOMAXPROCS instead.
func Hammer(count int, f func()) {
if count == 0 {
count = runtime.GOMAXPROCS(0)
}

start := new(sync.WaitGroup)
end := new(sync.WaitGroup)
for range count {
start.Add(1)
end.Add(1)
go func() {
defer end.Done()

// This ensures that we have a thundering herd situation: all of
// these goroutines wake up and hammer f() at the same time.
start.Done()
start.Wait()

f()
}()
}

end.Wait()
}
117 changes: 117 additions & 0 deletions internal/ext/syncx/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//nolint:revive,predeclared
package syncx

import (
"errors"
"math"
"runtime"
"sync/atomic"
"testing"
"unsafe"
)

var ErrLogExhausted = errors.New("internal/syncx: cannot allocate more than 2^31 elements")

// Log is an append-only log.
//
// Loading and append operations may happen concurrently with each other.
// Can hold at most 2^31 elements.
type Log[T any] struct {
ptr atomic.Pointer[T]

next, len atomic.Int32 // The next index to fill.
cap atomic.Int32 // Top bit is used as a spinlock.
}

// Load returns the value at the given index.
//
// Panics if no value is at that index.
func (s *Log[T]) Load(idx int) T {
// Read cap first, which is required before we can read s.ptr.
len := s.len.Load()
ptr := s.ptr.Load()

return unsafe.Slice(ptr, len)[idx]
}

// Append adds a new value to this slice.
//
// Returns the index of the appended element, which can be looked up with
// [Log.Load].
//
// Returns an error if indices are exhausted.
func (s *Log[T]) Append(v T) (int, error) {
i := s.next.Add(1)
if i < 0 {
return 0, ErrLogExhausted
}
i--

// Wait for the capacity to be large enough for our index, or for us to
// be responsible for growing it (i == c).
c := s.cap.Load()
for i > c {
runtime.Gosched()
c = s.cap.Load()
}

// Fast path (i < c): slice is already large enough.
if i < c {
p := s.ptr.Load()
unsafe.Slice(p, c)[i] = v

s.len.Add(1)
for s.len.Load() <= i {
// Make sure that every index before us also completes, to ensure
// that Load does not panic.
runtime.Gosched()
}

return int(i), nil
}

// Slow path (i == c): we are responsible for growing the slice. Need to
// wait until all fast-path writers to finish. Any further writers will
// spin in the i > c loop.
for s.len.Load() != c {
runtime.Gosched()
}

// Grow the slice.
// i == c, so we are appending exactly one element right now.
p := s.ptr.Load()
slice := append(unsafe.Slice(p, c), v)

// Publish the new slice to readers and waiting writers.
// Pointer must be stored before capacity to prevent out-of-bounds panics in Load.
s.ptr.Store(unsafe.SliceData(slice))
s.cap.Store(int32(cap(slice)))

s.len.Add(1)
return int(i), nil
}

// SetFullForTesting sets this log to full, so that future calls to [Log.Append]
// panic.
//
// Must not be called concurrently. Can only be called from a unit test.
func (s *Log[T]) SetFullForTesting() {
if !testing.Testing() {
panic("called SetFull outside of a test")
}
s.next.Store(math.MaxInt32)
}
54 changes: 54 additions & 0 deletions internal/ext/syncx/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package syncx_test

import (
"math/rand"
"testing"

"github.com/stretchr/testify/assert"

"github.com/bufbuild/protocompile/internal/ext/synctestx"
"github.com/bufbuild/protocompile/internal/ext/syncx"
)

func TestLog(t *testing.T) {
t.Parallel()

const trials = 1000

log := new(syncx.Log[int])
synctestx.Hammer(0, func() {
for range trials {
n := rand.Int()
i, _ := log.Append(n)
assert.Equal(t, n, log.Load(i))
}
})

// Verify that mis-using an index panics.
i, _ := log.Append(0)
assert.Panics(t, func() { log.Load(i + 1) })
}

func TestExhaust(t *testing.T) {
t.Parallel()

log := new(syncx.Log[int])
log.SetFullForTesting()

_, err := log.Append(0)
assert.Error(t, err)
}
80 changes: 80 additions & 0 deletions internal/inlinetest/inlinetest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package inlinetest

import (
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"runtime/debug"
"strings"
"testing"

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

// AssertInlined returns whether the compiler is willing to inline the given
// symbols in the package being tested.
//
// The symbols must be either a single identifier or of the form Type.Method.
// Pointer-receiver methods should not use the (*Type).Method syntax.
func AssertInlined(t *testing.T, symbols ...string) {
t.Helper()
for _, symbol := range symbols {
_, ok := inlined[symbol]
assert.True(t, ok, "%s is not inlined", symbol)
}
}

var inlined = make(map[string]struct{})

func init() {
if !testing.Testing() {
panic("inlinetest: cannot import inlinetest except in a test")
}

// This is based on a pattern of tests appearing in several places in Go's
// standard library.
tool := "go"
if env, ok := os.LookupEnv("GO"); ok {
tool = env
}

info, ok := debug.ReadBuildInfo()
if !ok {
panic(errors.New("inlinetest: could not read build info"))
}

//nolint:gosec
out, err := exec.Command(
tool,
"build",
"--gcflags=-m", // -m records optimization decisions.
strings.TrimSuffix(info.Path, ".test"),
).CombinedOutput()
if err != nil {
panic(fmt.Errorf("inlinetest: go build failed: %w, %s", err, out))
}

remarkRe := regexp.MustCompile(`(?m)^\./\S+\.go:\d+:\d+: can inline (.+?)$`)
ptrRe := regexp.MustCompile(`\(\*(.+)\)\.`)
for _, match := range remarkRe.FindAllSubmatch(out, -1) {
match := string(match[1])
match = ptrRe.ReplaceAllString(match, "$1.")
inlined[match] = struct{}{}
}
}
29 changes: 14 additions & 15 deletions internal/intern/char6.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ var (
}()
)

type inlined [maxInlined]byte

// encodeChar6 attempts to encoding data using the char6 encoding. Returns
// whether encoding was successful, and an encoded value.
func encodeChar6(data string) (ID, bool) {
Expand Down Expand Up @@ -79,30 +81,27 @@ func encodeOutlined(data string) (ID, bool) {
return value, true
}

// decodeChar6 decodes id assuming it contains a char6-encoded string.
func decodeChar6(id ID) string {
// The main decoding loop is outlined to promote inlining of decodeChar6,
// and thus heap-promotion of the returned string.
data, len := decodeOutlined(id) //nolint:predeclared,revive // For `len`.
return unsafex.StringAlias(data[:len])
}
// decodeChar6 decodes id assuming it contains a char6-encoded string, and
// writes the result to buf.
func decodeChar6(id ID, buf *inlined) string {
if id == 0 {
return ""
}

//nolint:predeclared,revive // For `len`.
func decodeOutlined(id ID) (data [maxInlined]byte, len int) {
for i := range data {
data[i] = char6ToByte[int(id&077)]
for i := range buf {
buf[i] = char6ToByte[int(id&077)]
id >>= 6
}

// Figure out the length by removing a maximal suffix of
// '.' bytes. Note that an all-ones value will decode to "", but encode
// will never return that value.
len = maxInlined
for ; len > 0; len-- {
if data[len-1] != '.' {
n := maxInlined
for ; n > 0; n-- {
if buf[n-1] != '.' {
break
}
}

return data, len
return unsafex.StringAlias(buf[:n])
}
Loading
Loading