github.com/fufuok/utils@v1.0.10/xsync/mpmcqueueof_test.go (about)

     1  //go:build go1.19
     2  // +build go1.19
     3  
     4  // Copyright notice. The following tests are partially based on
     5  // the following file from the Go Programming Language core repo:
     6  // https://github.com/golang/go/blob/831f9376d8d730b16fb33dfd775618dffe13ce7a/src/runtime/chan_test.go
     7  
     8  package xsync_test
     9  
    10  import (
    11  	"runtime"
    12  	"strconv"
    13  	"sync"
    14  	"sync/atomic"
    15  	"testing"
    16  	"time"
    17  
    18  	. "github.com/fufuok/utils/xsync"
    19  )
    20  
    21  func TestQueueOf_InvalidSize(t *testing.T) {
    22  	defer func() { recover() }()
    23  	NewMPMCQueueOf[int](0)
    24  	t.Fatal("no panic detected")
    25  }
    26  
    27  func TestQueueOfEnqueueDequeueInt(t *testing.T) {
    28  	q := NewMPMCQueueOf[int](10)
    29  	for i := 0; i < 10; i++ {
    30  		q.Enqueue(i)
    31  	}
    32  	for i := 0; i < 10; i++ {
    33  		if got := q.Dequeue(); got != i {
    34  			t.Fatalf("got %v, want %d", got, i)
    35  		}
    36  	}
    37  }
    38  
    39  func TestQueueOfEnqueueDequeueString(t *testing.T) {
    40  	q := NewMPMCQueueOf[string](10)
    41  	for i := 0; i < 10; i++ {
    42  		q.Enqueue(strconv.Itoa(i))
    43  	}
    44  	for i := 0; i < 10; i++ {
    45  		if got := q.Dequeue(); got != strconv.Itoa(i) {
    46  			t.Fatalf("got %v, want %d", got, i)
    47  		}
    48  	}
    49  }
    50  
    51  func TestQueueOfEnqueueDequeueStruct(t *testing.T) {
    52  	type foo struct {
    53  		bar int
    54  		baz int
    55  	}
    56  	q := NewMPMCQueueOf[foo](10)
    57  	for i := 0; i < 10; i++ {
    58  		q.Enqueue(foo{i, i})
    59  	}
    60  	for i := 0; i < 10; i++ {
    61  		if got := q.Dequeue(); got.bar != i || got.baz != i {
    62  			t.Fatalf("got %v, want %d", got, i)
    63  		}
    64  	}
    65  }
    66  
    67  func TestQueueOfEnqueueDequeueStructRef(t *testing.T) {
    68  	type foo struct {
    69  		bar int
    70  		baz int
    71  	}
    72  	q := NewMPMCQueueOf[*foo](11)
    73  	for i := 0; i < 10; i++ {
    74  		q.Enqueue(&foo{i, i})
    75  	}
    76  	q.Enqueue(nil)
    77  	for i := 0; i < 10; i++ {
    78  		if got := q.Dequeue(); got.bar != i || got.baz != i {
    79  			t.Fatalf("got %v, want %d", got, i)
    80  		}
    81  	}
    82  	if last := q.Dequeue(); last != nil {
    83  		t.Fatalf("got %v, want nil", last)
    84  	}
    85  }
    86  
    87  func TestQueueOfEnqueueBlocksOnFull(t *testing.T) {
    88  	q := NewMPMCQueueOf[string](1)
    89  	q.Enqueue("foo")
    90  	cdone := make(chan bool)
    91  	flag := int32(0)
    92  	go func() {
    93  		q.Enqueue("bar")
    94  		if atomic.LoadInt32(&flag) == 0 {
    95  			t.Error("enqueue on full queue didn't wait for dequeue")
    96  		}
    97  		cdone <- true
    98  	}()
    99  	time.Sleep(50 * time.Millisecond)
   100  	atomic.StoreInt32(&flag, 1)
   101  	if got := q.Dequeue(); got != "foo" {
   102  		t.Fatalf("got %v, want foo", got)
   103  	}
   104  	<-cdone
   105  }
   106  
   107  func TestQueueOfDequeueBlocksOnEmpty(t *testing.T) {
   108  	q := NewMPMCQueueOf[string](2)
   109  	cdone := make(chan bool)
   110  	flag := int32(0)
   111  	go func() {
   112  		q.Dequeue()
   113  		if atomic.LoadInt32(&flag) == 0 {
   114  			t.Error("dequeue on empty queue didn't wait for enqueue")
   115  		}
   116  		cdone <- true
   117  	}()
   118  	time.Sleep(50 * time.Millisecond)
   119  	atomic.StoreInt32(&flag, 1)
   120  	q.Enqueue("foobar")
   121  	<-cdone
   122  }
   123  
   124  func TestQueueOfTryEnqueueDequeue(t *testing.T) {
   125  	q := NewMPMCQueueOf[int](10)
   126  	for i := 0; i < 10; i++ {
   127  		if !q.TryEnqueue(i) {
   128  			t.Fatalf("failed to enqueue for %d", i)
   129  		}
   130  	}
   131  	for i := 0; i < 10; i++ {
   132  		if got, ok := q.TryDequeue(); !ok || got != i {
   133  			t.Fatalf("got %v, want %d, for status %v", got, i, ok)
   134  		}
   135  	}
   136  }
   137  
   138  func TestQueueOfTryEnqueueOnFull(t *testing.T) {
   139  	q := NewMPMCQueueOf[string](1)
   140  	if !q.TryEnqueue("foo") {
   141  		t.Error("failed to enqueue initial item")
   142  	}
   143  	if q.TryEnqueue("bar") {
   144  		t.Error("got success for enqueue on full queue")
   145  	}
   146  }
   147  
   148  func TestQueueOfTryDequeueBlocksOnEmpty(t *testing.T) {
   149  	q := NewMPMCQueueOf[int](2)
   150  	if _, ok := q.TryDequeue(); ok {
   151  		t.Error("got success for enqueue on empty queue")
   152  	}
   153  }
   154  
   155  func hammerQueueOfBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) {
   156  	runtime.GOMAXPROCS(gomaxprocs)
   157  	q := NewMPMCQueueOf[int](numThreads)
   158  	startwg := sync.WaitGroup{}
   159  	startwg.Add(1)
   160  	csum := make(chan int, numThreads)
   161  	// Start producers.
   162  	for i := 0; i < numThreads; i++ {
   163  		go func(n int) {
   164  			startwg.Wait()
   165  			for j := n; j < numOps; j += numThreads {
   166  				q.Enqueue(j)
   167  			}
   168  		}(i)
   169  	}
   170  	// Start consumers.
   171  	for i := 0; i < numThreads; i++ {
   172  		go func(n int) {
   173  			startwg.Wait()
   174  			sum := 0
   175  			for j := n; j < numOps; j += numThreads {
   176  				item := q.Dequeue()
   177  				sum += item
   178  			}
   179  			csum <- sum
   180  		}(i)
   181  	}
   182  	startwg.Done()
   183  	// Wait for all the sums from producers.
   184  	sum := 0
   185  	for i := 0; i < numThreads; i++ {
   186  		s := <-csum
   187  		sum += s
   188  	}
   189  	// Assert the total sum.
   190  	expectedSum := numOps * (numOps - 1) / 2
   191  	if sum != expectedSum {
   192  		t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d",
   193  			numOps, numThreads, sum, expectedSum)
   194  	}
   195  }
   196  
   197  func TestQueueOfBlockingCalls(t *testing.T) {
   198  	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1))
   199  	n := 100
   200  	if testing.Short() {
   201  		n = 10
   202  	}
   203  	hammerQueueOfBlockingCalls(t, 1, 100*n, n)
   204  	hammerQueueOfBlockingCalls(t, 1, 1000*n, 10*n)
   205  	hammerQueueOfBlockingCalls(t, 4, 100*n, n)
   206  	hammerQueueOfBlockingCalls(t, 4, 1000*n, 10*n)
   207  	hammerQueueOfBlockingCalls(t, 8, 100*n, n)
   208  	hammerQueueOfBlockingCalls(t, 8, 1000*n, 10*n)
   209  }
   210  
   211  func hammerQueueOfNonBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) {
   212  	runtime.GOMAXPROCS(gomaxprocs)
   213  	q := NewMPMCQueueOf[int](numThreads)
   214  	startwg := sync.WaitGroup{}
   215  	startwg.Add(1)
   216  	csum := make(chan int, numThreads)
   217  	// Start producers.
   218  	for i := 0; i < numThreads; i++ {
   219  		go func(n int) {
   220  			startwg.Wait()
   221  			for j := n; j < numOps; j += numThreads {
   222  				for !q.TryEnqueue(j) {
   223  					// busy spin until success
   224  				}
   225  			}
   226  		}(i)
   227  	}
   228  	// Start consumers.
   229  	for i := 0; i < numThreads; i++ {
   230  		go func(n int) {
   231  			startwg.Wait()
   232  			sum := 0
   233  			for j := n; j < numOps; j += numThreads {
   234  				var (
   235  					item int
   236  					ok   bool
   237  				)
   238  				for {
   239  					// busy spin until success
   240  					if item, ok = q.TryDequeue(); ok {
   241  						sum += item
   242  						break
   243  					}
   244  				}
   245  			}
   246  			csum <- sum
   247  		}(i)
   248  	}
   249  	startwg.Done()
   250  	// Wait for all the sums from producers.
   251  	sum := 0
   252  	for i := 0; i < numThreads; i++ {
   253  		s := <-csum
   254  		sum += s
   255  	}
   256  	// Assert the total sum.
   257  	expectedSum := numOps * (numOps - 1) / 2
   258  	if sum != expectedSum {
   259  		t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d",
   260  			numOps, numThreads, sum, expectedSum)
   261  	}
   262  }
   263  
   264  func TestQueueOfNonBlockingCalls(t *testing.T) {
   265  	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1))
   266  	n := 10
   267  	if testing.Short() {
   268  		n = 1
   269  	}
   270  	hammerQueueOfNonBlockingCalls(t, 1, n, n)
   271  	hammerQueueOfNonBlockingCalls(t, 2, 10*n, 2*n)
   272  	hammerQueueOfNonBlockingCalls(t, 4, 100*n, 4*n)
   273  }
   274  
   275  func benchmarkQueueOfProdCons(b *testing.B, queueSize, localWork int) {
   276  	callsPerSched := queueSize
   277  	procs := runtime.GOMAXPROCS(-1) / 2
   278  	if procs == 0 {
   279  		procs = 1
   280  	}
   281  	N := int32(b.N / callsPerSched)
   282  	c := make(chan bool, 2*procs)
   283  	q := NewMPMCQueueOf[int](queueSize)
   284  	for p := 0; p < procs; p++ {
   285  		go func() {
   286  			foo := 0
   287  			for atomic.AddInt32(&N, -1) >= 0 {
   288  				for g := 0; g < callsPerSched; g++ {
   289  					for i := 0; i < localWork; i++ {
   290  						foo *= 2
   291  						foo /= 2
   292  					}
   293  					q.Enqueue(1)
   294  				}
   295  			}
   296  			q.Enqueue(0)
   297  			c <- foo == 42
   298  		}()
   299  		go func() {
   300  			foo := 0
   301  			for {
   302  				v := q.Dequeue()
   303  				if v == 0 {
   304  					break
   305  				}
   306  				for i := 0; i < localWork; i++ {
   307  					foo *= 2
   308  					foo /= 2
   309  				}
   310  			}
   311  			c <- foo == 42
   312  		}()
   313  	}
   314  	for p := 0; p < procs; p++ {
   315  		<-c
   316  		<-c
   317  	}
   318  }
   319  
   320  func BenchmarkQueueOfProdCons(b *testing.B) {
   321  	benchmarkQueueOfProdCons(b, 1000, 0)
   322  }
   323  
   324  func BenchmarkOfQueueProdConsWork100(b *testing.B) {
   325  	benchmarkQueueOfProdCons(b, 1000, 100)
   326  }