Skip to content

Commit

Permalink
sync: move lock linearity test and treat it like a performance test
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mknyszek committed Jun 13, 2022
1 parent 6130461 commit 1fe2810
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 171 deletions.
65 changes: 65 additions & 0 deletions src/internal/testenv/testenv.go
Expand Up @@ -16,6 +16,7 @@ import (
"flag"
"fmt"
"internal/cfg"
"internal/testmath"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -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 changes: 38 additions & 0 deletions src/internal/testmath/bench.go
@@ -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()
}
90 changes: 90 additions & 0 deletions src/sync/mutex_test.go
Expand Up @@ -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()
}
}
})
}
171 changes: 0 additions & 171 deletions test/locklinear.go

This file was deleted.

0 comments on commit 1fe2810

Please sign in to comment.