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 }