mirror of
https://github.com/golang/go
synced 2024-09-05 16:04:50 +00:00
sync: move lock linearity test and treat it like a performance test
This change moves test/locklinear.go into the sync package tests, and adds a bit of infrastructure since there are other linearity-checking tests that could benefit from it too. This infrastructure is also different than what test/locklinear.go does: instead of trying really hard to get at least one success, we instead treat this like a performance test and look for a significant difference via a t-test. This makes the methodology behind the tests more rigorous, and should reduce flakiness as transient noise should produce an insignificant result. A follow-up CL does more to make these tests even more robust. For #32986. Change-Id: I408c5f643962b70ea708930edb4ac9df1c6123ce Reviewed-on: https://go-review.googlesource.com/c/go/+/411396 Reviewed-by: Michael Pratt <mpratt@google.com>
This commit is contained in:
parent
6130461149
commit
1fe2810f9c
|
@ -16,6 +16,7 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"internal/cfg"
|
||||
"internal/testmath"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -463,3 +464,67 @@ func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) {
|
|||
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
// CheckLinear checks if the function produced by f scales linearly.
|
||||
//
|
||||
// f must accept a scale factor which causes the input to the function it
|
||||
// produces to scale by that factor.
|
||||
func CheckLinear(t *testing.T, f func(scale float64) func(*testing.B)) {
|
||||
MustHaveExec(t)
|
||||
|
||||
if os.Getenv("GO_PERF_UNIT_TEST") == "" {
|
||||
// Invoke the same test as a subprocess with the GO_PERF_UNIT_TEST environment variable set.
|
||||
// We create a subprocess for two reasons:
|
||||
//
|
||||
// 1. There's no other way to set the benchmarking parameters of testing.Benchmark.
|
||||
// 2. Since we're effectively running a performance test, running in a subprocess grants
|
||||
// us a little bit more isolation than using the same process.
|
||||
//
|
||||
// As an alternative, we could fairly easily reimplement the timing code in testing.Benchmark,
|
||||
// but a subprocess is just as easy to create.
|
||||
|
||||
selfCmd := CleanCmdEnv(exec.Command(os.Args[0], "-test.v", fmt.Sprintf("-test.run=^%s$", t.Name()), "-test.benchtime=1x"))
|
||||
selfCmd.Env = append(selfCmd.Env, "GO_PERF_UNIT_TEST=1")
|
||||
output, err := RunWithTimeout(t, selfCmd)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Logf("--- subprocess output ---\n%s", string(output))
|
||||
}
|
||||
if bytes.Contains(output, []byte("insignificant result")) {
|
||||
t.Skip("insignificant result")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pick a reasonable sample count.
|
||||
const count = 10
|
||||
|
||||
// Collect samples for scale factor 1.
|
||||
x1 := make([]testing.BenchmarkResult, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
x1 = append(x1, testing.Benchmark(f(1.0)))
|
||||
}
|
||||
|
||||
// Collect samples for scale factor 2.
|
||||
x2 := make([]testing.BenchmarkResult, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
x2 = append(x2, testing.Benchmark(f(2.0)))
|
||||
}
|
||||
|
||||
// Run a t-test on the results.
|
||||
r1 := testmath.BenchmarkResults(x1)
|
||||
r2 := testmath.BenchmarkResults(x2)
|
||||
result, err := testmath.TwoSampleWelchTTest(r1, r2, testmath.LocationDiffers)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run t-test: %v", err)
|
||||
}
|
||||
if result.P > 0.005 {
|
||||
// Insignificant result.
|
||||
t.Skip("insignificant result")
|
||||
}
|
||||
|
||||
// Let ourselves be within 3x; 2x is too strict.
|
||||
if m1, m2 := r1.Mean(), r2.Mean(); 3.0*m1 < m2 {
|
||||
t.Fatalf("failure to scale linearly: µ_1=%s µ_2=%s p=%f", time.Duration(m1), time.Duration(m2), result.P)
|
||||
}
|
||||
}
|
||||
|
|
38
src/internal/testmath/bench.go
Normal file
38
src/internal/testmath/bench.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// 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 testmath
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BenchmarkResults []testing.BenchmarkResult
|
||||
|
||||
func (b BenchmarkResults) Weight() float64 {
|
||||
var weight int
|
||||
for _, r := range b {
|
||||
weight += r.N
|
||||
}
|
||||
return float64(weight)
|
||||
}
|
||||
|
||||
func (b BenchmarkResults) Mean() float64 {
|
||||
var dur time.Duration
|
||||
for _, r := range b {
|
||||
dur += r.T * time.Duration(r.N)
|
||||
}
|
||||
return float64(dur) / b.Weight()
|
||||
}
|
||||
|
||||
func (b BenchmarkResults) Variance() float64 {
|
||||
var num float64
|
||||
mean := b.Mean()
|
||||
for _, r := range b {
|
||||
num += math.Pow(float64(r.T)-mean, 2) * float64(r.N)
|
||||
}
|
||||
return float64(num) / b.Weight()
|
||||
}
|
|
@ -333,3 +333,93 @@ func BenchmarkMutexSpin(b *testing.B) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
const runtimeSemaHashTableSize = 251 // known size of runtime hash table
|
||||
|
||||
func TestMutexLinearOne(t *testing.T) {
|
||||
testenv.CheckLinear(t, func(scale float64) func(*testing.B) {
|
||||
n := int(1000 * scale)
|
||||
return func(b *testing.B) {
|
||||
ch := make(chan int)
|
||||
locks := make([]RWMutex, runtimeSemaHashTableSize+1)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
locks[0].Lock()
|
||||
ch <- 1
|
||||
}()
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
go func() {
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[runtimeSemaHashTableSize].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[runtimeSemaHashTableSize].Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[runtimeSemaHashTableSize].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[runtimeSemaHashTableSize].Unlock()
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
<-ch
|
||||
locks[0].Unlock()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMutexLinearMany(t *testing.T) {
|
||||
if runtime.GOARCH == "arm" && os.Getenv("GOARM") == "5" {
|
||||
// stressLockMany reliably fails on the linux-arm-arm5spacemonkey
|
||||
// builder. See https://golang.org/issue/24221.
|
||||
return
|
||||
}
|
||||
testenv.CheckLinear(t, func(scale float64) func(*testing.B) {
|
||||
n := int(1000 * scale)
|
||||
return func(b *testing.B) {
|
||||
locks := make([]RWMutex, n*runtimeSemaHashTableSize+1)
|
||||
|
||||
var wg WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
locks[(i+1)*runtimeSemaHashTableSize].Lock()
|
||||
wg.Done()
|
||||
locks[(i+1)*runtimeSemaHashTableSize].Lock()
|
||||
locks[(i+1)*runtimeSemaHashTableSize].Unlock()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
go func() {
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[0].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[0].Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[0].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[0].Unlock()
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
locks[(i+1)*runtimeSemaHashTableSize].Unlock()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
// run
|
||||
|
||||
// Copyright 2017 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.
|
||||
|
||||
// Test that locks don't go quadratic due to runtime hash table collisions.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
|
||||
// checkLinear asserts that the running time of f(n) is at least linear but sub-quadratic.
|
||||
// tries is the initial number of iterations.
|
||||
func checkLinear(typ string, tries int, f func(n int)) {
|
||||
// Depending on the machine and OS, this test might be too fast
|
||||
// to measure with accurate enough granularity. On failure,
|
||||
// make it run longer, hoping that the timing granularity
|
||||
// is eventually sufficient.
|
||||
|
||||
timeF := func(n int) time.Duration {
|
||||
t1 := time.Now()
|
||||
f(n)
|
||||
return time.Since(t1)
|
||||
}
|
||||
|
||||
n := tries
|
||||
fails := 0
|
||||
var buf bytes.Buffer
|
||||
inversions := 0
|
||||
for {
|
||||
t1 := timeF(n)
|
||||
t2 := timeF(2 * n)
|
||||
if debug {
|
||||
println(n, t1.String(), 2*n, t2.String())
|
||||
}
|
||||
fmt.Fprintf(&buf, "%d %v %d %v (%.1fX)\n", n, t1, 2*n, t2, float64(t2)/float64(t1))
|
||||
// should be 2x (linear); allow up to 3x
|
||||
if t1*3/2 < t2 && t2 < t1*3 {
|
||||
return
|
||||
}
|
||||
if t2 < t1 {
|
||||
if inversions++; inversions >= 5 {
|
||||
// The system must be overloaded (some builders). Give up.
|
||||
return
|
||||
}
|
||||
continue // try again; don't increment fails
|
||||
}
|
||||
// Once the test runs long enough for n ops,
|
||||
// try to get the right ratio at least once.
|
||||
// If many in a row all fail, give up.
|
||||
if fails++; fails >= 5 {
|
||||
// If 2n ops run in under a second and the ratio
|
||||
// doesn't work out, make n bigger, trying to reduce
|
||||
// the effect that a constant amount of overhead has
|
||||
// on the computed ratio.
|
||||
if t2 < time.Second*4/10 {
|
||||
fails = 0
|
||||
n *= 2
|
||||
continue
|
||||
}
|
||||
panic(fmt.Sprintf("%s: too slow: %d ops: %v; %d ops: %v\n\n%s",
|
||||
typ, n, t1, 2*n, t2, buf.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const offset = 251 // known size of runtime hash table
|
||||
|
||||
const profile = false
|
||||
|
||||
func main() {
|
||||
if profile {
|
||||
f, err := os.Create("lock.prof")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
checkLinear("lockone", 1000, func(n int) {
|
||||
ch := make(chan int)
|
||||
locks := make([]sync.RWMutex, offset+1)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
locks[0].Lock()
|
||||
ch <- 1
|
||||
}()
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
go func() {
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[offset].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[offset].Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[offset].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[offset].Unlock()
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
<-ch
|
||||
locks[0].Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
if runtime.GOARCH == "arm" && os.Getenv("GOARM") == "5" {
|
||||
// lockmany reliably fails on the linux-arm-arm5spacemonkey
|
||||
// builder. See https://golang.org/issue/24221.
|
||||
return
|
||||
}
|
||||
|
||||
checkLinear("lockmany", 1000, func(n int) {
|
||||
locks := make([]sync.RWMutex, n*offset+1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
locks[(i+1)*offset].Lock()
|
||||
wg.Done()
|
||||
locks[(i+1)*offset].Lock()
|
||||
locks[(i+1)*offset].Unlock()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
go func() {
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[0].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[0].Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
locks[1].Lock()
|
||||
locks[0].Lock()
|
||||
locks[1].Unlock()
|
||||
runtime.Gosched()
|
||||
locks[0].Unlock()
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
locks[(i+1)*offset].Unlock()
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue