| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | // Copyright (c) 2022 MinIO, Inc.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This file is part of MinIO Object Storage stack
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify
 | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by
 | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or
 | 
					
						
							|  |  |  | // (at your option) any later version.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful
 | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					
						
							|  |  |  | // GNU Affero General Public License for more details.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License
 | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package cmd | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"math/rand" | 
					
						
							| 
									
										
										
										
											2022-04-08 12:20:40 +08:00
										 |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	"sync" | 
					
						
							|  |  |  | 	"sync/atomic" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/dustin/go-humanize" | 
					
						
							|  |  |  | 	"github.com/minio/madmin-go" | 
					
						
							| 
									
										
										
										
											2022-04-08 12:20:40 +08:00
										 |  |  | 	"github.com/minio/minio-go/v7" | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	"github.com/minio/pkg/randreader" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | // SpeedTestResult return value of the speedtest function
 | 
					
						
							|  |  |  | type SpeedTestResult struct { | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 	Endpoint      string | 
					
						
							|  |  |  | 	Uploads       uint64 | 
					
						
							|  |  |  | 	Downloads     uint64 | 
					
						
							|  |  |  | 	UploadTimes   madmin.TimeDurations | 
					
						
							|  |  |  | 	DownloadTimes madmin.TimeDurations | 
					
						
							|  |  |  | 	DownloadTTFB  madmin.TimeDurations | 
					
						
							|  |  |  | 	Error         string | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func newRandomReader(size int) io.Reader { | 
					
						
							|  |  |  | 	return io.LimitReader(randreader.New(), int64(size)) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | type firstByteRecorder struct { | 
					
						
							|  |  |  | 	t *time.Time | 
					
						
							|  |  |  | 	r io.Reader | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *firstByteRecorder) Read(p []byte) (n int, err error) { | 
					
						
							|  |  |  | 	if f.t != nil || len(p) == 0 { | 
					
						
							|  |  |  | 		return f.r.Read(p) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	// Read a single byte.
 | 
					
						
							|  |  |  | 	n, err = f.r.Read(p[:1]) | 
					
						
							|  |  |  | 	if n > 0 { | 
					
						
							|  |  |  | 		t := time.Now() | 
					
						
							|  |  |  | 		f.t = &t | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return n, err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | // Runs the speedtest on local MinIO process.
 | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | func selfSpeedTest(ctx context.Context, opts speedTestOpts) (SpeedTestResult, error) { | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	objAPI := newObjectLayerFn() | 
					
						
							|  |  |  | 	if objAPI == nil { | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 		return SpeedTestResult{}, errServerNotInitialized | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 	var wg sync.WaitGroup | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	var errOnce sync.Once | 
					
						
							|  |  |  | 	var retError string | 
					
						
							|  |  |  | 	var totalBytesWritten uint64 | 
					
						
							|  |  |  | 	var totalBytesRead uint64 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	objCountPerThread := make([]uint64, opts.concurrency) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	uploadsCtx, uploadsCancel := context.WithCancel(context.Background()) | 
					
						
							|  |  |  | 	defer uploadsCancel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	go func() { | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 		time.Sleep(opts.duration) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 		uploadsCancel() | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	objNamePrefix := pathJoin(speedTest, mustGetUUID()) | 
					
						
							| 
									
										
										
										
											2022-04-08 12:20:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	userMetadata := make(map[string]string) | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	userMetadata[globalObjectPerfUserMetadata] = "true" // Bypass S3 API freeze
 | 
					
						
							|  |  |  | 	popts := minio.PutObjectOptions{ | 
					
						
							|  |  |  | 		UserMetadata:         userMetadata, | 
					
						
							|  |  |  | 		DisableContentSha256: true, | 
					
						
							|  |  |  | 		DisableMultipart:     true, | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 	var mu sync.Mutex | 
					
						
							|  |  |  | 	var uploadTimes madmin.TimeDurations | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	wg.Add(opts.concurrency) | 
					
						
							|  |  |  | 	for i := 0; i < opts.concurrency; i++ { | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 		go func(i int) { | 
					
						
							|  |  |  | 			defer wg.Done() | 
					
						
							|  |  |  | 			for { | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 				t := time.Now() | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 				reader := newRandomReader(opts.objectSize) | 
					
						
							|  |  |  | 				tmpObjName := pathJoin(objNamePrefix, fmt.Sprintf("%d/%d", i, objCountPerThread[i])) | 
					
						
							|  |  |  | 				info, err := globalMinioClient.PutObject(uploadsCtx, opts.bucketName, tmpObjName, reader, int64(opts.objectSize), popts) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 				if err != nil { | 
					
						
							|  |  |  | 					if !contextCanceled(uploadsCtx) && !errors.Is(err, context.Canceled) { | 
					
						
							|  |  |  | 						errOnce.Do(func() { | 
					
						
							|  |  |  | 							retError = err.Error() | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					uploadsCancel() | 
					
						
							|  |  |  | 					return | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 				response := time.Since(t) | 
					
						
							| 
									
										
										
										
											2022-04-08 12:20:40 +08:00
										 |  |  | 				atomic.AddUint64(&totalBytesWritten, uint64(info.Size)) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 				objCountPerThread[i]++ | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 				mu.Lock() | 
					
						
							|  |  |  | 				uploadTimes = append(uploadTimes, response) | 
					
						
							|  |  |  | 				mu.Unlock() | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		}(i) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	wg.Wait() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// We already saw write failures, no need to proceed into read's
 | 
					
						
							|  |  |  | 	if retError != "" { | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 		return SpeedTestResult{ | 
					
						
							|  |  |  | 			Uploads:     totalBytesWritten, | 
					
						
							|  |  |  | 			Downloads:   totalBytesRead, | 
					
						
							|  |  |  | 			UploadTimes: uploadTimes, | 
					
						
							|  |  |  | 			Error:       retError, | 
					
						
							|  |  |  | 		}, nil | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	downloadsCtx, downloadsCancel := context.WithCancel(context.Background()) | 
					
						
							|  |  |  | 	defer downloadsCancel() | 
					
						
							|  |  |  | 	go func() { | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 		time.Sleep(opts.duration) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 		downloadsCancel() | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	gopts := minio.GetObjectOptions{} | 
					
						
							|  |  |  | 	gopts.Set(globalObjectPerfUserMetadata, "true") // Bypass S3 API freeze
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 	var downloadTimes madmin.TimeDurations | 
					
						
							|  |  |  | 	var downloadTTFB madmin.TimeDurations | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 	wg.Add(opts.concurrency) | 
					
						
							|  |  |  | 	for i := 0; i < opts.concurrency; i++ { | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 		go func(i int) { | 
					
						
							|  |  |  | 			defer wg.Done() | 
					
						
							|  |  |  | 			var j uint64 | 
					
						
							|  |  |  | 			if objCountPerThread[i] == 0 { | 
					
						
							|  |  |  | 				return | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			for { | 
					
						
							|  |  |  | 				if objCountPerThread[i] == j { | 
					
						
							|  |  |  | 					j = 0 | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 				tmpObjName := pathJoin(objNamePrefix, fmt.Sprintf("%d/%d", i, j)) | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 				t := time.Now() | 
					
						
							| 
									
										
										
										
											2022-07-13 01:12:47 +08:00
										 |  |  | 				r, err := globalMinioClient.GetObject(downloadsCtx, opts.bucketName, tmpObjName, gopts) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 				if err != nil { | 
					
						
							| 
									
										
										
										
											2022-04-08 12:20:40 +08:00
										 |  |  | 					errResp, ok := err.(minio.ErrorResponse) | 
					
						
							|  |  |  | 					if ok && errResp.StatusCode == http.StatusNotFound { | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 						continue | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					if !contextCanceled(downloadsCtx) && !errors.Is(err, context.Canceled) { | 
					
						
							|  |  |  | 						errOnce.Do(func() { | 
					
						
							|  |  |  | 							retError = err.Error() | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					downloadsCancel() | 
					
						
							|  |  |  | 					return | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 				fbr := firstByteRecorder{ | 
					
						
							|  |  |  | 					r: r, | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2022-09-20 02:05:16 +08:00
										 |  |  | 				n, err := io.Copy(io.Discard, &fbr) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 				r.Close() | 
					
						
							|  |  |  | 				if err == nil { | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 					response := time.Since(t) | 
					
						
							|  |  |  | 					ttfb := time.Since(*fbr.t) | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 					// Only capture success criteria - do not
 | 
					
						
							|  |  |  | 					// have to capture failed reads, truncated
 | 
					
						
							|  |  |  | 					// reads etc.
 | 
					
						
							|  |  |  | 					atomic.AddUint64(&totalBytesRead, uint64(n)) | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 					mu.Lock() | 
					
						
							|  |  |  | 					downloadTimes = append(downloadTimes, response) | 
					
						
							|  |  |  | 					downloadTTFB = append(downloadTTFB, ttfb) | 
					
						
							|  |  |  | 					mu.Unlock() | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 				if err != nil { | 
					
						
							|  |  |  | 					if !contextCanceled(downloadsCtx) && !errors.Is(err, context.Canceled) { | 
					
						
							|  |  |  | 						errOnce.Do(func() { | 
					
						
							|  |  |  | 							retError = err.Error() | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					downloadsCancel() | 
					
						
							|  |  |  | 					return | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				j++ | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}(i) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	wg.Wait() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-06 00:40:03 +08:00
										 |  |  | 	return SpeedTestResult{ | 
					
						
							|  |  |  | 		Uploads:       totalBytesWritten, | 
					
						
							|  |  |  | 		Downloads:     totalBytesRead, | 
					
						
							|  |  |  | 		UploadTimes:   uploadTimes, | 
					
						
							|  |  |  | 		DownloadTimes: downloadTimes, | 
					
						
							|  |  |  | 		DownloadTTFB:  downloadTTFB, | 
					
						
							|  |  |  | 		Error:         retError, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // To collect RX stats during "mc support perf net"
 | 
					
						
							|  |  |  | // RXSample holds the RX bytes for the duration between
 | 
					
						
							|  |  |  | // the last peer to connect and the first peer to disconnect.
 | 
					
						
							|  |  |  | // This is to improve the RX throughput accuracy.
 | 
					
						
							|  |  |  | type netPerfRX struct { | 
					
						
							|  |  |  | 	RX                uint64    // RX bytes
 | 
					
						
							|  |  |  | 	lastToConnect     time.Time // time at which last peer to connect to us
 | 
					
						
							|  |  |  | 	firstToDisconnect time.Time // time at which the first peer disconnects from us
 | 
					
						
							|  |  |  | 	RXSample          uint64    // RX bytes between lastToConnect and firstToDisconnect
 | 
					
						
							|  |  |  | 	activeConnections uint64 | 
					
						
							|  |  |  | 	sync.RWMutex | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (n *netPerfRX) Connect() { | 
					
						
							|  |  |  | 	n.Lock() | 
					
						
							|  |  |  | 	defer n.Unlock() | 
					
						
							|  |  |  | 	n.activeConnections++ | 
					
						
							|  |  |  | 	atomic.StoreUint64(&globalNetPerfRX.RX, 0) | 
					
						
							|  |  |  | 	n.lastToConnect = time.Now() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (n *netPerfRX) Disconnect() { | 
					
						
							|  |  |  | 	n.Lock() | 
					
						
							|  |  |  | 	defer n.Unlock() | 
					
						
							|  |  |  | 	n.activeConnections-- | 
					
						
							|  |  |  | 	if n.firstToDisconnect.IsZero() { | 
					
						
							|  |  |  | 		n.RXSample = atomic.LoadUint64(&n.RX) | 
					
						
							|  |  |  | 		n.firstToDisconnect = time.Now() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (n *netPerfRX) ActiveConnections() uint64 { | 
					
						
							|  |  |  | 	n.RLock() | 
					
						
							|  |  |  | 	defer n.RUnlock() | 
					
						
							|  |  |  | 	return n.activeConnections | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (n *netPerfRX) Reset() { | 
					
						
							| 
									
										
										
										
											2022-11-11 11:44:20 +08:00
										 |  |  | 	n.Lock() | 
					
						
							|  |  |  | 	defer n.Unlock() | 
					
						
							| 
									
										
										
										
											2022-03-09 01:54:38 +08:00
										 |  |  | 	n.RX = 0 | 
					
						
							|  |  |  | 	n.RXSample = 0 | 
					
						
							|  |  |  | 	n.lastToConnect = time.Time{} | 
					
						
							|  |  |  | 	n.firstToDisconnect = time.Time{} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Reader to read random data.
 | 
					
						
							|  |  |  | type netperfReader struct { | 
					
						
							|  |  |  | 	n   uint64 | 
					
						
							|  |  |  | 	eof chan struct{} | 
					
						
							|  |  |  | 	buf []byte | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (m *netperfReader) Read(b []byte) (int, error) { | 
					
						
							|  |  |  | 	select { | 
					
						
							|  |  |  | 	case <-m.eof: | 
					
						
							|  |  |  | 		return 0, io.EOF | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	n := copy(b, m.buf) | 
					
						
							|  |  |  | 	atomic.AddUint64(&m.n, uint64(n)) | 
					
						
							|  |  |  | 	return n, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func netperf(ctx context.Context, duration time.Duration) madmin.NetperfNodeResult { | 
					
						
							|  |  |  | 	r := &netperfReader{eof: make(chan struct{})} | 
					
						
							|  |  |  | 	r.buf = make([]byte, 128*humanize.KiByte) | 
					
						
							|  |  |  | 	rand.Read(r.buf) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	connectionsPerPeer := 16 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(globalNotificationSys.peerClients) > 16 { | 
					
						
							|  |  |  | 		// For a large cluster it's enough to have 1 connection per peer to saturate the network.
 | 
					
						
							|  |  |  | 		connectionsPerPeer = 1 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	errStr := "" | 
					
						
							|  |  |  | 	var wg sync.WaitGroup | 
					
						
							|  |  |  | 	for index := range globalNotificationSys.peerClients { | 
					
						
							|  |  |  | 		if globalNotificationSys.peerClients[index] == nil { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		go func(index int) { | 
					
						
							|  |  |  | 			for i := 0; i < connectionsPerPeer; i++ { | 
					
						
							|  |  |  | 				wg.Add(1) | 
					
						
							|  |  |  | 				go func() { | 
					
						
							|  |  |  | 					defer wg.Done() | 
					
						
							|  |  |  | 					err := globalNotificationSys.peerClients[index].DevNull(ctx, r) | 
					
						
							|  |  |  | 					if err != nil { | 
					
						
							|  |  |  | 						errStr = err.Error() | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				}() | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}(index) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	time.Sleep(duration) | 
					
						
							|  |  |  | 	close(r.eof) | 
					
						
							|  |  |  | 	wg.Wait() | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		if globalNetPerfRX.ActiveConnections() == 0 { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		time.Sleep(time.Second) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	rx := float64(globalNetPerfRX.RXSample) | 
					
						
							|  |  |  | 	delta := globalNetPerfRX.firstToDisconnect.Sub(globalNetPerfRX.lastToConnect) | 
					
						
							|  |  |  | 	if delta < 0 { | 
					
						
							|  |  |  | 		rx = 0 | 
					
						
							|  |  |  | 		errStr = "network disconnection issues detected" | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	globalNetPerfRX.Reset() | 
					
						
							|  |  |  | 	return madmin.NetperfNodeResult{Endpoint: "", TX: r.n / uint64(duration.Seconds()), RX: uint64(rx / delta.Seconds()), Error: errStr} | 
					
						
							|  |  |  | } |