blob: 9172016635b0d854c9643bf32b2bbb0232cfb989 [file] [log] [blame] [edit]
// Copyright 2022 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 sync_test
import (
"bytes"
"math"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"testing"
_ "unsafe"
)
// We assume that the Once.Do tests have already covered parallelism.
func TestOnceFunc(t *testing.T) {
calls := 0
of := func() { calls++ }
f := sync.OnceFunc(of)
allocs := testing.AllocsPerRun(10, f)
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call to f, got %v", allocs)
}
allocs = testing.AllocsPerRun(10, func() {
f = sync.OnceFunc(of)
})
if allocs > 2 {
t.Errorf("want at most 2 allocations per call to OnceFunc, got %v", allocs)
}
}
func TestOnceValue(t *testing.T) {
calls := 0
of := func() int {
calls++
return calls
}
f := sync.OnceValue(of)
allocs := testing.AllocsPerRun(10, func() { f() })
value := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if value != 1 {
t.Errorf("want value==1, got %d", value)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call to f, got %v", allocs)
}
allocs = testing.AllocsPerRun(10, func() {
f = sync.OnceValue(of)
})
if allocs > 2 {
t.Errorf("want at most 2 allocations per call to OnceValue, got %v", allocs)
}
}
func TestOnceValues(t *testing.T) {
calls := 0
of := func() (int, int) {
calls++
return calls, calls + 1
}
f := sync.OnceValues(of)
allocs := testing.AllocsPerRun(10, func() { f() })
v1, v2 := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if v1 != 1 || v2 != 2 {
t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call to f, got %v", allocs)
}
allocs = testing.AllocsPerRun(10, func() {
f = sync.OnceValues(of)
})
if allocs > 2 {
t.Errorf("want at most 2 allocations per call to OnceValues, got %v", allocs)
}
}
func testOncePanicX(t *testing.T, calls *int, f func()) {
testOncePanicWith(t, calls, f, func(label string, p any) {
if p != "x" {
t.Fatalf("%s: want panic %v, got %v", label, "x", p)
}
})
}
func testOncePanicWith(t *testing.T, calls *int, f func(), check func(label string, p any)) {
// Check that the each call to f panics with the same value, but the
// underlying function is only called once.
for _, label := range []string{"first time", "second time"} {
var p any
panicked := true
func() {
defer func() {
p = recover()
}()
f()
panicked = false
}()
if !panicked {
t.Fatalf("%s: f did not panic", label)
}
check(label, p)
}
if *calls != 1 {
t.Errorf("want calls==1, got %d", *calls)
}
}
func TestOnceFuncPanic(t *testing.T) {
calls := 0
f := sync.OnceFunc(func() {
calls++
panic("x")
})
testOncePanicX(t, &calls, f)
}
func TestOnceValuePanic(t *testing.T) {
calls := 0
f := sync.OnceValue(func() int {
calls++
panic("x")
})
testOncePanicX(t, &calls, func() { f() })
}
func TestOnceValuesPanic(t *testing.T) {
calls := 0
f := sync.OnceValues(func() (int, int) {
calls++
panic("x")
})
testOncePanicX(t, &calls, func() { f() })
}
func TestOnceFuncPanicNil(t *testing.T) {
calls := 0
f := sync.OnceFunc(func() {
calls++
panic(nil)
})
testOncePanicWith(t, &calls, f, func(label string, p any) {
switch p.(type) {
case nil, *runtime.PanicNilError:
return
}
t.Fatalf("%s: want nil panic, got %v", label, p)
})
}
func TestOnceFuncGoexit(t *testing.T) {
// If f calls Goexit, the results are unspecified. But check that f doesn't
// get called twice.
calls := 0
f := sync.OnceFunc(func() {
calls++
runtime.Goexit()
})
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer func() { recover() }()
f()
}()
wg.Wait()
}
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
}
func TestOnceFuncPanicTraceback(t *testing.T) {
// Test that on the first invocation of a OnceFunc, the stack trace goes all
// the way to the origin of the panic.
f := sync.OnceFunc(onceFuncPanic)
defer func() {
if p := recover(); p != "x" {
t.Fatalf("want panic %v, got %v", "x", p)
}
stack := debug.Stack()
want := "sync_test.onceFuncPanic"
if !bytes.Contains(stack, []byte(want)) {
t.Fatalf("want stack containing %v, got:\n%s", want, string(stack))
}
}()
f()
}
func onceFuncPanic() {
panic("x")
}
func TestOnceXGC(t *testing.T) {
fns := map[string]func([]byte) func(){
"OnceFunc": func(buf []byte) func() {
return sync.OnceFunc(func() { buf[0] = 1 })
},
"OnceValue": func(buf []byte) func() {
f := sync.OnceValue(func() any { buf[0] = 1; return nil })
return func() { f() }
},
"OnceValues": func(buf []byte) func() {
f := sync.OnceValues(func() (any, any) { buf[0] = 1; return nil, nil })
return func() { f() }
},
"OnceFunc panic": func(buf []byte) func() {
return sync.OnceFunc(func() { buf[0] = 1; panic("test panic") })
},
"OnceValue panic": func(buf []byte) func() {
f := sync.OnceValue(func() any { buf[0] = 1; panic("test panic") })
return func() { f() }
},
"OnceValues panic": func(buf []byte) func() {
f := sync.OnceValues(func() (any, any) { buf[0] = 1; panic("test panic") })
return func() { f() }
},
}
for n, fn := range fns {
t.Run(n, func(t *testing.T) {
buf := make([]byte, 1024)
var gc atomic.Bool
runtime.AddCleanup(&buf[0], func(g *atomic.Bool) { g.Store(true) }, &gc)
f := fn(buf)
runCleanups()
if gc.Load() != false {
t.Fatal("wrapped function garbage collected too early")
}
func() {
defer func() { recover() }()
f()
}()
runCleanups()
if gc.Load() != true {
// Even if f is still alive, the function passed to Once(Func|Value|Values)
// is not kept alive after the first call to f.
t.Fatal("wrapped function should be garbage collected, but still live")
}
func() {
defer func() { recover() }()
f()
}()
})
}
}
// runCleanups performs garbage collection and waits for all cleanups to run.
func runCleanups() {
runtime.GC()
runtime_blockUntilEmptyCleanupQueue(math.MaxInt64)
}
//go:linkname runtime_blockUntilEmptyCleanupQueue
func runtime_blockUntilEmptyCleanupQueue(int64) bool
var (
onceFunc = sync.OnceFunc(func() {})
onceFuncOnce sync.Once
onceFuncFunc func()
)
func doOnceFunc() {
onceFuncOnce.Do(func() {})
}
func BenchmarkOnceFunc(b *testing.B) {
b.Run("v=Once", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// The baseline is direct use of sync.Once.
doOnceFunc()
}
})
b.Run("v=Global", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// As of 3/2023, the compiler doesn't recognize that onceFunc is
// never mutated and is a closure that could be inlined.
// Too bad, because this is how OnceFunc will usually be used.
onceFunc()
}
})
b.Run("v=Local", func(b *testing.B) {
b.ReportAllocs()
// As of 3/2023, the compiler *does* recognize this local binding as an
// inlinable closure. This is the best case for OnceFunc, but probably
// not typical usage.
f := sync.OnceFunc(func() {})
for i := 0; i < b.N; i++ {
f()
}
})
b.Run("v=Make", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
onceFuncFunc = sync.OnceFunc(func() {})
}
})
}
var (
onceValue = sync.OnceValue(func() int { return 42 })
onceValueOnce sync.Once
onceValueValue int
onceValueFunc func() int
)
func doOnceValue() int {
onceValueOnce.Do(func() {
onceValueValue = 42
})
return onceValueValue
}
func BenchmarkOnceValue(b *testing.B) {
// See BenchmarkOnceFunc
b.Run("v=Once", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if want, got := 42, doOnceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
b.Run("v=Global", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if want, got := 42, onceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
b.Run("v=Local", func(b *testing.B) {
b.ReportAllocs()
onceValue := sync.OnceValue(func() int { return 42 })
for i := 0; i < b.N; i++ {
if want, got := 42, onceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
b.Run("v=Make", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
onceValueFunc = sync.OnceValue(func() int { return 42 })
}
})
}
const (
onceValuesWant1 = 42
onceValuesWant2 = true
)
var (
onceValues = sync.OnceValues(func() (int, bool) {
return onceValuesWant1, onceValuesWant2
})
onceValuesOnce sync.Once
onceValuesValue1 int
onceValuesValue2 bool
onceValuesFunc func() (int, bool)
)
func doOnceValues() (int, bool) {
onceValuesOnce.Do(func() {
onceValuesValue1 = onceValuesWant1
onceValuesValue2 = onceValuesWant2
})
return onceValuesValue1, onceValuesValue2
}
func BenchmarkOnceValues(b *testing.B) {
// See BenchmarkOnceFunc
b.Run("v=Once", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if got1, got2 := doOnceValues(); got1 != onceValuesWant1 {
b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
} else if got2 != onceValuesWant2 {
b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
}
}
})
b.Run("v=Global", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if got1, got2 := onceValues(); got1 != onceValuesWant1 {
b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
} else if got2 != onceValuesWant2 {
b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
}
}
})
b.Run("v=Local", func(b *testing.B) {
b.ReportAllocs()
onceValues := sync.OnceValues(func() (int, bool) {
return onceValuesWant1, onceValuesWant2
})
for i := 0; i < b.N; i++ {
if got1, got2 := onceValues(); got1 != onceValuesWant1 {
b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
} else if got2 != onceValuesWant2 {
b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
}
}
})
b.Run("v=Make", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
onceValuesFunc = sync.OnceValues(func() (int, bool) {
return onceValuesWant1, onceValuesWant2
})
}
})
}