From 027d22c6a5452025adff81063a839aca6c406b49 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 26 Aug 2025 14:38:03 +0700 Subject: [PATCH 1/3] Add benchmark --- Makefile | 5 +- cmd/mpcium-benchmark/main.go | 614 +++++++++++++++++++++++++++++++++++ 2 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 cmd/mpcium-benchmark/main.go diff --git a/Makefile b/Makefile index bf640ec1..6313e1f0 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ BIN_DIR := bin all: build # Build both binaries -build: mpcium mpc +build: mpcium mpc benchmark # Install mpcium (builds and places it in $GOBIN or $GOPATH/bin) mpcium: @@ -25,6 +25,9 @@ install: sudo install -m 755 /tmp/mpcium-cli /usr/local/bin/ rm -f /tmp/mpcium /tmp/mpcium-cli @echo "Successfully installed mpcium and mpcium-cli to /usr/local/bin/" +# Install mpcium-benchmark +benchmark: + go install ./cmd/mpcium-benchmark # Run all tests test: diff --git a/cmd/mpcium-benchmark/main.go b/cmd/mpcium-benchmark/main.go new file mode 100644 index 00000000..d95d2294 --- /dev/null +++ b/cmd/mpcium-benchmark/main.go @@ -0,0 +1,614 @@ +package main + +import ( + "context" + "crypto/rand" + "fmt" + "log" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/types" + "github.com/nats-io/nats.go" + "github.com/urfave/cli/v3" +) + +type BenchmarkResult struct { + TotalOperations int + SuccessfulOps int + FailedOps int + TotalTime time.Duration + AverageTime time.Duration + MedianTime time.Duration + OperationTimes []time.Duration + ErrorRate float64 + OperationsPerSec float64 +} + +func main() { + app := &cli.Command{ + Name: "mpcium-benchmark", + Usage: "Benchmark tool for MPC operations", + Description: "Run benchmarks for keygen, signing (ECDSA/EdDSA), and resharing operations", + Commands: []*cli.Command{ + keygenBenchmarkCommand(), + ecdsaSignBenchmarkCommand(), + eddsaSignBenchmarkCommand(), + reshareBenchmarkCommand(), + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "nats-url", + Usage: "NATS server URL", + Value: "nats://localhost:4222", + Category: "connection", + }, + &cli.StringFlag{ + Name: "key-path", + Usage: "Path to event initiator private key", + Value: "./event_initiator.key", + Category: "authentication", + }, + &cli.StringFlag{ + Name: "password", + Usage: "Password for encrypted key (if needed)", + Category: "authentication", + }, + }, + } + + if err := app.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} + +func keygenBenchmarkCommand() *cli.Command { + return &cli.Command{ + Name: "keygen", + Usage: "Benchmark keygen operations", + ArgsUsage: "", + Action: runKeygenBenchmark, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout per operation in seconds", + Value: 30, + Aliases: []string{"t"}, + }, + }, + } +} + +func ecdsaSignBenchmarkCommand() *cli.Command { + return &cli.Command{ + Name: "sign-ecdsa", + Usage: "Benchmark ECDSA signing operations", + ArgsUsage: " ", + Action: runECDSASignBenchmark, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout per operation in seconds", + Value: 30, + Aliases: []string{"t"}, + }, + }, + } +} + +func eddsaSignBenchmarkCommand() *cli.Command { + return &cli.Command{ + Name: "sign-eddsa", + Usage: "Benchmark EdDSA signing operations", + ArgsUsage: " ", + Action: runEdDSASignBenchmark, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout per operation in seconds", + Value: 30, + Aliases: []string{"t"}, + }, + }, + } +} + +func reshareBenchmarkCommand() *cli.Command { + return &cli.Command{ + Name: "reshare", + Usage: "Benchmark reshare operations", + ArgsUsage: " ", + Action: runReshareBenchmark, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout per operation in seconds", + Value: 30, + Aliases: []string{"t"}, + }, + &cli.IntFlag{ + Name: "new-threshold", + Usage: "New threshold for resharing", + Value: 2, + Aliases: []string{"nt"}, + }, + &cli.StringSliceFlag{ + Name: "node-ids", + Usage: "Node IDs for resharing (comma separated)", + Aliases: []string{"n"}, + }, + }, + } +} + +func createMPCClient(cmd *cli.Command) (client.MPCClient, error) { + natsURL := cmd.String("nats-url") + keyPath := cmd.String("key-path") + password := cmd.String("password") + + nc, err := nats.Connect(natsURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to NATS: %w", err) + } + + opts := client.Options{ + NatsConn: nc, + KeyPath: keyPath, + Password: password, + } + return client.NewMPCClient(opts), nil +} + +func runKeygenBenchmark(ctx context.Context, cmd *cli.Command) error { + if cmd.Args().Len() < 1 { + return fmt.Errorf("missing required argument: num_operations") + } + + numOps := cmd.Args().Get(0) + n, err := parseNumOps(numOps) + if err != nil { + return err + } + + timeout := time.Duration(cmd.Int("timeout")) * time.Second + + mpcClient, err := createMPCClient(cmd) + if err != nil { + return err + } + + fmt.Printf("Starting keygen benchmark with %d operations...\n", n) + + var results []OperationResult + var wg sync.WaitGroup + var mu sync.Mutex + + // Set up result listener + err = mpcClient.OnWalletCreationResult(func(result event.KeygenResultEvent) { + mu.Lock() + defer mu.Unlock() + + for i := range results { + if results[i].ID == result.WalletID && !results[i].Completed { + results[i].EndTime = time.Now() + results[i].Completed = true + results[i].Success = result.ResultType == event.ResultTypeSuccess + if !results[i].Success { + results[i].ErrorReason = result.ErrorReason + results[i].ErrorCode = result.ErrorCode + } + wg.Done() + break + } + } + }) + if err != nil { + return fmt.Errorf("failed to set up result listener: %w", err) + } + + // Run operations + startTime := time.Now() + for i := 0; i < n; i++ { + walletID := fmt.Sprintf("benchmark-keygen-%d-%d", time.Now().UnixNano(), i) + + result := OperationResult{ + ID: walletID, + StartTime: time.Now(), + } + + mu.Lock() + results = append(results, result) + mu.Unlock() + + wg.Add(1) + + err := mpcClient.CreateWallet(walletID) + if err != nil { + mu.Lock() + results[i].Completed = true + results[i].Success = false + results[i].ErrorReason = err.Error() + results[i].EndTime = time.Now() + mu.Unlock() + wg.Done() + } + + // Add small delay between operations to avoid overwhelming the system + time.Sleep(10 * time.Millisecond) + } + + // Wait for all operations with timeout + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + // All operations completed + case <-time.After(timeout * time.Duration(n)): + fmt.Println("Timeout reached, some operations may still be pending") + } + + totalTime := time.Since(startTime) + + // Calculate results + benchResult := calculateBenchmarkResult(results, totalTime) + printBenchmarkResult("Keygen", benchResult) + + return nil +} + +func runECDSASignBenchmark(ctx context.Context, cmd *cli.Command) error { + return runSignBenchmark(ctx, cmd, types.KeyTypeSecp256k1, "ECDSA") +} + +func runEdDSASignBenchmark(ctx context.Context, cmd *cli.Command) error { + return runSignBenchmark(ctx, cmd, types.KeyTypeEd25519, "EdDSA") +} + +func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyType, keyTypeName string) error { + if cmd.Args().Len() < 2 { + return fmt.Errorf("missing required arguments: num_operations and wallet_id") + } + + numOps := cmd.Args().Get(0) + walletID := cmd.Args().Get(1) + + n, err := parseNumOps(numOps) + if err != nil { + return err + } + + timeout := time.Duration(cmd.Int("timeout")) * time.Second + + mpcClient, err := createMPCClient(cmd) + if err != nil { + return err + } + + fmt.Printf("Starting %s signing benchmark with %d operations for wallet %s...\n", keyTypeName, n, walletID) + + var results []OperationResult + var wg sync.WaitGroup + var mu sync.Mutex + + // Set up result listener + err = mpcClient.OnSignResult(func(result event.SigningResultEvent) { + mu.Lock() + defer mu.Unlock() + + for i := range results { + if results[i].ID == result.TxID && !results[i].Completed { + results[i].EndTime = time.Now() + results[i].Completed = true + results[i].Success = result.ResultType == event.ResultTypeSuccess + if !results[i].Success { + results[i].ErrorReason = result.ErrorReason + results[i].ErrorCode = string(result.ErrorCode) + } + wg.Done() + break + } + } + }) + if err != nil { + return fmt.Errorf("failed to set up result listener: %w", err) + } + + // Run operations + startTime := time.Now() + for i := 0; i < n; i++ { + txID := fmt.Sprintf("benchmark-%s-sign-%d-%d", keyTypeName, time.Now().UnixNano(), i) + + // Generate random transaction data + txData := make([]byte, 32) + rand.Read(txData) + + msg := &types.SignTxMessage{ + KeyType: keyType, + WalletID: walletID, + NetworkInternalCode: "benchmark", + TxID: txID, + Tx: txData, + } + + result := OperationResult{ + ID: txID, + StartTime: time.Now(), + } + + mu.Lock() + results = append(results, result) + mu.Unlock() + + wg.Add(1) + + err := mpcClient.SignTransaction(msg) + if err != nil { + mu.Lock() + results[i].Completed = true + results[i].Success = false + results[i].ErrorReason = err.Error() + results[i].EndTime = time.Now() + mu.Unlock() + wg.Done() + } + + // Add small delay between operations + time.Sleep(10 * time.Millisecond) + } + + // Wait for all operations with timeout + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + // All operations completed + case <-time.After(timeout * time.Duration(n)): + fmt.Println("Timeout reached, some operations may still be pending") + } + + totalTime := time.Since(startTime) + + // Calculate results + benchResult := calculateBenchmarkResult(results, totalTime) + printBenchmarkResult(fmt.Sprintf("%s Signing", keyTypeName), benchResult) + + return nil +} + +func runReshareBenchmark(ctx context.Context, cmd *cli.Command) error { + if cmd.Args().Len() < 2 { + return fmt.Errorf("missing required arguments: num_operations and wallet_id") + } + + numOps := cmd.Args().Get(0) + walletID := cmd.Args().Get(1) + + n, err := parseNumOps(numOps) + if err != nil { + return err + } + + timeout := time.Duration(cmd.Int("timeout")) * time.Second + newThreshold := cmd.Int("new-threshold") + nodeIDs := cmd.StringSlice("node-ids") + + if len(nodeIDs) == 0 { + return fmt.Errorf("node-ids are required for resharing benchmark") + } + + mpcClient, err := createMPCClient(cmd) + if err != nil { + return err + } + + fmt.Printf("Starting reshare benchmark with %d operations for wallet %s...\n", n, walletID) + + var results []OperationResult + var wg sync.WaitGroup + var mu sync.Mutex + + // Set up result listener + err = mpcClient.OnResharingResult(func(result event.ResharingResultEvent) { + mu.Lock() + defer mu.Unlock() + + // For resharing, find the first incomplete operation for this wallet + found := false + for i := range results { + if strings.HasPrefix(results[i].ID, result.WalletID) && !results[i].Completed { + results[i].EndTime = time.Now() + results[i].Completed = true + results[i].Success = result.ResultType == event.ResultTypeSuccess + if !results[i].Success { + results[i].ErrorReason = result.ErrorReason + results[i].ErrorCode = result.ErrorCode + } + wg.Done() + found = true + break + } + } + if !found { + fmt.Printf("Warning: Received reshare result for wallet %s but no matching pending operation found\n", result.WalletID) + } + }) + if err != nil { + return fmt.Errorf("failed to set up result listener: %w", err) + } + + // Run operations + startTime := time.Now() + for i := 0; i < n; i++ { + sessionID := fmt.Sprintf("benchmark-reshare-%d-%d", time.Now().UnixNano(), i) + + msg := &types.ResharingMessage{ + SessionID: sessionID, + NodeIDs: nodeIDs, + NewThreshold: newThreshold, + KeyType: types.KeyTypeSecp256k1, // Default to secp256k1 + WalletID: walletID, + } + + result := OperationResult{ + ID: fmt.Sprintf("%s-%d", walletID, i), + StartTime: time.Now(), + } + + mu.Lock() + results = append(results, result) + mu.Unlock() + + wg.Add(1) + + err := mpcClient.Resharing(msg) + if err != nil { + mu.Lock() + results[i].Completed = true + results[i].Success = false + results[i].ErrorReason = err.Error() + results[i].EndTime = time.Now() + mu.Unlock() + wg.Done() + } + + // Add small delay between operations + time.Sleep(10 * time.Millisecond) + } + + // Wait for all operations with timeout + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + // All operations completed + case <-time.After(timeout * time.Duration(n)): + fmt.Println("Timeout reached, some operations may still be pending") + } + + totalTime := time.Since(startTime) + + // Calculate results + benchResult := calculateBenchmarkResult(results, totalTime) + printBenchmarkResult("Reshare", benchResult) + + return nil +} + +type OperationResult struct { + ID string + StartTime time.Time + EndTime time.Time + Completed bool + Success bool + ErrorReason string + ErrorCode string +} + +func parseNumOps(numOps string) (int, error) { + var n int + _, err := fmt.Sscanf(numOps, "%d", &n) + if err != nil { + return 0, fmt.Errorf("invalid number of operations: %s", numOps) + } + if n <= 0 { + return 0, fmt.Errorf("number of operations must be positive") + } + return n, nil +} + +func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration) BenchmarkResult { + var operationTimes []time.Duration + successfulOps := 0 + failedOps := 0 + + for _, result := range results { + if result.Completed { + if result.Success { + successfulOps++ + if !result.EndTime.IsZero() { + operationTimes = append(operationTimes, result.EndTime.Sub(result.StartTime)) + } + } else { + failedOps++ + } + } else { + failedOps++ // Uncompleted operations are considered failed + } + } + + totalOperations := len(results) + errorRate := float64(failedOps) / float64(totalOperations) * 100 + + var averageTime, medianTime time.Duration + var operationsPerSec float64 + + if len(operationTimes) > 0 { + // Calculate average + var totalOpTime time.Duration + for _, opTime := range operationTimes { + totalOpTime += opTime + } + averageTime = totalOpTime / time.Duration(len(operationTimes)) + + // Calculate median + sort.Slice(operationTimes, func(i, j int) bool { + return operationTimes[i] < operationTimes[j] + }) + if len(operationTimes)%2 == 0 { + medianTime = (operationTimes[len(operationTimes)/2-1] + operationTimes[len(operationTimes)/2]) / 2 + } else { + medianTime = operationTimes[len(operationTimes)/2] + } + + // Calculate operations per second + operationsPerSec = float64(successfulOps) / totalTime.Seconds() + } + + return BenchmarkResult{ + TotalOperations: totalOperations, + SuccessfulOps: successfulOps, + FailedOps: failedOps, + TotalTime: totalTime, + AverageTime: averageTime, + MedianTime: medianTime, + OperationTimes: operationTimes, + ErrorRate: errorRate, + OperationsPerSec: operationsPerSec, + } +} + +func printBenchmarkResult(operationType string, result BenchmarkResult) { + fmt.Printf("\n=== %s Benchmark Results ===\n", operationType) + fmt.Printf("Total Operations: %d\n", result.TotalOperations) + fmt.Printf("Successful: %d\n", result.SuccessfulOps) + fmt.Printf("Failed: %d\n", result.FailedOps) + fmt.Printf("Error Rate: %.2f%%\n", result.ErrorRate) + fmt.Printf("Total Time: %v\n", result.TotalTime) + fmt.Printf("Average Time/Op: %v\n", result.AverageTime) + fmt.Printf("Median Time/Op: %v\n", result.MedianTime) + fmt.Printf("Operations/Second: %.2f\n", result.OperationsPerSec) + + if len(result.OperationTimes) > 0 { + fmt.Printf("Min Time/Op: %v\n", result.OperationTimes[0]) + fmt.Printf("Max Time/Op: %v\n", result.OperationTimes[len(result.OperationTimes)-1]) + } + + fmt.Println("=====================================") +} From 189cb6ad226dd1632046daca47c5fb5d9707c3c3 Mon Sep 17 00:00:00 2001 From: anhthii Date: Sat, 20 Sep 2025 09:51:56 +0700 Subject: [PATCH 2/3] Refine benchmark --- cmd/mpcium-benchmark/main.go | 221 ++++++++++++++++++++++++++--------- 1 file changed, 164 insertions(+), 57 deletions(-) diff --git a/cmd/mpcium-benchmark/main.go b/cmd/mpcium-benchmark/main.go index d95d2294..908184a4 100644 --- a/cmd/mpcium-benchmark/main.go +++ b/cmd/mpcium-benchmark/main.go @@ -27,7 +27,10 @@ type BenchmarkResult struct { MedianTime time.Duration OperationTimes []time.Duration ErrorRate float64 - OperationsPerSec float64 + OperationsPerMin float64 + BatchSize int + TotalBatches int + BatchTimes []time.Duration } func main() { @@ -80,6 +83,12 @@ func keygenBenchmarkCommand() *cli.Command { Value: 30, Aliases: []string{"t"}, }, + &cli.IntFlag{ + Name: "batch-size", + Usage: "Number of operations per batch", + Value: 10, + Aliases: []string{"b"}, + }, }, } } @@ -97,6 +106,12 @@ func ecdsaSignBenchmarkCommand() *cli.Command { Value: 30, Aliases: []string{"t"}, }, + &cli.IntFlag{ + Name: "batch-size", + Usage: "Number of operations per batch", + Value: 10, + Aliases: []string{"b"}, + }, }, } } @@ -114,6 +129,12 @@ func eddsaSignBenchmarkCommand() *cli.Command { Value: 30, Aliases: []string{"t"}, }, + &cli.IntFlag{ + Name: "batch-size", + Usage: "Number of operations per batch", + Value: 10, + Aliases: []string{"b"}, + }, }, } } @@ -131,6 +152,12 @@ func reshareBenchmarkCommand() *cli.Command { Value: 30, Aliases: []string{"t"}, }, + &cli.IntFlag{ + Name: "batch-size", + Usage: "Number of operations per batch", + Value: 10, + Aliases: []string{"b"}, + }, &cli.IntFlag{ Name: "new-threshold", Usage: "New threshold for resharing", @@ -156,11 +183,22 @@ func createMPCClient(cmd *cli.Command) (client.MPCClient, error) { return nil, fmt.Errorf("failed to connect to NATS: %w", err) } - opts := client.Options{ - NatsConn: nc, + // Create a LocalSigner with the provided key path and password + signerOpts := client.LocalSignerOptions{ KeyPath: keyPath, Password: password, } + + // Default to Ed25519 for event initiator keys + signer, err := client.NewLocalSigner(types.EventInitiatorKeyTypeEd25519, signerOpts) + if err != nil { + return nil, fmt.Errorf("failed to create signer: %w", err) + } + + opts := client.Options{ + NatsConn: nc, + Signer: signer, + } return client.NewMPCClient(opts), nil } @@ -259,7 +297,7 @@ func runKeygenBenchmark(ctx context.Context, cmd *cli.Command) error { totalTime := time.Since(startTime) // Calculate results - benchResult := calculateBenchmarkResult(results, totalTime) + benchResult := calculateBenchmarkResult(results, totalTime, 1, []time.Duration{totalTime}) printBenchmarkResult("Keygen", benchResult) return nil @@ -287,13 +325,15 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy } timeout := time.Duration(cmd.Int("timeout")) * time.Second + batchSize := cmd.Int("batch-size") mpcClient, err := createMPCClient(cmd) if err != nil { return err } - fmt.Printf("Starting %s signing benchmark with %d operations for wallet %s...\n", keyTypeName, n, walletID) + totalBatches := (n + batchSize - 1) / batchSize + fmt.Printf("Starting %s signing benchmark with %d operations (%d batches of %d) for wallet %s...\n", keyTypeName, n, totalBatches, batchSize, walletID) var results []OperationResult var wg sync.WaitGroup @@ -322,47 +362,79 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy return fmt.Errorf("failed to set up result listener: %w", err) } - // Run operations + // Run operations in batches startTime := time.Now() - for i := 0; i < n; i++ { - txID := fmt.Sprintf("benchmark-%s-sign-%d-%d", keyTypeName, time.Now().UnixNano(), i) - - // Generate random transaction data - txData := make([]byte, 32) - rand.Read(txData) - - msg := &types.SignTxMessage{ - KeyType: keyType, - WalletID: walletID, - NetworkInternalCode: "benchmark", - TxID: txID, - Tx: txData, + var batchTimes []time.Duration + + // Start progress reporting goroutine + progressTicker := time.NewTicker(10 * time.Second) + defer progressTicker.Stop() + go func() { + for range progressTicker.C { + mu.Lock() + completed := 0 + for _, r := range results { + if r.Completed { + completed++ + } + } + mu.Unlock() + fmt.Printf("Progress: %d/%d results received\n", completed, n) } + }() - result := OperationResult{ - ID: txID, - StartTime: time.Now(), + for batchNum := 0; batchNum < totalBatches; batchNum++ { + batchStart := time.Now() + batchStartIdx := batchNum * batchSize + batchEndIdx := batchStartIdx + batchSize + if batchEndIdx > n { + batchEndIdx = n } - mu.Lock() - results = append(results, result) - mu.Unlock() + fmt.Printf("Starting batch %d/%d (%d operations)...\n", batchNum+1, totalBatches, batchEndIdx-batchStartIdx) - wg.Add(1) + for i := batchStartIdx; i < batchEndIdx; i++ { + txID := fmt.Sprintf("benchmark-%s-sign-%d-%d", keyTypeName, time.Now().UnixNano(), i) + + // Generate random transaction data + txData := make([]byte, 32) + rand.Read(txData) + + msg := &types.SignTxMessage{ + KeyType: keyType, + WalletID: walletID, + NetworkInternalCode: "benchmark", + TxID: txID, + Tx: txData, + } + + result := OperationResult{ + ID: txID, + StartTime: time.Now(), + } - err := mpcClient.SignTransaction(msg) - if err != nil { mu.Lock() - results[i].Completed = true - results[i].Success = false - results[i].ErrorReason = err.Error() - results[i].EndTime = time.Now() + results = append(results, result) mu.Unlock() - wg.Done() + + wg.Add(1) + + err := mpcClient.SignTransaction(msg) + if err != nil { + mu.Lock() + results[i].Completed = true + results[i].Success = false + results[i].ErrorReason = err.Error() + results[i].EndTime = time.Now() + mu.Unlock() + wg.Done() + } + + // Add small delay between operations + time.Sleep(10 * time.Millisecond) } - // Add small delay between operations - time.Sleep(10 * time.Millisecond) + batchTimes = append(batchTimes, time.Since(batchStart)) } // Wait for all operations with timeout @@ -382,7 +454,7 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy totalTime := time.Since(startTime) // Calculate results - benchResult := calculateBenchmarkResult(results, totalTime) + benchResult := calculateBenchmarkResult(results, totalTime, batchSize, batchTimes) printBenchmarkResult(fmt.Sprintf("%s Signing", keyTypeName), benchResult) return nil @@ -505,7 +577,7 @@ func runReshareBenchmark(ctx context.Context, cmd *cli.Command) error { totalTime := time.Since(startTime) // Calculate results - benchResult := calculateBenchmarkResult(results, totalTime) + benchResult := calculateBenchmarkResult(results, totalTime, 1, []time.Duration{totalTime}) printBenchmarkResult("Reshare", benchResult) return nil @@ -533,7 +605,7 @@ func parseNumOps(numOps string) (int, error) { return n, nil } -func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration) BenchmarkResult { +func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration, batchSize int, batchTimes []time.Duration) BenchmarkResult { var operationTimes []time.Duration successfulOps := 0 failedOps := 0 @@ -577,8 +649,8 @@ func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration medianTime = operationTimes[len(operationTimes)/2] } - // Calculate operations per second - operationsPerSec = float64(successfulOps) / totalTime.Seconds() + // Calculate operations per minute + operationsPerSec = float64(successfulOps) / totalTime.Minutes() } return BenchmarkResult{ @@ -590,25 +662,60 @@ func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration MedianTime: medianTime, OperationTimes: operationTimes, ErrorRate: errorRate, - OperationsPerSec: operationsPerSec, + OperationsPerMin: operationsPerSec, + BatchSize: batchSize, + TotalBatches: len(batchTimes), + BatchTimes: batchTimes, } } func printBenchmarkResult(operationType string, result BenchmarkResult) { - fmt.Printf("\n=== %s Benchmark Results ===\n", operationType) - fmt.Printf("Total Operations: %d\n", result.TotalOperations) - fmt.Printf("Successful: %d\n", result.SuccessfulOps) - fmt.Printf("Failed: %d\n", result.FailedOps) - fmt.Printf("Error Rate: %.2f%%\n", result.ErrorRate) - fmt.Printf("Total Time: %v\n", result.TotalTime) - fmt.Printf("Average Time/Op: %v\n", result.AverageTime) - fmt.Printf("Median Time/Op: %v\n", result.MedianTime) - fmt.Printf("Operations/Second: %.2f\n", result.OperationsPerSec) - - if len(result.OperationTimes) > 0 { - fmt.Printf("Min Time/Op: %v\n", result.OperationTimes[0]) - fmt.Printf("Max Time/Op: %v\n", result.OperationTimes[len(result.OperationTimes)-1]) - } - - fmt.Println("=====================================") + fmt.Println() + fmt.Println("===============================") + fmt.Printf("BENCHMARK RESULTS SUMMARY\n") + fmt.Println("===============================") + fmt.Printf("Total benchmark time: %v\n", result.TotalTime) + fmt.Printf("Total batches sent: %d\n", result.TotalBatches) + fmt.Printf("Total requests sent: %d\n", result.TotalOperations) + fmt.Printf("Successful completions: %d\n", result.SuccessfulOps) + fmt.Printf("Success rate: %.2f%%\n", 100.0-result.ErrorRate) + fmt.Printf("Average signs per minute: %.2f\n", result.OperationsPerMin) + + fmt.Println() + fmt.Println("------------------------------") + fmt.Printf("%d REQUEST ANALYSIS\n", result.BatchSize) + fmt.Println("------------------------------") + + if len(result.OperationTimes) >= result.BatchSize { + firstNResults := result.OperationTimes[:result.BatchSize] + if len(firstNResults) > len(result.OperationTimes) { + firstNResults = result.OperationTimes + } + + completedCount := len(firstNResults) + if completedCount > result.BatchSize { + completedCount = result.BatchSize + } + + fmt.Printf("Completed from first %d: %d/%d\n", result.BatchSize, completedCount, result.BatchSize) + + if len(firstNResults) > 0 { + var totalTime time.Duration + minTime := firstNResults[0] + maxTime := firstNResults[0] + + for _, t := range firstNResults { + totalTime += t + if t < minTime { + minTime = t + } + if t > maxTime { + maxTime = t + } + } + + fmt.Printf("Fastest (first %d): %v\n", result.BatchSize, minTime) + fmt.Printf("Slowest (first %d): %v\n", result.BatchSize, maxTime) + } + } } From 1d07ffb3a3a39d425da5e5c5a5de5eb758032a09 Mon Sep 17 00:00:00 2001 From: anhthii Date: Thu, 25 Sep 2025 21:46:26 +0700 Subject: [PATCH 3/3] Add benchmark command --- Makefile | 5 +- README.md | 28 ++ benchmark/result-ecdsa.txt | 54 +++ benchmark/result-eddsa.txt | 54 +++ .../main.go => mpcium-cli/benchmark.go} | 312 ++++++++++++++---- cmd/mpcium-cli/main.go | 1 + 6 files changed, 392 insertions(+), 62 deletions(-) create mode 100644 benchmark/result-ecdsa.txt create mode 100644 benchmark/result-eddsa.txt rename cmd/{mpcium-benchmark/main.go => mpcium-cli/benchmark.go} (64%) diff --git a/Makefile b/Makefile index 6313e1f0..bf640ec1 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ BIN_DIR := bin all: build # Build both binaries -build: mpcium mpc benchmark +build: mpcium mpc # Install mpcium (builds and places it in $GOBIN or $GOPATH/bin) mpcium: @@ -25,9 +25,6 @@ install: sudo install -m 755 /tmp/mpcium-cli /usr/local/bin/ rm -f /tmp/mpcium /tmp/mpcium-cli @echo "Successfully installed mpcium and mpcium-cli to /usr/local/bin/" -# Install mpcium-benchmark -benchmark: - go install ./cmd/mpcium-benchmark # Run all tests test: diff --git a/README.md b/README.md index 559da160..ae5edf35 100644 --- a/README.md +++ b/README.md @@ -423,3 +423,31 @@ go test ./... -v cd e2e make test ``` + +## Benchmarking + +Test MPC performance with the integrated benchmark tool: + +### Keygen Benchmark +```bash +# Test wallet creation +mpcium-cli benchmark keygen 10 + +# With config and output file +mpcium-cli benchmark --config config.yaml --output results.txt keygen 50 +``` + +### Signing Benchmark +```bash +# Test ECDSA signing +mpcium-cli benchmark sign-ecdsa 100 wallet-id + +# Test EdDSA signing +mpcium-cli benchmark sign-eddsa 100 wallet-id + +# With custom batch size and output +mpcium-cli benchmark --config config.yaml --output signing-results.txt \ + sign-ecdsa 1000 wallet-id --batch-size 20 +``` + +Use `--prompt-password` for secure key password input and `--help` for all options. diff --git a/benchmark/result-ecdsa.txt b/benchmark/result-ecdsa.txt new file mode 100644 index 00000000..8fe3781c --- /dev/null +++ b/benchmark/result-ecdsa.txt @@ -0,0 +1,54 @@ + +=============================== +EDDSA SIGNING BENCHMARK RESULTS SUMMARY +=============================== +Timestamp: 2025-09-25T20:40:38+07:00 +Operation Type: EdDSA Signing +Total benchmark time: 1m7.976697225s +Total batches sent: 125 +Total requests sent: 1000 +Successful completions: 1000 +Failed operations: 0 +Success rate: 100.00% +Error rate: 0.00% +Average operations per minute: 882.66 +Average operation time: 28.657688821s +Median operation time: 28.655265336s + +------------------------------ +8 REQUEST ANALYSIS +------------------------------ +Completed from first 8: 8/8 +Fastest (first 8): 199.81656ms +Slowest (first 8): 590.329859ms + + +================================================================================ + + +=============================== +ECDSA SIGNING BENCHMARK RESULTS SUMMARY +=============================== +Timestamp: 2025-09-25T20:44:54+07:00 +Operation Type: ECDSA Signing +Total benchmark time: 3m45.068993117s +Total batches sent: 125 +Total requests sent: 1000 +Successful completions: 1000 +Failed operations: 0 +Success rate: 100.00% +Error rate: 0.00% +Average operations per minute: 266.58 +Average operation time: 1m48.220676872s +Median operation time: 1m48.41344833s + +------------------------------ +8 REQUEST ANALYSIS +------------------------------ +Completed from first 8: 8/8 +Fastest (first 8): 434.642477ms +Slowest (first 8): 1.810709039s + + +================================================================================ + diff --git a/benchmark/result-eddsa.txt b/benchmark/result-eddsa.txt new file mode 100644 index 00000000..d449fc81 --- /dev/null +++ b/benchmark/result-eddsa.txt @@ -0,0 +1,54 @@ + +=============================== +EDDSA SIGNING BENCHMARK RESULTS SUMMARY +=============================== +Timestamp: 2025-09-25T20:52:35+07:00 +Operation Type: EdDSA Signing +Total benchmark time: 1m5.271427885s +Total batches sent: 125 +Total requests sent: 1000 +Successful completions: 1000 +Failed operations: 0 +Success rate: 100.00% +Error rate: 0.00% +Average operations per minute: 919.24 +Average operation time: 27.639836611s +Median operation time: 28.132345011s + +------------------------------ +8 REQUEST ANALYSIS +------------------------------ +Completed from first 8: 8/8 +Fastest (first 8): 164.535132ms +Slowest (first 8): 488.81932ms + + +================================================================================ + + +=============================== +EDDSA SIGNING BENCHMARK RESULTS SUMMARY +=============================== +Timestamp: 2025-09-25T21:01:04+07:00 +Operation Type: EdDSA Signing +Total benchmark time: 1m4.385020314s +Total batches sent: 125 +Total requests sent: 1000 +Successful completions: 999 +Failed operations: 1 +Success rate: 99.90% +Error rate: 0.10% +Average operations per minute: 930.96 +Average operation time: 26.909120645s +Median operation time: 26.883269259s + +------------------------------ +8 REQUEST ANALYSIS +------------------------------ +Completed from first 8: 8/8 +Fastest (first 8): 169.161193ms +Slowest (first 8): 457.503189ms + + +================================================================================ + diff --git a/cmd/mpcium-benchmark/main.go b/cmd/mpcium-cli/benchmark.go similarity index 64% rename from cmd/mpcium-benchmark/main.go rename to cmd/mpcium-cli/benchmark.go index 908184a4..9754bbd5 100644 --- a/cmd/mpcium-benchmark/main.go +++ b/cmd/mpcium-cli/benchmark.go @@ -3,19 +3,25 @@ package main import ( "context" "crypto/rand" + "encoding/hex" "fmt" - "log" "os" + "path/filepath" "sort" "strings" "sync" + "syscall" "time" "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/constant" "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" "github.com/fystack/mpcium/pkg/types" "github.com/nats-io/nats.go" "github.com/urfave/cli/v3" + "golang.org/x/term" ) type BenchmarkResult struct { @@ -33,9 +39,19 @@ type BenchmarkResult struct { BatchTimes []time.Duration } -func main() { - app := &cli.Command{ - Name: "mpcium-benchmark", +type OperationResult struct { + ID string + StartTime time.Time + EndTime time.Time + Completed bool + Success bool + ErrorReason string + ErrorCode string +} + +func benchmarkCommand() *cli.Command { + return &cli.Command{ + Name: "benchmark", Usage: "Benchmark tool for MPC operations", Description: "Run benchmarks for keygen, signing (ECDSA/EdDSA), and resharing operations", Commands: []*cli.Command{ @@ -46,10 +62,10 @@ func main() { }, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "nats-url", - Usage: "NATS server URL", - Value: "nats://localhost:4222", - Category: "connection", + Name: "config", + Aliases: []string{"c"}, + Usage: "Path to configuration file", + Category: "configuration", }, &cli.StringFlag{ Name: "key-path", @@ -57,17 +73,28 @@ func main() { Value: "./event_initiator.key", Category: "authentication", }, - &cli.StringFlag{ - Name: "password", - Usage: "Password for encrypted key (if needed)", + &cli.BoolFlag{ + Name: "prompt-password", + Aliases: []string{"p"}, + Usage: "Prompt for encrypted key password (secure)", + Value: false, Category: "authentication", }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + Value: false, + Category: "logging", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Output file for benchmark results", + Value: "benchmark/output.txt", + Category: "output", + }, }, } - - if err := app.Run(context.Background(), os.Args); err != nil { - log.Fatal(err) - } } func keygenBenchmarkCommand() *cli.Command { @@ -80,7 +107,7 @@ func keygenBenchmarkCommand() *cli.Command { &cli.IntFlag{ Name: "timeout", Usage: "Timeout per operation in seconds", - Value: 30, + Value: 60, Aliases: []string{"t"}, }, &cli.IntFlag{ @@ -103,7 +130,7 @@ func ecdsaSignBenchmarkCommand() *cli.Command { &cli.IntFlag{ Name: "timeout", Usage: "Timeout per operation in seconds", - Value: 30, + Value: 60, Aliases: []string{"t"}, }, &cli.IntFlag{ @@ -126,7 +153,7 @@ func eddsaSignBenchmarkCommand() *cli.Command { &cli.IntFlag{ Name: "timeout", Usage: "Timeout per operation in seconds", - Value: 30, + Value: 60, Aliases: []string{"t"}, }, &cli.IntFlag{ @@ -149,7 +176,7 @@ func reshareBenchmarkCommand() *cli.Command { &cli.IntFlag{ Name: "timeout", Usage: "Timeout per operation in seconds", - Value: 30, + Value: 60, Aliases: []string{"t"}, }, &cli.IntFlag{ @@ -174,15 +201,34 @@ func reshareBenchmarkCommand() *cli.Command { } func createMPCClient(cmd *cli.Command) (client.MPCClient, error) { - natsURL := cmd.String("nats-url") + configPath := cmd.String("config") keyPath := cmd.String("key-path") - password := cmd.String("password") + promptPassword := cmd.Bool("prompt-password") + debug := cmd.Bool("debug") + + // Initialize configuration + config.InitViperConfig(configPath) + appConfig := config.LoadConfig() + environment := appConfig.Environment + + // Initialize logger + logger.Init(environment, debug) - nc, err := nats.Connect(natsURL) + // Create NATS connection using the same logic as main mpcium + nc, err := getNATSConnection(environment, appConfig) if err != nil { return nil, fmt.Errorf("failed to connect to NATS: %w", err) } + // Handle password prompting + var password string + if promptPassword { + password, err = promptForPassword() + if err != nil { + return nil, fmt.Errorf("failed to get password: %w", err) + } + } + // Create a LocalSigner with the provided key path and password signerOpts := client.LocalSignerOptions{ KeyPath: keyPath, @@ -202,6 +248,85 @@ func createMPCClient(cmd *cli.Command) (client.MPCClient, error) { return client.NewMPCClient(opts), nil } +// promptForPassword securely prompts for a password without echoing to terminal +func promptForPassword() (string, error) { + fmt.Print("Enter password for encrypted key: ") + passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() // Add newline after password input + if err != nil { + return "", fmt.Errorf("failed to read password: %w", err) + } + + password := string(passwordBytes) + if len(password) == 0 { + return "", fmt.Errorf("password cannot be empty") + } + + return password, nil +} + +// generateUniqueID creates a highly unique ID for benchmark operations +func generateUniqueID(prefix string) string { + // Generate random bytes for extra uniqueness + randomBytes := make([]byte, 8) + if _, err := rand.Read(randomBytes); err != nil { + // Fallback to timestamp-only if random generation fails + return fmt.Sprintf("%s-%d-%d", prefix, time.Now().UnixNano(), os.Getpid()) + } + randomHex := hex.EncodeToString(randomBytes) + + // Combine timestamp, process ID, and random bytes + return fmt.Sprintf("%s-%d-%d-%s", prefix, time.Now().UnixNano(), os.Getpid(), randomHex) +} + +// getNATSConnection creates a NATS connection with proper TLS configuration +// This is similar to GetNATSConnection in cmd/mpcium/main.go +func getNATSConnection(environment string, appConfig *config.AppConfig) (*nats.Conn, error) { + url := appConfig.NATs.URL + opts := []nats.Option{ + nats.MaxReconnects(-1), // retry forever + nats.ReconnectWait(2 * time.Second), + nats.DisconnectHandler(func(nc *nats.Conn) { + logger.Warn("Disconnected from NATS") + }), + nats.ReconnectHandler(func(nc *nats.Conn) { + logger.Info("Reconnected to NATS", "url", nc.ConnectedUrl()) + }), + nats.ClosedHandler(func(nc *nats.Conn) { + logger.Info("NATS connection closed!") + }), + } + + if environment == constant.EnvProduction { + // Load TLS config from configuration + var clientCert, clientKey, caCert string + if appConfig.NATs.TLS != nil { + clientCert = appConfig.NATs.TLS.ClientCert + clientKey = appConfig.NATs.TLS.ClientKey + caCert = appConfig.NATs.TLS.CACert + } + + // Fallback to default paths if not configured + if clientCert == "" { + clientCert = filepath.Join(".", "certs", "client-cert.pem") + } + if clientKey == "" { + clientKey = filepath.Join(".", "certs", "client-key.pem") + } + if caCert == "" { + caCert = filepath.Join(".", "certs", "rootCA.pem") + } + + opts = append(opts, + nats.ClientCert(clientCert, clientKey), + nats.RootCAs(caCert), + nats.UserInfo(appConfig.NATs.Username, appConfig.NATs.Password), + ) + } + + return nats.Connect(url, opts...) +} + func runKeygenBenchmark(ctx context.Context, cmd *cli.Command) error { if cmd.Args().Len() < 1 { return fmt.Errorf("missing required argument: num_operations") @@ -252,7 +377,8 @@ func runKeygenBenchmark(ctx context.Context, cmd *cli.Command) error { // Run operations startTime := time.Now() for i := 0; i < n; i++ { - walletID := fmt.Sprintf("benchmark-keygen-%d-%d", time.Now().UnixNano(), i) + // Generate unique wallet ID to avoid duplicates across runs + walletID := generateUniqueID(fmt.Sprintf("benchmark-keygen-%d", i)) result := OperationResult{ ID: walletID, @@ -298,7 +424,10 @@ func runKeygenBenchmark(ctx context.Context, cmd *cli.Command) error { // Calculate results benchResult := calculateBenchmarkResult(results, totalTime, 1, []time.Duration{totalTime}) - printBenchmarkResult("Keygen", benchResult) + outputFile := cmd.String("output") + if err := printBenchmarkResult("Keygen", benchResult, outputFile); err != nil { + return fmt.Errorf("failed to write benchmark results: %w", err) + } return nil } @@ -334,6 +463,7 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy totalBatches := (n + batchSize - 1) / batchSize fmt.Printf("Starting %s signing benchmark with %d operations (%d batches of %d) for wallet %s...\n", keyTypeName, n, totalBatches, batchSize, walletID) + fmt.Printf("Note: If you see 'Duplicate signing request detected' errors, wait a few minutes between benchmark runs\n") var results []OperationResult var wg sync.WaitGroup @@ -394,11 +524,15 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy fmt.Printf("Starting batch %d/%d (%d operations)...\n", batchNum+1, totalBatches, batchEndIdx-batchStartIdx) for i := batchStartIdx; i < batchEndIdx; i++ { - txID := fmt.Sprintf("benchmark-%s-sign-%d-%d", keyTypeName, time.Now().UnixNano(), i) + // Generate unique transaction ID to avoid duplicates across runs + txID := generateUniqueID(fmt.Sprintf("benchmark-%s-sign-%d", keyTypeName, i)) // Generate random transaction data txData := make([]byte, 32) - rand.Read(txData) + if _, err := rand.Read(txData); err != nil { + // Use zero bytes if random generation fails (still valid for benchmark) + txData = make([]byte, 32) + } msg := &types.SignTxMessage{ KeyType: keyType, @@ -455,7 +589,10 @@ func runSignBenchmark(ctx context.Context, cmd *cli.Command, keyType types.KeyTy // Calculate results benchResult := calculateBenchmarkResult(results, totalTime, batchSize, batchTimes) - printBenchmarkResult(fmt.Sprintf("%s Signing", keyTypeName), benchResult) + outputFile := cmd.String("output") + if err := printBenchmarkResult(fmt.Sprintf("%s Signing", keyTypeName), benchResult, outputFile); err != nil { + return fmt.Errorf("failed to write benchmark results: %w", err) + } return nil } @@ -524,7 +661,8 @@ func runReshareBenchmark(ctx context.Context, cmd *cli.Command) error { // Run operations startTime := time.Now() for i := 0; i < n; i++ { - sessionID := fmt.Sprintf("benchmark-reshare-%d-%d", time.Now().UnixNano(), i) + // Generate unique session ID to avoid duplicates across runs + sessionID := generateUniqueID(fmt.Sprintf("benchmark-reshare-%d", i)) msg := &types.ResharingMessage{ SessionID: sessionID, @@ -578,21 +716,14 @@ func runReshareBenchmark(ctx context.Context, cmd *cli.Command) error { // Calculate results benchResult := calculateBenchmarkResult(results, totalTime, 1, []time.Duration{totalTime}) - printBenchmarkResult("Reshare", benchResult) + outputFile := cmd.String("output") + if err := printBenchmarkResult("Reshare", benchResult, outputFile); err != nil { + return fmt.Errorf("failed to write benchmark results: %w", err) + } return nil } -type OperationResult struct { - ID string - StartTime time.Time - EndTime time.Time - Completed bool - Success bool - ErrorReason string - ErrorCode string -} - func parseNumOps(numOps string) (int, error) { var n int _, err := fmt.Sscanf(numOps, "%d", &n) @@ -669,22 +800,51 @@ func calculateBenchmarkResult(results []OperationResult, totalTime time.Duration } } -func printBenchmarkResult(operationType string, result BenchmarkResult) { - fmt.Println() - fmt.Println("===============================") - fmt.Printf("BENCHMARK RESULTS SUMMARY\n") - fmt.Println("===============================") - fmt.Printf("Total benchmark time: %v\n", result.TotalTime) - fmt.Printf("Total batches sent: %d\n", result.TotalBatches) - fmt.Printf("Total requests sent: %d\n", result.TotalOperations) - fmt.Printf("Successful completions: %d\n", result.SuccessfulOps) - fmt.Printf("Success rate: %.2f%%\n", 100.0-result.ErrorRate) - fmt.Printf("Average signs per minute: %.2f\n", result.OperationsPerMin) - - fmt.Println() - fmt.Println("------------------------------") - fmt.Printf("%d REQUEST ANALYSIS\n", result.BatchSize) - fmt.Println("------------------------------") +func printBenchmarkResult(operationType string, result BenchmarkResult, outputFile string) error { + // Generate the benchmark report content + reportContent := generateBenchmarkReport(operationType, result) + + // Print to console + fmt.Print(reportContent) + + // Write to file if specified + if outputFile != "" { + if err := writeBenchmarkToFile(reportContent, outputFile, operationType); err != nil { + return fmt.Errorf("failed to write to file %s: %w", outputFile, err) + } + fmt.Printf("\nBenchmark results written to: %s\n", outputFile) + } + + return nil +} + +func generateBenchmarkReport(operationType string, result BenchmarkResult) string { + var report strings.Builder + + report.WriteString("\n") + report.WriteString("===============================\n") + report.WriteString(fmt.Sprintf("%s BENCHMARK RESULTS SUMMARY\n", strings.ToUpper(operationType))) + report.WriteString("===============================\n") + report.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339))) + report.WriteString(fmt.Sprintf("Operation Type: %s\n", operationType)) + report.WriteString(fmt.Sprintf("Total benchmark time: %v\n", result.TotalTime)) + report.WriteString(fmt.Sprintf("Total batches sent: %d\n", result.TotalBatches)) + report.WriteString(fmt.Sprintf("Total requests sent: %d\n", result.TotalOperations)) + report.WriteString(fmt.Sprintf("Successful completions: %d\n", result.SuccessfulOps)) + report.WriteString(fmt.Sprintf("Failed operations: %d\n", result.FailedOps)) + report.WriteString(fmt.Sprintf("Success rate: %.2f%%\n", 100.0-result.ErrorRate)) + report.WriteString(fmt.Sprintf("Error rate: %.2f%%\n", result.ErrorRate)) + report.WriteString(fmt.Sprintf("Average operations per minute: %.2f\n", result.OperationsPerMin)) + + if len(result.OperationTimes) > 0 { + report.WriteString(fmt.Sprintf("Average operation time: %v\n", result.AverageTime)) + report.WriteString(fmt.Sprintf("Median operation time: %v\n", result.MedianTime)) + } + + report.WriteString("\n") + report.WriteString("------------------------------\n") + report.WriteString(fmt.Sprintf("%d REQUEST ANALYSIS\n", result.BatchSize)) + report.WriteString("------------------------------\n") if len(result.OperationTimes) >= result.BatchSize { firstNResults := result.OperationTimes[:result.BatchSize] @@ -697,7 +857,7 @@ func printBenchmarkResult(operationType string, result BenchmarkResult) { completedCount = result.BatchSize } - fmt.Printf("Completed from first %d: %d/%d\n", result.BatchSize, completedCount, result.BatchSize) + report.WriteString(fmt.Sprintf("Completed from first %d: %d/%d\n", result.BatchSize, completedCount, result.BatchSize)) if len(firstNResults) > 0 { var totalTime time.Duration @@ -714,8 +874,44 @@ func printBenchmarkResult(operationType string, result BenchmarkResult) { } } - fmt.Printf("Fastest (first %d): %v\n", result.BatchSize, minTime) - fmt.Printf("Slowest (first %d): %v\n", result.BatchSize, maxTime) + report.WriteString(fmt.Sprintf("Fastest (first %d): %v\n", result.BatchSize, minTime)) + report.WriteString(fmt.Sprintf("Slowest (first %d): %v\n", result.BatchSize, maxTime)) } } + + report.WriteString("\n") + + return report.String() +} + +func writeBenchmarkToFile(content, outputFile, operationType string) (err error) { + // Create directory if it doesn't exist + dir := filepath.Dir(outputFile) + if err := os.MkdirAll(dir, 0750); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + // Open file for appending (create if doesn't exist) + file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close file: %w", closeErr) + } + }() + + // Write content to file + if _, err := file.WriteString(content); err != nil { + return fmt.Errorf("failed to write content: %w", err) + } + + // Add separator for multiple benchmark runs + separator := fmt.Sprintf("\n%s\n\n", strings.Repeat("=", 80)) + if _, err := file.WriteString(separator); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + + return nil } diff --git a/cmd/mpcium-cli/main.go b/cmd/mpcium-cli/main.go index 9731a2bb..cc8cabe2 100644 --- a/cmd/mpcium-cli/main.go +++ b/cmd/mpcium-cli/main.go @@ -26,6 +26,7 @@ func main() { }, }, Commands: []*cli.Command{ + benchmarkCommand(), { Name: "generate-peers", Usage: "Generate a new peers.json file",