github.com/thiagoyeds/go-cloud@v0.26.0/pubsub/benchmark_test.go (about)

     1  // Copyright 2019 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pubsub
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"math/rand"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"gocloud.dev/pubsub/driver"
    26  	"golang.org/x/sync/errgroup"
    27  	"golang.org/x/xerrors"
    28  )
    29  
    30  const (
    31  	// How long to run the test.
    32  	runFor = 25 * time.Second
    33  	// How long the "warmup period" is, during which we report more frequently.
    34  	reportWarmup = 500 * time.Millisecond
    35  	// Minimum frequency for reporting throughput, during warmup and after that.
    36  	reportPeriodWarmup = 50 * time.Millisecond
    37  	reportPeriod       = 1 * time.Second
    38  	// Number of output lines per test. We set this to a constant so that it's
    39  	// easy to copy/paste the output into a Google Sheet with pre-created graphs.
    40  	// Should be above runFor / reportPeriod + reportWarmup / reportPeriodWarmup.
    41  	numLinesPerTest = 50
    42  	// Number of data points to smooth msgs/sec and RPCs/sec over.
    43  	smoothing = 5
    44  )
    45  
    46  type fakeSub struct {
    47  	driver.Subscription
    48  	start   time.Time
    49  	profile func(bool, int) (int, time.Duration)
    50  	msgs    []*driver.Message
    51  }
    52  
    53  func (*fakeSub) SendAcks(ctx context.Context, ackIDs []driver.AckID) error { return nil }
    54  func (*fakeSub) CanNack() bool                                             { return false }
    55  func (*fakeSub) Close() error                                              { return nil }
    56  
    57  func (s *fakeSub) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
    58  	n, delay := s.profile(s.inMiddleThird(), maxMessages)
    59  	if delay > 0 {
    60  		time.Sleep(delay)
    61  	}
    62  	return s.msgs[:n], nil
    63  }
    64  
    65  // inMiddleThird returns true if this test is in the middle third of the running
    66  // time; used for burstiness tests.
    67  func (s *fakeSub) inMiddleThird() bool {
    68  	elapsed := time.Since(s.start)
    69  	return elapsed > runFor/3 && elapsed < runFor*2/3
    70  }
    71  
    72  // TestReceivePerformance enables characterization of Receive under various
    73  // situations, characterized in "tests" below.
    74  func TestReceivePerformance(t *testing.T) {
    75  	t.Skip("Skipped by default")
    76  
    77  	const defaultNumGoRoutines = 100
    78  	defaultReceiveProfile := func(_ bool, maxMessages int) (int, time.Duration) { return maxMessages, 0 }
    79  	defaultProcessProfile := func(bool) time.Duration { return 0 }
    80  
    81  	tests := []struct {
    82  		description string
    83  		// See the defaults above.
    84  		numGoRoutines  int
    85  		receiveProfile func(bool, int) (int, time.Duration)
    86  		processProfile func(bool) time.Duration
    87  	}{
    88  		{
    89  			description: "baseline",
    90  		},
    91  		{
    92  			description:   "1 goroutine",
    93  			numGoRoutines: 1,
    94  		},
    95  		{
    96  			description:    "receive 100ms",
    97  			receiveProfile: func(_ bool, maxMessages int) (int, time.Duration) { return maxMessages, 100 * time.Millisecond },
    98  		},
    99  		{
   100  			description:    "receive 1s",
   101  			receiveProfile: func(_ bool, maxMessages int) (int, time.Duration) { return maxMessages, 1 * time.Second },
   102  		},
   103  		{
   104  			description:    "process 100ms",
   105  			processProfile: func(bool) time.Duration { return 100 * time.Millisecond },
   106  		},
   107  		{
   108  			description:    "process 1s",
   109  			processProfile: func(bool) time.Duration { return 1 * time.Second },
   110  		},
   111  		{
   112  			description: "receive 1s process 70ms",
   113  			receiveProfile: func(_ bool, maxMessages int) (int, time.Duration) {
   114  				return maxMessages, 1 * time.Second
   115  			},
   116  			processProfile: func(bool) time.Duration { return 70 * time.Millisecond },
   117  		},
   118  		{
   119  			description: "receive 250ms+stddev 150ms, process 10ms + stddev 5ms",
   120  			receiveProfile: func(_ bool, maxMessages int) (int, time.Duration) {
   121  				return maxMessages, time.Duration(rand.NormFloat64()*150+250) * time.Millisecond
   122  			},
   123  			processProfile: func(bool) time.Duration { return time.Duration(rand.NormFloat64()*5+10) * time.Millisecond },
   124  		},
   125  		{
   126  			description: "bursty message arrival",
   127  			receiveProfile: func(inMiddleThird bool, maxMessages int) (int, time.Duration) {
   128  				// When in the middle third of the running time, return 0 messages.
   129  				n := maxMessages
   130  				if inMiddleThird {
   131  					n = 0
   132  				}
   133  				return n, time.Duration(rand.NormFloat64()*25+100) * time.Millisecond
   134  			},
   135  			processProfile: func(bool) time.Duration { return time.Duration(rand.NormFloat64()*5+10) * time.Millisecond },
   136  		},
   137  		{
   138  			description: "bursty receive time",
   139  			receiveProfile: func(inMiddleThird bool, maxMessages int) (int, time.Duration) {
   140  				// When in the middle third of the running time, 10x the RPC time.
   141  				d := time.Duration(rand.NormFloat64()*25+100) * time.Millisecond
   142  				if inMiddleThird {
   143  					d *= 10
   144  				}
   145  				return maxMessages, d
   146  			},
   147  			processProfile: func(bool) time.Duration { return time.Duration(rand.NormFloat64()*5+10) * time.Millisecond },
   148  		},
   149  		{
   150  			description: "bursty process time",
   151  			receiveProfile: func(_ bool, maxMessages int) (int, time.Duration) {
   152  				return maxMessages, time.Duration(rand.NormFloat64()*25+100) * time.Millisecond
   153  			},
   154  			processProfile: func(inMiddleThird bool) time.Duration {
   155  				d := time.Duration(rand.NormFloat64()*5+10) * time.Millisecond
   156  				if inMiddleThird {
   157  					d *= 100
   158  				}
   159  				return d
   160  			},
   161  		},
   162  	}
   163  
   164  	for _, test := range tests {
   165  		if test.numGoRoutines == 0 {
   166  			test.numGoRoutines = defaultNumGoRoutines
   167  		}
   168  		if test.receiveProfile == nil {
   169  			test.receiveProfile = defaultReceiveProfile
   170  		}
   171  		if test.processProfile == nil {
   172  			test.processProfile = defaultProcessProfile
   173  		}
   174  		t.Run(test.description, func(t *testing.T) {
   175  			runBenchmark(t, test.description, test.numGoRoutines, test.receiveProfile, test.processProfile)
   176  		})
   177  	}
   178  }
   179  
   180  func runBenchmark(t *testing.T, description string, numGoRoutines int, receiveProfile func(bool, int) (int, time.Duration), processProfile func(bool) time.Duration) {
   181  	msgs := make([]*driver.Message, maxBatchSize)
   182  	for i := range msgs {
   183  		msgs[i] = &driver.Message{}
   184  	}
   185  
   186  	fake := &fakeSub{msgs: msgs, profile: receiveProfile, start: time.Now()}
   187  	sub := newSubscription(fake, nil, nil)
   188  	defer sub.Shutdown(context.Background())
   189  
   190  	// Header row.
   191  	fmt.Printf("%s\tmsgs/sec\tRPCs/sec\tbatchsize\n", description)
   192  
   193  	var mu sync.Mutex
   194  	start := time.Now()
   195  	var lastReport time.Time
   196  	numMsgs := 0
   197  	var prevMsgsPerSec, prevRPCsPerSec []float64     // last <smoothing> datapoints
   198  	var runningMsgsPerSec, runningRPCsPerSec float64 // sum of values in above slices
   199  	numRPCs := 0
   200  	lastMaxMessages := 0
   201  	nLines := 1 // header
   202  
   203  	// mu must be locked when called.
   204  	reportLine := func(now time.Time) {
   205  		elapsed := now.Sub(start)
   206  		elapsedSinceReport := now.Sub(lastReport)
   207  
   208  		// Smooth msgsPerSec over the last <smoothing> datapoints.
   209  		msgsPerSec := float64(numMsgs) / elapsedSinceReport.Seconds()
   210  		prevMsgsPerSec = append(prevMsgsPerSec, msgsPerSec)
   211  		runningMsgsPerSec += msgsPerSec
   212  		if len(prevMsgsPerSec) > smoothing {
   213  			runningMsgsPerSec -= prevMsgsPerSec[0]
   214  			if runningMsgsPerSec < 0 {
   215  				runningMsgsPerSec = 0
   216  			}
   217  			prevMsgsPerSec = prevMsgsPerSec[1:]
   218  		}
   219  
   220  		// Smooth rpcsPerSec over the last <smoothing> datapoints.
   221  		rpcsPerSec := float64(numRPCs) / elapsedSinceReport.Seconds()
   222  		prevRPCsPerSec = append(prevRPCsPerSec, rpcsPerSec)
   223  		runningRPCsPerSec += rpcsPerSec
   224  		if len(prevRPCsPerSec) > smoothing {
   225  			runningRPCsPerSec -= prevRPCsPerSec[0]
   226  			if runningRPCsPerSec < 0 {
   227  				runningRPCsPerSec = 0
   228  			}
   229  			prevRPCsPerSec = prevRPCsPerSec[1:]
   230  		}
   231  
   232  		fmt.Printf("%f\t%f\t%f\t%d\n", elapsed.Seconds(), runningMsgsPerSec/float64(len(prevMsgsPerSec)), runningRPCsPerSec/float64(len(prevRPCsPerSec)), lastMaxMessages)
   233  		nLines++
   234  
   235  		lastReport = now
   236  		numMsgs = 0
   237  		numRPCs = 0
   238  	}
   239  
   240  	sub.preReceiveBatchHook = func(maxMessages int) {
   241  		mu.Lock()
   242  		defer mu.Unlock()
   243  		lastMaxMessages = maxMessages
   244  		numRPCs++
   245  		if lastReport.IsZero() {
   246  			reportLine(time.Now())
   247  		}
   248  	}
   249  
   250  	ctx, cancel := context.WithTimeout(context.Background(), runFor)
   251  	defer cancel()
   252  	done := make(chan struct{})
   253  	go func() {
   254  		period := reportPeriodWarmup
   255  		for {
   256  			select {
   257  			case now := <-time.After(period):
   258  				mu.Lock()
   259  				reportLine(now)
   260  				mu.Unlock()
   261  				if now.Sub(start) > reportWarmup {
   262  					period = reportPeriod
   263  				}
   264  			case <-ctx.Done():
   265  				close(done)
   266  				return
   267  			}
   268  		}
   269  	}()
   270  
   271  	var grp errgroup.Group
   272  	for i := 0; i < numGoRoutines; i++ {
   273  		grp.Go(func() error {
   274  			// Each goroutine loops until ctx is canceled.
   275  			for {
   276  				m, err := sub.Receive(ctx)
   277  				if xerrors.Is(err, context.DeadlineExceeded) {
   278  					return nil
   279  				}
   280  				if err != nil {
   281  					return err
   282  				}
   283  				mu.Lock()
   284  				numMsgs++
   285  				mu.Unlock()
   286  				delay := processProfile(fake.inMiddleThird())
   287  				if delay > 0 {
   288  					time.Sleep(delay)
   289  				}
   290  				m.Ack()
   291  			}
   292  		})
   293  	}
   294  	if err := grp.Wait(); err != nil {
   295  		t.Errorf("%s: %v", description, err)
   296  	}
   297  	<-done
   298  	if nLines > numLinesPerTest {
   299  		t.Errorf("produced too many lines (%d)", nLines)
   300  	}
   301  	for n := nLines; n < numLinesPerTest; n++ {
   302  		fmt.Println()
   303  	}
   304  }