diff --git a/.gitignore b/.gitignore index 92f7f07..aeaf3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ main .DS_Store ffperf/ffperf ff-perf.log +ffperf-report.html .vscode dist/ *.iml diff --git a/cmd/run.go b/cmd/run.go index d754775..e67c0ae 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -28,6 +28,7 @@ import ( "github.com/hyperledger/firefly-perf-cli/internal/perf" "github.com/hyperledger/firefly-perf-cli/internal/server" "github.com/hyperledger/firefly-perf-cli/internal/types" + "github.com/hyperledger/firefly-perf-cli/internal/util" "github.com/hyperledger/firefly/pkg/core" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -65,17 +66,22 @@ Executes a instance within a performance test suite to generate synthetic load a log.Warn("both the \"instance-name\" and \"instance-index\" flags were provided, using \"instance-name\"") } - instance, err := selectInstance(config) + instanceConfig, err := selectInstance(config) if err != nil { return err } - runnerConfig, err := generateRunnerConfigFromInstance(instance, config) + runnerConfig, err := generateRunnerConfigFromInstance(instanceConfig, config) if err != nil { return err } - perfRunner = perf.New(runnerConfig) + configYaml, err := yaml.Marshal(instanceConfig) + if err != nil { + return err + } + + perfRunner = perf.New(runnerConfig, util.NewReportForTestInstance(string(configYaml), instanceName)) httpServer = server.NewHttpServer() return nil diff --git a/go.sum b/go.sum index b506040..6d3972b 100644 --- a/go.sum +++ b/go.sum @@ -173,12 +173,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hyperledger/firefly v1.2.0 h1:No82vzsur3TODU0giIECDcMnWQ/8BRGoYo7QK/avr4A= github.com/hyperledger/firefly v1.2.0/go.mod h1:tmpTfSjX/NIa7xHTtTb36S48X9+3nNutY7ZxLt3lgCU= -github.com/hyperledger/firefly-common v1.2.13 h1:4pGL8LusXoijeoxM9J36fzBq4jvZpZbGjpQqgempXMk= -github.com/hyperledger/firefly-common v1.2.13/go.mod h1:17lOH4YufiPy82LpKm8fPa/YXJ0pUyq01zK1CmklJwM= -github.com/hyperledger/firefly-common v1.2.14 h1:HON9GJZXvrL0l2AG5DWHSGiBh05hElgFS5lm1OPR83M= -github.com/hyperledger/firefly-common v1.2.14/go.mod h1:17lOH4YufiPy82LpKm8fPa/YXJ0pUyq01zK1CmklJwM= -github.com/hyperledger/firefly-common v1.2.15 h1:WdNB65IJvIyiOhVW3nxB3sQKqtJbdJ7ie0PJIM11CSU= -github.com/hyperledger/firefly-common v1.2.15/go.mod h1:17lOH4YufiPy82LpKm8fPa/YXJ0pUyq01zK1CmklJwM= github.com/hyperledger/firefly-common v1.2.16 h1:cVSaxKycOb+/oT2wExbrzxr68aVKQObeBOLaiJ0mTLg= github.com/hyperledger/firefly-common v1.2.16/go.mod h1:17lOH4YufiPy82LpKm8fPa/YXJ0pUyq01zK1CmklJwM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/internal/conf/conf.go b/internal/conf/conf.go index d05bc93..17a3998 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -81,7 +81,7 @@ type InstanceConfig struct { MaxTimePerAction time.Duration `json:"maxTimePerAction,omitempty" yaml:"maxTimePerAction,omitempty"` MaxActions int64 `json:"maxActions,omitempty" yaml:"maxActions,omitempty"` RampLength time.Duration `json:"rampLength,omitempty" yaml:"rampLength,omitempty"` - SkipMintConfirmations bool `json:"skipMintConfirmations,omitempty" yaml:"skipMintConfirmations,omitempty"` + SkipMintConfirmations bool `json:"skipMintConfirmations" yaml:"skipMintConfirmations"` DelinquentAction string `json:"delinquentAction,omitempty" yaml:"delinquentAction,omitempty"` PerWorkerSigningKeyPrefix string `json:"perWorkerSigningKeyPrefix,omitempty" yaml:"perWorkerSigningKeyPrefix,omitempty"` } @@ -121,8 +121,8 @@ type TokenConfig struct { type ContractOptions struct { Address string `json:"address" yaml:"address"` - Channel string `json:"channel" yaml:"channel"` - Chaincode string `json:"chaincode" yaml:"chaincode"` + Channel string `json:"channel,omitempty" yaml:"channel,omitempty"` + Chaincode string `json:"chaincode,omitempty" yaml:"chaincode,omitempty"` } type FireFlyWsConfig struct { diff --git a/internal/perf/blob_broadcast_msg.go b/internal/perf/blob_broadcast_msg.go index ca41f1e..ebda500 100644 --- a/internal/perf/blob_broadcast_msg.go +++ b/internal/perf/blob_broadcast_msg.go @@ -33,7 +33,7 @@ func (tc *blobBroadcast) IDType() TrackingIDType { return TrackingIDTypeMessageID } -func (tc *blobBroadcast) RunOnce() (string, error) { +func (tc *blobBroadcast) RunOnce(iterationCount int) (string, error) { blob, hash := tc.generateBlob(big.NewInt(1024)) dataID, err := tc.uploadBlob(blob, hash, tc.pr.client.BaseURL) diff --git a/internal/perf/blob_private_msg.go b/internal/perf/blob_private_msg.go index 7d7e788..1c0d735 100644 --- a/internal/perf/blob_private_msg.go +++ b/internal/perf/blob_private_msg.go @@ -33,7 +33,7 @@ func (tc *blobPrivate) IDType() TrackingIDType { return TrackingIDTypeMessageID } -func (tc *blobPrivate) RunOnce() (string, error) { +func (tc *blobPrivate) RunOnce(iterationCount int) (string, error) { blob, hash := tc.generateBlob(big.NewInt(1024)) dataID, err := tc.uploadBlob(blob, hash, tc.pr.client.BaseURL) diff --git a/internal/perf/broadcast_msg.go b/internal/perf/broadcast_msg.go index 8e8b1c6..3656d03 100644 --- a/internal/perf/broadcast_msg.go +++ b/internal/perf/broadcast_msg.go @@ -32,7 +32,7 @@ func (tc *broadcast) IDType() TrackingIDType { return TrackingIDTypeMessageID } -func (tc *broadcast) RunOnce() (string, error) { +func (tc *broadcast) RunOnce(iterationCount int) (string, error) { payload := fmt.Sprintf(`{ "data":[ diff --git a/internal/perf/custom_ethereum_contract.go b/internal/perf/custom_ethereum_contract.go index a945437..b255bb4 100644 --- a/internal/perf/custom_ethereum_contract.go +++ b/internal/perf/custom_ethereum_contract.go @@ -30,7 +30,6 @@ import ( type customEthereum struct { testBase - iteration int } func newCustomEthereumTestWorker(pr *perfRunner, workerID int, actionsPerLoop int) TestCase { @@ -51,8 +50,8 @@ func (tc *customEthereum) IDType() TrackingIDType { return TrackingIDTypeWorkerNumber } -func (tc *customEthereum) RunOnce() (string, error) { - idempotencyKey := tc.pr.getIdempotencyKey(tc.workerID, tc.iteration) +func (tc *customEthereum) RunOnce(iterationCount int) (string, error) { + idempotencyKey := tc.pr.getIdempotencyKey(tc.workerID, iterationCount) invokeOptionsJSON := "" if tc.pr.cfg.InvokeOptions != nil { b, err := json.Marshal(tc.pr.cfg.InvokeOptions) @@ -106,6 +105,5 @@ func (tc *customEthereum) RunOnce() (string, error) { return "", fmt.Errorf("Error invoking contract [%d]: %s (%+v)", resStatus(res), err, &resError) } } - tc.iteration++ return strconv.Itoa(tc.workerID), nil } diff --git a/internal/perf/custom_fabric_contract.go b/internal/perf/custom_fabric_contract.go index a0e2dc4..3b2ea23 100644 --- a/internal/perf/custom_fabric_contract.go +++ b/internal/perf/custom_fabric_contract.go @@ -29,7 +29,6 @@ import ( type customFabric struct { testBase - iteration int } func newCustomFabricTestWorker(pr *perfRunner, workerID int, actionsPerLoop int) TestCase { @@ -50,8 +49,8 @@ func (tc *customFabric) IDType() TrackingIDType { return TrackingIDTypeWorkerNumber } -func (tc *customFabric) RunOnce() (string, error) { - idempotencyKey := tc.pr.getIdempotencyKey(tc.workerID, tc.iteration) +func (tc *customFabric) RunOnce(iterationCount int) (string, error) { + idempotencyKey := tc.pr.getIdempotencyKey(tc.workerID, iterationCount) invokeOptionsJSON := "" if tc.pr.cfg.InvokeOptions != nil { b, err := json.Marshal(tc.pr.cfg.InvokeOptions) @@ -143,6 +142,5 @@ func (tc *customFabric) RunOnce() (string, error) { if err != nil || res.IsError() { return "", fmt.Errorf("Error invoking contract [%d]: %s (%+v)", resStatus(res), err, &resError) } - tc.iteration++ return strconv.Itoa(tc.workerID), nil } diff --git a/internal/perf/perf.go b/internal/perf/perf.go index 2378835..10d3458 100644 --- a/internal/perf/perf.go +++ b/internal/perf/perf.go @@ -36,6 +36,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly-perf-cli/internal/conf" + "github.com/hyperledger/firefly-perf-cli/internal/util" "github.com/hyperledger/firefly/pkg/core" dto "github.com/prometheus/client_model/go" log "github.com/sirupsen/logrus" @@ -116,7 +117,7 @@ func getMetricVal(collector prometheus.Collector) float64 { metric := dto.Metric{} err := (<-collectorChannel).Write(&metric) if err != nil { - log.Error("Error writing metric: %s", err) + log.Error("error writing metric: %s", err) } if metric.Counter != nil { return *metric.Counter.Value @@ -141,7 +142,7 @@ const ( type TestCase interface { WorkerID() int - RunOnce() (trackingID string, err error) + RunOnce(iterationCount int) (trackingID string, err error) IDType() TrackingIDType Name() string ActionsPerLoop() int @@ -155,16 +156,24 @@ type inflightTest struct { var mintStartingBalance int type perfRunner struct { - bfr chan int - cfg *conf.RunnerConfig - client *resty.Client - ctx context.Context - shutdown context.CancelFunc - stopping bool - startTime int64 - endTime int64 - startRampTime int64 - endRampTime int64 + bfr chan int + cfg *conf.RunnerConfig + client *resty.Client + ctx context.Context + shutdown context.CancelFunc + stopping bool + + startTime int64 + endSendTime int64 + endTime int64 + startRampTime int64 + endRampTime int64 + + reportBuilder *util.Report + sendTime *util.Latency + receiveTime *util.Latency + totalTime *util.Latency + msgTimeMap map[string]*inflightTest rampSummary int64 totalSummary int64 @@ -189,7 +198,7 @@ type SubscriptionInfo struct { Job fftypes.FFEnum } -func New(config *conf.RunnerConfig) PerfRunner { +func New(config *conf.RunnerConfig, reportBuilder *util.Report) PerfRunner { if config.LogLevel != "" { if level, err := log.ParseLevel(config.LogLevel); err == nil { log.SetLevel(level) @@ -227,6 +236,10 @@ func New(config *conf.RunnerConfig) PerfRunner { startTime: startTime, endTime: endTime, poolName: poolName, + reportBuilder: reportBuilder, + sendTime: &util.Latency{}, + receiveTime: &util.Latency{}, + totalTime: &util.Latency{}, poolConnectorName: config.TokenOptions.TokenPoolConnectorName, tagPrefix: fmt.Sprintf("perf_%s", wsUUID.String()), msgTimeMap: make(map[string]*inflightTest), @@ -434,7 +447,7 @@ func (pr *perfRunner) Start() (err error) { case conf.PerfTestBlobPrivateMsg: tc = newBlobPrivateTestWorker(pr, id, test.ActionsPerLoop) default: - return fmt.Errorf("Unknown test case '%s'", test.Name) + return fmt.Errorf("unknown test case '%s'", test.Name) } delayPerWorker := pr.cfg.RampLength / time.Duration(test.Workers) @@ -497,14 +510,29 @@ perfLoop: // If configured, check that the balance of the mint recipient address is correct if pr.detectDelinquentBalance() { if pr.cfg.DelinquentAction == conf.DelinquentActionExit.String() { - log.Panic(fmt.Errorf("Token mint recipient balance didn't reach the expected value in the allowed time")) + log.Panic(fmt.Errorf("token mint recipient balance didn't reach the expected value in the allowed time")) } } pr.stopping = true measuredActions := pr.totalSummary - measuredTime := time.Since(time.Unix(pr.startTime, 0)).Seconds() - measuredTps := pr.calculateCurrentTps(true) + measuredTime := time.Since(time.Unix(pr.startTime, 0)) + + testNames := make([]string, len(pr.cfg.Tests)) + for i, t := range pr.cfg.Tests { + testNames[i] = t.Name.String() + } + testNameString := testNames[0] + if len(testNames) > 1 { + testNameString = strings.Join(testNames[:], ",") + } + tps := util.GenerateTPS(measuredActions, pr.startTime, pr.endSendTime) + pr.reportBuilder.AddTestRunMetrics(testNameString, measuredActions, measuredTime, tps, pr.totalTime) + err = pr.reportBuilder.GenerateHTML() + + if err != nil { + log.Errorf("failed to generate performance report: %+v", err) + } // we sleep on shutdown / completion to allow for Prometheus metrics to be scraped one final time // After 30 seconds workers should be completed, so we check for delinquent messages @@ -525,9 +553,13 @@ perfLoop: log.Infof(" - Prometheus metric incomplete_events_total = %f\n", getMetricVal(incompleteEventsCounter)) log.Infof(" - Prometheus metric delinquent_msgs_total = %f\n", getMetricVal(delinquentMsgsCounter)) log.Infof(" - Prometheus metric actions_submitted_total = %f\n", getMetricVal(totalActionsCounter)) - log.Infof(" - Test duration (secs): %2f", measuredTime) + log.Infof(" - Test duration: %s", measuredTime) log.Infof(" - Measured actions: %d", measuredActions) - log.Infof(" - Measured actions/sec: %2f", measuredTps) + log.Infof(" - Measured send TPS: %2f", tps.SendRate) + log.Infof(" - Measured throughput: %2f", tps.Throughput) + log.Infof(" - Measured send duration: %s", pr.sendTime) + log.Infof(" - Measured event receiving duration: %s", pr.receiveTime) + log.Infof(" - Measured total duration: %s", pr.totalTime) return nil } @@ -618,14 +650,14 @@ func (pr *perfRunner) eventLoop(nodeURL string, wsconn wsclient.WSClient) (err e log.Infof("\n\t%d - Received from %s\n\t%d --- Event ID: %s\n\t%d --- Ref: %s", workerID, wsconn.URL(), workerID, event.ID.String(), workerID, event.Reference) } } else { - log.Errorf("No URI in token transfer event: %s") + log.Errorf("no URI in token transfer event: %s") b, _ := json.Marshal(&event) log.Errorf("Full event: %s", b) incompleteEventsCounter.Inc() if pr.cfg.DelinquentAction == conf.DelinquentActionExit.String() { - log.Panic(fmt.Errorf("Error - no URI found in token_transfer_confirmed event")) + log.Panic(fmt.Errorf("error - no URI found in token_transfer_confirmed event")) } } } @@ -719,6 +751,17 @@ func (pr *perfRunner) runLoop(tc TestCase) error { } startTime := time.Now() + + type ActionResponse struct { + trackingID string + err error + } + + actionResponses := make(chan *ActionResponse, tc.ActionsPerLoop()) + + var sentTime time.Time + var submissionSecondsPerLoop float64 + var eventReceivingSecondsPerLoop float64 trackingIDs := make([]string, 0) for actionsCompleted = 0; actionsCompleted < tc.ActionsPerLoop(); actionsCompleted++ { @@ -726,25 +769,44 @@ func (pr *perfRunner) runLoop(tc TestCase) error { if pr.allActionsComplete() { break } - - trackingID, err := tc.RunOnce() - - if err != nil { + actionCount := actionsCompleted + go func() { + trackingID, err := tc.RunOnce(actionCount) + log.Debugf("%d --> %s action %d sent after %f seconds", workerID, testName, actionCount, time.Since(startTime).Seconds()) + actionResponses <- &ActionResponse{ + trackingID: trackingID, + err: err, + } + }() + } + resultCount := 0 + for { + aResponse := <-actionResponses + resultCount++ + if aResponse.err != nil { if pr.cfg.DelinquentAction == conf.DelinquentActionExit.String() { - return err + return aResponse.err } else { - log.Errorf("Worker %d error running job (logging but continuing): %s", workerID, err) - err = nil - continue + log.Errorf("Worker %d error running job (logging but continuing): %s", workerID, aResponse.err) } } else { - trackingIDs = append(trackingIDs, trackingID) - pr.markTestInFlight(tc, trackingID) - log.Infof("%d --> %s Sent %s: %s", workerID, testName, idType, trackingID) + trackingIDs = append(trackingIDs, aResponse.trackingID) + pr.markTestInFlight(tc, aResponse.trackingID) + log.Debugf("%d --> %s Sent %s: %s", workerID, testName, idType, aResponse.trackingID) totalActionsCounter.Inc() } + // if we've reached the expected amount of metadata calls then stop + if resultCount == tc.ActionsPerLoop() { + submissionDurationPerLoop := time.Since(startTime) + pr.sendTime.Record(submissionDurationPerLoop) + submissionSecondsPerLoop = submissionDurationPerLoop.Seconds() + sentTime = time.Now() + log.Debugf("%d --> %s All actions sent %d after %f seconds", workerID, testName, resultCount, submissionSecondsPerLoop) + + pr.endSendTime = time.Now().Unix() + break + } } - if testName == conf.PerfTestTokenMint.String() && pr.cfg.SkipMintConfirmations { // For minting tests a worker can (if configured) skip waiting for a matching response event // before making itself available for the next job @@ -774,10 +836,23 @@ func (pr *perfRunner) runLoop(tc TestCase) error { pr.stopTrackingRequest(nextTrackingID) } } - log.Infof("%d <-- %s Finished (loop=%d)", workerID, testName, loop) + totalDurationPerLoop := time.Since(startTime) + pr.totalTime.Record(totalDurationPerLoop) + secondsPerLoop := totalDurationPerLoop.Seconds() + + eventReceivingDurationPerLoop := time.Since(sentTime) + eventReceivingSecondsPerLoop = eventReceivingDurationPerLoop.Seconds() + pr.receiveTime.Record(totalDurationPerLoop) + + total := submissionSecondsPerLoop + eventReceivingSecondsPerLoop + subPortion := int((submissionSecondsPerLoop / total) * 100) + envPortion := int((eventReceivingSecondsPerLoop / total) * 100) + log.Infof("%d <-- %s Finished (loop=%d), submission time: %f s, event receive time: %f s. Ratio (%d/%d) after %f seconds", workerID, testName, loop, submissionSecondsPerLoop, eventReceivingSecondsPerLoop, subPortion, envPortion, secondsPerLoop) if histErr == nil { - hist.Observe(time.Since(startTime).Seconds()) + log.Infof("%d <-- %s Emmiting (loop=%d) after %f seconds", workerID, testName, loop, secondsPerLoop) + + hist.Observe(secondsPerLoop) } loop++ @@ -982,13 +1057,13 @@ func (pr *perfRunner) detectDelinquentBalance() bool { func (pr *perfRunner) markTestInFlight(tc TestCase, trackingID string) { mutex.Lock() + defer mutex.Unlock() if len(trackingID) > 0 { pr.msgTimeMap[trackingID] = &inflightTest{ testCase: tc, time: time.Now(), } } - mutex.Unlock() } func (pr *perfRunner) recordCompletedAction() { @@ -998,14 +1073,14 @@ func (pr *perfRunner) recordCompletedAction() { pr.totalSummary++ } mutex.Lock() - mutex.Unlock() + defer mutex.Unlock() pr.calculateCurrentTps(true) } func (pr *perfRunner) stopTrackingRequest(trackingID string) { mutex.Lock() + defer mutex.Unlock() delete(pr.msgTimeMap, trackingID) - mutex.Unlock() } func (pr *perfRunner) createEthereumContractListener(nodeURL string) (string, error) { @@ -1062,7 +1137,7 @@ func (pr *perfRunner) createEthereumContractListener(nodeURL string) (string, er return "", err } if res.IsError() { - return "", fmt.Errorf("Failed: %s", errResponse) + return "", fmt.Errorf("failed: %s", errResponse) } id := responseBody["id"].(string) log.Infof("Created contract listener on %s: %s", nodeURL, id) @@ -1257,7 +1332,7 @@ func (pr *perfRunner) getMintRecipientBalance() (int, error) { SetError(&resError). Get(fullPath) if err != nil || res.IsError() { - return 0, fmt.Errorf("Error querying token balance [%d]: %s (%+v)", resStatus(res), err, &resError) + return 0, fmt.Errorf("error querying token balance [%d]: %s (%+v)", resStatus(res), err, &resError) } return response.Total, nil @@ -1272,7 +1347,7 @@ func (pr *perfRunner) getIdempotencyKey(workerId int, iteration int) string { workerIdStr := fmt.Sprintf("%05d", workerId) // Left pad iteration ID to 9 digits (supporting up to 999,999,999 iterations) iterationIdStr := fmt.Sprintf("%09d", iteration) - return fmt.Sprintf("%v-%s-%s", pr.startTime, workerIdStr, iterationIdStr) + return fmt.Sprintf("%v-%s-%s-%s", pr.startTime, workerIdStr, iterationIdStr, fftypes.NewUUID()) } func (pr *perfRunner) calculateCurrentTps(logValue bool) float64 { diff --git a/internal/perf/private_msg.go b/internal/perf/private_msg.go index 3f1646a..838fce6 100644 --- a/internal/perf/private_msg.go +++ b/internal/perf/private_msg.go @@ -32,7 +32,7 @@ func (tc *private) IDType() TrackingIDType { return TrackingIDTypeMessageID } -func (tc *private) RunOnce() (string, error) { +func (tc *private) RunOnce(iterationCount int) (string, error) { payload := fmt.Sprintf(`{ "data": [ diff --git a/internal/perf/token_mint.go b/internal/perf/token_mint.go index 64ccb48..66ba563 100644 --- a/internal/perf/token_mint.go +++ b/internal/perf/token_mint.go @@ -60,7 +60,7 @@ func (tc *tokenMint) GetSigningKey() string { return "" } -func (tc *tokenMint) RunOnce() (string, error) { +func (tc *tokenMint) RunOnce(iterationCount int) (string, error) { var payload string mintAmount := 10 if tc.pr.cfg.TokenOptions.TokenType == core.TokenTypeNonFungible.String() { diff --git a/internal/util/report_builder.go b/internal/util/report_builder.go new file mode 100644 index 0000000..88d3078 --- /dev/null +++ b/internal/util/report_builder.go @@ -0,0 +1,243 @@ +package util + +import ( + "fmt" + "html/template" + "os" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +type TestRunMetrics struct { + Name string + TotalActions string + Duration string + SendRate string + MinLatency string + MaxLatency string + AvgLatency string + Throughput string +} +type Report struct { + RunnerConfig string + TestInstanceName string + TestRuns []TestRunMetrics +} + +func (r *Report) GenerateHTML() error { + htmlTemplate := ` + + + + + + HyperLedger Firefly Performance Report + + + + + +
+

Test runner configuration

+ +
+{{.RunnerConfig}}
+            
+
+
+ +
+

Test metrics

+

+ Test instance:{{.TestInstanceName}} +

+
+ + + + + + + + + + + + {{range .TestRuns}} + + + + + + + + + + + {{end}} +
Test nameTest durationActionsSend TPSMin LatencyMax LatencyAvg LatencyThroughput
{{.Name}}{{.TotalActions}}{{.Duration}}{{.SendRate}}{{.MinLatency}}{{.MaxLatency}}{{.AvgLatency}}{{.Throughput}}
+
+
+ + +` + // Execute the template + tmpl, err := template.New("template").Parse(htmlTemplate) + if err != nil { + return err + } + + // Create or open the output file + outputFile, err := os.Create("ffperf-report.html") + if err != nil { + return err + } + defer outputFile.Close() + + // Write the HTML output to the file + err = tmpl.Execute(outputFile, r) + if err != nil { + return err + } + + return nil +} + +func (r *Report) AddTestRunMetrics(name string, totalActions int64, duration time.Duration, tps *TPS, lt *Latency) { + r.TestRuns = append(r.TestRuns, TestRunMetrics{ + Name: name, + TotalActions: fmt.Sprintf("%d", totalActions), + Duration: fmt.Sprintf("%s", duration), + SendRate: fmt.Sprintf("%f", tps.SendRate), + Throughput: fmt.Sprintf("%f", tps.Throughput), + MinLatency: lt.Min().String(), + MaxLatency: lt.Max().String(), + AvgLatency: lt.Avg().String(), + }) +} + +func NewReportForTestInstance(runnerConfig string, instanceName string) *Report { + return &Report{ + RunnerConfig: runnerConfig, + TestInstanceName: instanceName, + TestRuns: make([]TestRunMetrics, 0), + } +} + +type TPS struct { + SendRate float64 `json:"sendRate"` + Throughput float64 `json:"throughput"` +} + +func GenerateTPS(totalActions int64, startTime int64, endSendTime int64) *TPS { + sendDuration := time.Duration((endSendTime - startTime) * int64(time.Second)) + sendDurationSec := sendDuration.Seconds() + sendRate := float64(totalActions) / sendDurationSec + + totalDurationSec := time.Since(time.Unix(startTime, 0)).Seconds() + throughput := float64(totalActions) / totalDurationSec + log.Infof("Send rate: %f, Throughput: %f, Measured Actions: %v Duration: %v (Send duration: %v)", sendRate, throughput, totalActions, sendDurationSec, totalDurationSec) + return &TPS{ + SendRate: sendRate, + Throughput: throughput, + } +} + +type Latency struct { + mux sync.Mutex + min time.Duration + max time.Duration + total int64 + count int64 +} + +func (lt *Latency) Record(latency time.Duration) { + lt.mux.Lock() + defer lt.mux.Unlock() + if latency < lt.min || lt.min.Nanoseconds() == 0 { + lt.min = latency + } + if latency > lt.max { + lt.max = latency + } + lt.total += latency.Milliseconds() + lt.count++ +} + +func (lt *Latency) Avg() time.Duration { + return time.Duration((lt.total / lt.count) * int64(time.Millisecond)) +} + +func (lt *Latency) Min() time.Duration { + return lt.min +} + +func (lt *Latency) Max() time.Duration { + return lt.max +} +func (lt *Latency) String() string { + return fmt.Sprintf("min: %s, max: %s, avg: %s", lt.Min(), lt.Max(), lt.Avg()) +}