diff --git a/internal/ext/slicesx/slicesx.go b/internal/ext/slicesx/slicesx.go index 1a20b1ba..3fdd4e47 100644 --- a/internal/ext/slicesx/slicesx.go +++ b/internal/ext/slicesx/slicesx.go @@ -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. diff --git a/internal/ext/synctestx/synctestx.go b/internal/ext/synctestx/synctestx.go new file mode 100644 index 00000000..ad9128e6 --- /dev/null +++ b/internal/ext/synctestx/synctestx.go @@ -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() +} diff --git a/internal/ext/syncx/log.go b/internal/ext/syncx/log.go new file mode 100644 index 00000000..2cb6883f --- /dev/null +++ b/internal/ext/syncx/log.go @@ -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) +} diff --git a/internal/ext/syncx/log_test.go b/internal/ext/syncx/log_test.go new file mode 100644 index 00000000..ac01e205 --- /dev/null +++ b/internal/ext/syncx/log_test.go @@ -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) +} diff --git a/internal/inlinetest/inlinetest.go b/internal/inlinetest/inlinetest.go new file mode 100644 index 00000000..50fb3d23 --- /dev/null +++ b/internal/inlinetest/inlinetest.go @@ -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{}{} + } +} diff --git a/internal/intern/char6.go b/internal/intern/char6.go index 9c248244..5623b6e6 100644 --- a/internal/intern/char6.go +++ b/internal/intern/char6.go @@ -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) { @@ -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]) } diff --git a/internal/intern/container.go b/internal/intern/container.go new file mode 100644 index 00000000..6f870804 --- /dev/null +++ b/internal/intern/container.go @@ -0,0 +1,75 @@ +// 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 intern + +import "github.com/bufbuild/protocompile/internal/ext/mapsx" + +// Set is a set of intern IDs. +type Set map[ID]struct{} + +// ContainsID returns whether s contains the given ID. +func (s Set) ContainsID(id ID) bool { + _, ok := s[id] + return ok +} + +// Contains returns whether s contains the given string. +func (s Set) Contains(table *Table, key string) bool { + k, ok := table.Query(key) + if !ok { + return false + } + _, ok = s[k] + return ok +} + +// AddID adds an ID to s, and returns whether it was added. +func (s Set) AddID(id ID) (inserted bool) { + return mapsx.AddZero(s, id) +} + +// Add adds a string to s, and returns whether it was added. +func (s Set) Add(table *Table, key string) (inserted bool) { + k := table.Intern(key) + _, ok := s[k] + if !ok { + s[k] = struct{}{} + } + return !ok +} + +// Map is a map keyed by intern IDs. +type Map[T any] map[ID]T + +// Get returns the value that key maps to. +func (m Map[T]) Get(table *Table, key string) (T, bool) { + k, ok := table.Query(key) + if !ok { + var z T + return z, false + } + v, ok := m[k] + return v, ok +} + +// AddID adds an ID to m, and returns whether it was added. +func (m Map[T]) AddID(id ID, v T) (mapped T, inserted bool) { + return mapsx.Add(m, id, v) +} + +// Add adds a string to m, and returns whether it was added. +func (m Map[T]) Add(table *Table, key string, v T) (mapped T, inserted bool) { + return m.AddID(table.Intern(key), v) +} diff --git a/internal/intern/export_test.go b/internal/intern/export_test.go new file mode 100644 index 00000000..20644aad --- /dev/null +++ b/internal/intern/export_test.go @@ -0,0 +1,21 @@ +// 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 intern + +import "github.com/bufbuild/protocompile/internal/ext/syncx" + +func (t *Table) Table() *syncx.Log[string] { + return &t.table +} diff --git a/internal/intern/intern.go b/internal/intern/intern.go index 310bd63c..7d70ea22 100644 --- a/internal/intern/intern.go +++ b/internal/intern/intern.go @@ -19,12 +19,13 @@ package intern import ( "fmt" "reflect" + "runtime" "strings" "sync" "sync/atomic" "github.com/bufbuild/protocompile/internal/ext/bitsx" - "github.com/bufbuild/protocompile/internal/ext/mapsx" + "github.com/bufbuild/protocompile/internal/ext/syncx" "github.com/bufbuild/protocompile/internal/ext/unsafex" ) @@ -57,7 +58,7 @@ func (id ID) String() string { return `intern.ID("")` } if id < 0 { - return fmt.Sprintf("intern.ID(%q)", decodeChar6(id)) + return fmt.Sprintf("intern.ID(%q)", decodeChar6(id, new(inlined))) } return fmt.Sprintf("intern.ID(%d)", int(id)) } @@ -73,10 +74,8 @@ func (id ID) GoString() string { // // The zero value of Table is empty and ready to use. type Table struct { - mu sync.RWMutex - index map[string]ID - table []string - + index sync.Map // [string, atomic.Int32] + table syncx.Log[string] stats atomic.Pointer[stats] } @@ -192,16 +191,28 @@ func (t *Table) Query(s string) (ID, bool) { return char6, true } - t.mu.RLock() - id, ok := t.index[s] - t.mu.RUnlock() - + v, ok := t.index.Load(s) if stats != nil { stats.queries.Add(1) stats.queryBytes.Add(int64(len(s))) } - return id, ok + if !ok { + return 0, false + } + + p := v.(*atomic.Int32) //nolint:errcheck + if p == nil { + // This key has been poisoned because we ran out of entries. + return 0, false + } + id := ID(p.Load()) + if id == 0 { + // Handle the case where this is a mid-insertion. + return 0, false + } + + return id, true } func (t *Table) internSlow(s string) ID { @@ -212,34 +223,44 @@ func (t *Table) internSlow(s string) ID { // a []byte as a string temporarily for querying the intern table. s = strings.Clone(s) - t.mu.Lock() - defer t.mu.Unlock() + // Pre-convert to `any`, since this triggers an allocation via + // `runtime.convTstring`. + key := any(s) + +again: + // Try to become the "leader" which is interning s. Insert a 0, which is + // "" (never interned), to mark this slot as taken. + v, loaded := t.index.LoadOrStore(key, new(atomic.Int32)) + p := v.(*atomic.Int32) //nolint:errcheck + if loaded { + if p == nil { + // We ran out of IDs for this key. + panic(syncx.ErrLogExhausted) + } - // Check if someone raced us to intern this string. We have to check again - // because in the unsynchronized section between RUnlock and Lock, another - // goroutine might have successfully interned s. - // - // TODO: We can reduce the number of map hits if we switch to a different - // Map implementation that provides an upsert primitive. - if id, ok := t.index[s]; ok { + id := ID(p.Load()) + if id == 0 { + // Someone *else* is doing the inserting, apparently. + runtime.Gosched() + goto again + } + + // Someone else already inserted, we'de done. return id } - // As of here, we have unique ownership of the table, and s has not been - // inserted yet. - - t.table = append(t.table, s) - - // The first ID will have value 1. ID 0 is reserved for "". - id := ID(len(t.table)) - if id < 0 { - panic(fmt.Sprintf("internal/intern: %d interning IDs exhausted", len(t.table))) + // Figure out the next interning ID. + i, err := t.table.Append(s) + if err != nil { + // Poison this key. This will cause any goroutines waiting for interning + // to complete to also panic. + t.index.Store(key, (*atomic.Int32)(nil)) + panic(err) } - if t.index == nil { - t.index = make(map[string]ID) - } - t.index[s] = id + // Commit the new ID. + id := ID(i + 1) + p.Store(int32(id)) return id } @@ -274,18 +295,18 @@ func (t *Table) QueryBytes(bytes []byte) (ID, bool) { // // This function may be called by multiple goroutines concurrently. func (t *Table) Value(id ID) string { - if id == 0 { - return "" - } + // NOTE: this function is carefully written such that Go inlines it into + // the caller, allowing the result to be promoted to the stack. + return t.value(id, new(inlined)) +} - if id < 0 { - return decodeChar6(id) +//go:noinline +func (t *Table) value(id ID, buf *inlined) string { + if id <= 0 { + return decodeChar6(id, buf) } - // The locking part of Get is outlined to promote inlining of the two - // fast paths above. This in turn allows decodeChar6 to be inlined, which - // allows the returned string to be stack-promoted. - return t.getSlow(id) + return t.table.Load(int(id) - 1) } // Preload takes a pointer to a struct type and initializes [ID]-typed fields @@ -309,67 +330,3 @@ func (t *Table) Preload(ids any) { } } } - -func (t *Table) getSlow(id ID) string { - t.mu.RLock() - defer t.mu.RUnlock() - return t.table[int(id)-1] -} - -// Set is a set of intern IDs. -type Set map[ID]struct{} - -// ContainsID returns whether s contains the given ID. -func (s Set) ContainsID(id ID) bool { - _, ok := s[id] - return ok -} - -// Contains returns whether s contains the given string. -func (s Set) Contains(table *Table, key string) bool { - k, ok := table.Query(key) - if !ok { - return false - } - _, ok = s[k] - return ok -} - -// AddID adds an ID to s, and returns whether it was added. -func (s Set) AddID(id ID) (inserted bool) { - return mapsx.AddZero(s, id) -} - -// Add adds a string to s, and returns whether it was added. -func (s Set) Add(table *Table, key string) (inserted bool) { - k := table.Intern(key) - _, ok := s[k] - if !ok { - s[k] = struct{}{} - } - return !ok -} - -// Map is a map keyed by intern IDs. -type Map[T any] map[ID]T - -// Get returns the value that key maps to. -func (m Map[T]) Get(table *Table, key string) (T, bool) { - k, ok := table.Query(key) - if !ok { - var z T - return z, false - } - v, ok := m[k] - return v, ok -} - -// AddID adds an ID to m, and returns whether it was added. -func (m Map[T]) AddID(id ID, v T) (mapped T, inserted bool) { - return mapsx.Add(m, id, v) -} - -// Add adds a string to m, and returns whether it was added. -func (m Map[T]) Add(table *Table, key string, v T) (mapped T, inserted bool) { - return m.AddID(table.Intern(key), v) -} diff --git a/internal/intern/intern_test.go b/internal/intern/intern_test.go index b47c8ac2..5f7770c9 100644 --- a/internal/intern/intern_test.go +++ b/internal/intern/intern_test.go @@ -16,11 +16,20 @@ package intern_test import ( "fmt" + "math/rand" + "runtime" + "slices" "strings" + "sync" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" + "github.com/bufbuild/protocompile/internal/ext/slicesx" + "github.com/bufbuild/protocompile/internal/ext/synctestx" + "github.com/bufbuild/protocompile/internal/ext/syncx" + "github.com/bufbuild/protocompile/internal/inlinetest" "github.com/bufbuild/protocompile/internal/intern" ) @@ -75,3 +84,137 @@ func shouldInline(s string) bool { return true } + +func TestInline(t *testing.T) { + t.Parallel() + inlinetest.AssertInlined(t, "Table.Value") +} + +func TestHammer(t *testing.T) { + t.Parallel() + + n := new(atomic.Int64) + it := new(intern.Table) + + // We collect the results of every query to the table, and then ensure + // each gets a unique answer. + mu := new(sync.Mutex) + query := make(map[string][]intern.ID) + value := make(map[intern.ID][]string) + + synctestx.Hammer(0, func() { + data := makeData(int(n.Add(1))) + m1 := make(map[string][]intern.ID) + m2 := make(map[intern.ID][]string) + + for _, s := range data { + s := string(s) + id := it.Intern(s) + m1[s] = append(m1[s], id) + + v := it.Value(id) + m2[id] = append(m2[id], v) + + assert.Equal(t, s, v) + } + + mu.Lock() + defer mu.Unlock() + for k, v := range m1 { + query[k] = append(query[k], v...) + } + for k, v := range m2 { + value[k] = append(value[k], v...) + } + }) + + for k, v := range query { + slices.Sort(v) + v = slicesx.Dedup(v) + assert.Len(t, v, 1, "query[%v]: %v", k, v) + } + + for k, v := range value { + slices.Sort(v) + v = slicesx.Dedup(v) + assert.Len(t, v, 1, "value[%v]: %v", k, v) + } +} + +func TestExhaust(t *testing.T) { + t.Parallel() + + // Validate that if IDs are exhausted, every thread potentially waiting on + // that panics and does not hang. + it := new(intern.Table) + it.Table().SetFullForTesting() + synctestx.Hammer(0, func() { + defer func() { assert.Equal(t, syncx.ErrLogExhausted, recover()) }() + it.Intern("uh oh") + }) +} + +func BenchmarkIntern(b *testing.B) { + // Helper to ensure that it.Value is actually inlined, which is relevant + // for benchmarks. Calls within the body of a benchmark are never inlined. + // + // Returns the length of the string to ensure that this function is not + // DCE'd. + value := func(it *intern.Table, id intern.ID) int { + return len(it.Value(id)) + } + + run := func(name string, unique float64) { + b.Run(name, func(b *testing.B) { + // Pre-allocate data samples for each goroutine. + data := make([][][]byte, runtime.GOMAXPROCS(0)) + for i := range data { + data[i] = makeData(i) + } + + n := new(atomic.Int64) + it := new(intern.Table) + b.RunParallel(func(p *testing.PB) { + n := n.Add(1) - 1 + data := data[n] + r := rand.New(rand.NewSource(n)) + + for p.Next() { + for i, s := range data { + if r.Float64() < unique { + s = append(s, '0') + data[i] = s + } + + _ = value(it, it.InternBytes(s)) + } + } + }) + }) + } + + run("0pct", 0.0) + run("10pct", 0.1) + run("50pct", 0.5) + run("100pct", 1.0) +} + +// makeData generates deterministic pseudo-random data of poor quality, meaning +// that strings are likely to repeat in different orders across different +// seeds. +func makeData(seed int) [][]byte { + data := make([][]byte, 10000) + n := seed + r := rand.New(rand.NewSource(int64(seed))) + for i := range data { + n += 5 + n %= 99 + + buf := make([]byte, n, 10000) + for i := range buf { + buf[i] = byte('a' + r.Intn(26)) + } + data[i] = buf + } + return data +}