When mutating a byte slice, mutate in place, and only allocate once if
the slice's capacity is less than the maximum size.
mutateBytes already should not allocate; we check a post-condition
that the slice's data pointer does not change.
This speeds up the mutator from 4 ms per value to 200-600 ns. For
example:
goos: darwin
goarch: amd64
pkg: internal/fuzz
cpu: Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
BenchmarkMutatorBytes/1-8
5908735 275.3 ns/op
BenchmarkMutatorBytes/10-8
5198473 282.0 ns/op
BenchmarkMutatorBytes/100-8
4304750 233.9 ns/op
BenchmarkMutatorBytes/1000-8
4623988 295.2 ns/op
BenchmarkMutatorBytes/10000-8
4252104 458.5 ns/op
BenchmarkMutatorBytes/100000-8
1236751 950.8 ns/op
PASS
ok internal/fuzz 12.993s
Change-Id: I4bf2a04be6c648ef440af2c62bf0ffa3d310172c
Reviewed-on: https://go-review.googlesource.com/c/go/+/306675
Trust: Jay Conrod <jayconrod@google.com>
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Jay Conrod <jayconrod@google.com>
Reviewed-by: Katie Hockman <katie@golang.org>
parallel = runtime.GOMAXPROCS(0)
}
- sharedMemSize := 100 << 20 // 100 MB
// Make sure all of the seed corpus has marshalled data.
for i := range seed {
if seed[i].Data == nil {
errC := make(chan error)
newWorker := func() (*worker, error) {
- mem, err := sharedMemTempFile(sharedMemSize)
+ mem, err := sharedMemTempFile(workerSharedMemSize)
if err != nil {
return nil, err
}
}
// mutate performs several mutations on the provided values.
-func (m *mutator) mutate(vals []interface{}, maxBytes int) []interface{} {
- // TODO(jayconrod,katiehockman): use as few allocations as possible
+func (m *mutator) mutate(vals []interface{}, maxBytes int) {
// TODO(katiehockman): pull some of these functions into helper methods and
// test that each case is working as expected.
// TODO(katiehockman): perform more types of mutations.
if len(v) > maxPerVal {
panic(fmt.Sprintf("cannot mutate bytes of length %d", len(v)))
}
- b := make([]byte, 0, maxPerVal)
- b = append(b, v...)
- m.mutateBytes(&b)
- vals[i] = b
- return vals
+ if cap(v) < maxPerVal {
+ v = append(make([]byte, 0, maxPerVal), v...)
+ }
+ m.mutateBytes(&v)
+ vals[i] = v
default:
panic(fmt.Sprintf("type not supported for mutating: %T", vals[i]))
}
--- /dev/null
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fuzz
+
+import (
+ "strconv"
+ "testing"
+)
+
+func BenchmarkMutatorBytes(b *testing.B) {
+ for _, size := range []int{
+ 1,
+ 10,
+ 100,
+ 1000,
+ 10000,
+ 100000,
+ } {
+ size := size
+ b.Run(strconv.Itoa(size), func(b *testing.B) {
+ vals := []interface{}{make([]byte, size)}
+ m := newMutator()
+ for i := 0; i < b.N; i++ {
+ m.mutate(vals, workerSharedMemSize)
+ }
+ })
+ }
+}
// This distinguishes internal errors from uncontrolled panics and other crashes.
// Keep in sync with internal/fuzz.workerExitCode.
workerExitCode = 70
+
+ // workerSharedMemSize is the maximum size of the shared memory file used to
+ // communicate with workers. This limits the size of fuzz inputs.
+ workerSharedMemSize = 100 << 20 // 100 MB
)
// worker manages a worker process running a test binary. The worker object
// real heuristic once we have one.
return fuzzResponse{Interesting: true}
default:
- vals = ws.m.mutate(vals, cap(mem.valueRef()))
+ ws.m.mutate(vals, cap(mem.valueRef()))
writeToMem(vals, mem)
if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
if minErr := ws.minimize(ctx, vals, mem); minErr != nil {