From c52e48f7513b5c609f166fe3a0eaa35fa0df8fb3 Mon Sep 17 00:00:00 2001 From: Dumi Loghin Date: Fri, 27 Feb 2026 14:46:37 +0800 Subject: [PATCH 1/3] feat(tools): save benchmarking stats to csv file --- tools/adventure/Makefile | 4 +- tools/adventure/bench/erc20.go | 3 +- tools/adventure/bench/native.go | 3 +- tools/adventure/main.go | 8 +- tools/adventure/utils/tps.go | 134 +++++++++++++++++++++++++++++++- tools/adventure/utils/tx.go | 11 ++- 6 files changed, 155 insertions(+), 8 deletions(-) diff --git a/tools/adventure/Makefile b/tools/adventure/Makefile index 06759407..7a9436c0 100644 --- a/tools/adventure/Makefile +++ b/tools/adventure/Makefile @@ -32,7 +32,7 @@ erc20: build @echo "🚀 Starting ERC20 test..." @adventure erc20-init $(INIT_AMOUNT) -f $(CONFIG_FILE) 2>&1 | tee /tmp/erc20-init.log @sleep 10 - @adventure erc20-bench -f $(CONFIG_FILE) --contract $$(grep "ERC20 Address:" /tmp/erc20-init.log | awk '{print $$NF}') + @adventure erc20-bench -f $(CONFIG_FILE) --contract $$(grep "ERC20 Address:" /tmp/erc20-init.log | awk '{print $$NF}') --csv-report @echo "✅ ERC20 test completed!" # Native Token stress test (init + bench) @@ -40,5 +40,5 @@ native: build @echo "🚀 Starting Native Token test..." @adventure native-init $(INIT_AMOUNT) -f $(CONFIG_FILE) @sleep 10 - @adventure native-bench -f $(CONFIG_FILE) + @adventure native-bench -f $(CONFIG_FILE) --csv-report @echo "✅ Native Token test completed!" diff --git a/tools/adventure/bench/erc20.go b/tools/adventure/bench/erc20.go index e0701757..25c56b36 100644 --- a/tools/adventure/bench/erc20.go +++ b/tools/adventure/bench/erc20.go @@ -169,7 +169,7 @@ func Erc20Init(amountStr, configPath string) error { // ======================================== // Erc20Bench runs ERC20 transfer benchmark -func Erc20Bench(configPath, contractAddr string) error { +func Erc20Bench(configPath, contractAddr string, csvReport bool) error { if configPath == "" { return errors.New("configPath must not be empty") } @@ -179,6 +179,7 @@ func Erc20Bench(configPath, contractAddr string) error { } gasPrice := utils.ParseGasPriceToBigInt(utils.TransferCfg.GasPriceGwei, 9) + utils.EnableBenchmarkCSVReport(csvReport) eParam := utils.NewTxParam( ethcmn.HexToAddress(contractAddr), diff --git a/tools/adventure/bench/native.go b/tools/adventure/bench/native.go index fc8183a6..528d9b58 100644 --- a/tools/adventure/bench/native.go +++ b/tools/adventure/bench/native.go @@ -105,7 +105,7 @@ func NativeInit(amountStr, configPath string) error { } // NativeBench runs native token transfer benchmark -func NativeBench(configPath string) error { +func NativeBench(configPath string, csvReport bool) error { amount := new(big.Int).SetUint64(1) if configPath == "" { @@ -117,6 +117,7 @@ func NativeBench(configPath string) error { } gasPrice := utils.ParseGasPriceToBigInt(utils.TransferCfg.GasPriceGwei, 9) + utils.EnableBenchmarkCSVReport(csvReport) // Generate random recipient addresses to simulate real-world transfer scenarios toAddrs := generateAddresses() diff --git a/tools/adventure/main.go b/tools/adventure/main.go index 2d7c69c9..5c2bbfc1 100644 --- a/tools/adventure/main.go +++ b/tools/adventure/main.go @@ -18,11 +18,13 @@ func init() { const ( FlagConfigFile = "config-file" FlagContract = "contract" + FlagCSVReport = "csv-report" ) var ( configPath string contractAddr string + csvReport bool ) func main() { @@ -92,7 +94,7 @@ Example: os.Exit(1) } - if err := bench.Erc20Bench(configPath, contractAddr); err != nil { + if err := bench.Erc20Bench(configPath, contractAddr, csvReport); err != nil { fmt.Printf("ERC20 benchmark failed: %v\n", err) os.Exit(1) } @@ -101,6 +103,7 @@ Example: cmd.Flags().StringVarP(&configPath, FlagConfigFile, "f", "", "Path to the benchmark configuration file") cmd.Flags().StringVar(&contractAddr, FlagContract, "", "ERC20 contract address") + cmd.Flags().BoolVar(&csvReport, FlagCSVReport, false, "Save benchmark stats to CSV report (benchmark_report_.csv)") return cmd } @@ -148,7 +151,7 @@ Example: os.Exit(1) } - if err := bench.NativeBench(configPath); err != nil { + if err := bench.NativeBench(configPath, csvReport); err != nil { fmt.Printf("Native token benchmark failed: %v\n", err) os.Exit(1) } @@ -156,6 +159,7 @@ Example: } cmd.Flags().StringVarP(&configPath, FlagConfigFile, "f", "", "Path to the benchmark configuration file") + cmd.Flags().BoolVar(&csvReport, FlagCSVReport, false, "Save benchmark stats to CSV report (benchmark_report_.csv)") return cmd } diff --git a/tools/adventure/utils/tps.go b/tools/adventure/utils/tps.go index ac1c1141..ee45fcc4 100644 --- a/tools/adventure/utils/tps.go +++ b/tools/adventure/utils/tps.go @@ -3,11 +3,16 @@ package utils import ( "bytes" "context" + "encoding/csv" "encoding/json" "fmt" "io" + "log" "math/big" "net/http" + "os" + "strconv" + "sync" "time" "github.com/ethereum/go-ethereum/core/types" @@ -31,6 +36,119 @@ type SimpleTPSManager struct { url string } +var tpsCSVReportEnabled bool + +// EnableBenchmarkCSVReport controls whether TPS summaries are also written to CSV. +func EnableBenchmarkCSVReport(enabled bool) { + tpsCSVReportEnabled = enabled +} + +type tpsCSVWriter struct { + mu sync.Mutex + closed bool + path string + file *os.File + writer *csv.Writer +} + +var currentBenchmarkCSVWriter *tpsCSVWriter + +func buildBenchmarkCSVReportPath() string { + ts := time.Now().Format("20060102_150405") + return fmt.Sprintf("./benchmark_report_%s.csv", ts) +} + +func initBenchmarkCSVWriter() (*tpsCSVWriter, error) { + if !tpsCSVReportEnabled { + return nil, nil + } + + reportPath := buildBenchmarkCSVReportPath() + file, err := os.OpenFile(reportPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open tps csv file: %w", err) + } + + writer := csv.NewWriter(file) + if err := writer.Write([]string{ + "timestamp", + "start_block_num", + "new_block_num", + "total_tx_count", + "average_btps", + "max_tps", + "min_tps", + "time_last_seconds", + }); err != nil { + file.Close() + return nil, fmt.Errorf("failed to write tps csv header: %w", err) + } + writer.Flush() + if err := writer.Error(); err != nil { + file.Close() + return nil, fmt.Errorf("failed to flush tps csv header: %w", err) + } + + log.Printf("📝 TPS CSV report enabled: %s\n", reportPath) + csvWriter := &tpsCSVWriter{path: reportPath, file: file, writer: writer} + currentBenchmarkCSVWriter = csvWriter + return csvWriter, nil +} + +func (w *tpsCSVWriter) WriteRecord(startBlockNum, newBlockNum, totalTxCount uint64, avgTPS, maxTPS, minTPS float64, elapsedSeconds int64) error { + if w == nil { + return nil + } + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return nil + } + + record := []string{ + time.Now().Format(time.RFC3339), + strconv.FormatUint(startBlockNum, 10), + strconv.FormatUint(newBlockNum, 10), + strconv.FormatUint(totalTxCount, 10), + fmt.Sprintf("%.2f", avgTPS), + fmt.Sprintf("%.2f", maxTPS), + fmt.Sprintf("%.2f", minTPS), + strconv.FormatInt(elapsedSeconds, 10), + } + if err := w.writer.Write(record); err != nil { + return err + } + w.writer.Flush() + return w.writer.Error() +} + +func (w *tpsCSVWriter) Close() { + if w == nil { + return + } + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return + } + w.closed = true + w.writer.Flush() + if err := w.writer.Error(); err != nil { + log.Printf("⚠️ Failed to flush TPS CSV report: %v\n", err) + } + if err := w.file.Close(); err != nil { + log.Printf("⚠️ Failed to close TPS CSV report: %v\n", err) + } +} + +// CloseBenchmarkCSVReport flushes and closes active benchmark CSV report if enabled. +func CloseBenchmarkCSVReport() { + if currentBenchmarkCSVWriter != nil { + currentBenchmarkCSVWriter.Close() + currentBenchmarkCSVWriter = nil + } +} + func NewTPSMan(clientURL string) *SimpleTPSManager { //Dial EthClient client, err := ethclient.Dial(clientURL) @@ -79,6 +197,16 @@ func (tpsman *SimpleTPSManager) BlockHeder(height uint64) *types.Header { func (tpsman *SimpleTPSManager) TPSDisplay() { time.Sleep(time.Second * 10) fmt.Println("TPSDisplay") + csvWriter, err := initBenchmarkCSVWriter() + if err != nil { + log.Printf("⚠️ Failed to initialize TPS CSV report: %v\n", err) + } + defer func() { + if csvWriter != nil { + CloseBenchmarkCSVReport() + } + }() + var initHeight uint64 var totalTxCount uint64 var initTime time.Time @@ -127,10 +255,14 @@ func (tpsman *SimpleTPSManager) TPSDisplay() { minTps = avgTPS } } + elapsedSeconds := int64(time.Since(initTime).Seconds()) fmt.Println("========================================================") fmt.Printf("[TPS log] StartBlock Num: %d, NewBlockNum: %d, totalTxCount:%d\n", initHeight+1, lastHeight, totalTxCount) - fmt.Printf("[Summary] Average BTPS: %5.2f, Max TPS: %5.2f, Min TPS: %5.2f, Time Last: %ds\n", avgTPS, maxTps, minTps, int64(time.Since(initTime).Seconds())) + fmt.Printf("[Summary] Average BTPS: %5.2f, Max TPS: %5.2f, Min TPS: %5.2f, Time Last: %ds\n", avgTPS, maxTps, minTps, elapsedSeconds) fmt.Println("========================================================") + if err := csvWriter.WriteRecord(initHeight+1, lastHeight, totalTxCount, avgTPS, maxTps, minTps, elapsedSeconds); err != nil { + log.Printf("⚠️ Failed to write TPS CSV record: %v\n", err) + } time.Sleep(5 * time.Second) } diff --git a/tools/adventure/utils/tx.go b/tools/adventure/utils/tx.go index 3e91951e..a262f4f8 100644 --- a/tools/adventure/utils/tx.go +++ b/tools/adventure/utils/tx.go @@ -11,9 +11,11 @@ import ( "math/big" "net/http" "os" + "os/signal" "strconv" "strings" "sync" + "syscall" "time" ethcmn "github.com/ethereum/go-ethereum/common" @@ -208,7 +210,14 @@ func RunTxs(e func(ethcmn.Address) []TxParam) { } go tpsman.TPSDisplay() - select {} + + stopCh := make(chan os.Signal, 1) + signal.Notify(stopCh, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(stopCh) + + <-stopCh + log.Printf("🛑 Interrupt received, flushing benchmark outputs...\n") + CloseBenchmarkCSVReport() } var defaultGasPrice = big.NewInt(100000000000) From fb5a24393387e66a24924fa6203aeeab8a2d6ea0 Mon Sep 17 00:00:00 2001 From: Dumi Loghin Date: Fri, 6 Mar 2026 14:43:16 +0800 Subject: [PATCH 2/3] feat(tps): enhance CSV logging with system metrics --- tools/adventure/utils/sysmetrics.go | 265 ++++++++++++++++++++++++++++ tools/adventure/utils/tps.go | 22 ++- 2 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 tools/adventure/utils/sysmetrics.go diff --git a/tools/adventure/utils/sysmetrics.go b/tools/adventure/utils/sysmetrics.go new file mode 100644 index 00000000..4b19ccc7 --- /dev/null +++ b/tools/adventure/utils/sysmetrics.go @@ -0,0 +1,265 @@ +package utils + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +// SysMetrics holds a point-in-time snapshot of system-wide resource utilization. +type SysMetrics struct { + // CPU utilization in percent (0–100), system-wide across all cores. + CPUPercent float64 + // Memory utilization in percent (0–100). + MemPercent float64 + // Total physical memory in bytes. + MemTotalBytes uint64 + // Used physical memory in bytes (MemTotal - MemAvailable). + MemUsedBytes uint64 + // Aggregate disk read throughput since last sample, bytes/s. + DiskReadBytesPerSec float64 + // Aggregate disk write throughput since last sample, bytes/s. + DiskWriteBytesPerSec float64 +} + +// cpuStat holds raw values read from /proc/stat for a single sample. +type cpuStat struct { + user uint64 + nice uint64 + system uint64 + idle uint64 + iowait uint64 + irq uint64 + softirq uint64 + steal uint64 +} + +func (s cpuStat) total() uint64 { + return s.user + s.nice + s.system + s.idle + s.iowait + s.irq + s.softirq + s.steal +} + +func (s cpuStat) busy() uint64 { + return s.total() - s.idle - s.iowait +} + +// diskStat holds the read/write sector counts for a single block device from /proc/diskstats. +type diskStat struct { + readSectors uint64 + writeSectors uint64 +} + +// SysMetricsCollector samples system metrics across consecutive calls, computing +// deltas between samples to derive utilization rates. +type SysMetricsCollector struct { + prevCPU cpuStat + prevDisk map[string]diskStat + prevDiskTime time.Time +} + +// NewSysMetricsCollector creates a collector and records an initial baseline sample +// so the first call to Sample returns meaningful deltas. +func NewSysMetricsCollector() *SysMetricsCollector { + c := &SysMetricsCollector{ + prevDisk: make(map[string]diskStat), + } + // Prime the baseline; errors here are non-fatal. + c.prevCPU, _ = readCPUStat() + c.prevDisk, c.prevDiskTime = readDiskStats() + return c +} + +// Sample reads current system stats, computes deltas against the previous sample, +// and returns a populated SysMetrics. It is safe to call from a single goroutine. +func (c *SysMetricsCollector) Sample() SysMetrics { + var m SysMetrics + + // --- CPU --- + cur, err := readCPUStat() + if err == nil { + deltaBusy := float64(cur.busy() - c.prevCPU.busy()) + deltaTotal := float64(cur.total() - c.prevCPU.total()) + if deltaTotal > 0 { + m.CPUPercent = 100.0 * deltaBusy / deltaTotal + } + c.prevCPU = cur + } + + // --- Memory --- + m.MemTotalBytes, m.MemUsedBytes, m.MemPercent, _ = readMemInfo() + + // --- Disk I/O --- + curDisk, curDiskTime := readDiskStats() + elapsedSec := curDiskTime.Sub(c.prevDiskTime).Seconds() + if elapsedSec > 0 { + var totalReadSectors, totalWriteSectors uint64 + for dev, cs := range curDisk { + ps := c.prevDisk[dev] + totalReadSectors += cs.readSectors - ps.readSectors + totalWriteSectors += cs.writeSectors - ps.writeSectors + } + // Linux sector size is 512 bytes. + m.DiskReadBytesPerSec = float64(totalReadSectors) * 512.0 / elapsedSec + m.DiskWriteBytesPerSec = float64(totalWriteSectors) * 512.0 / elapsedSec + } + c.prevDisk = curDisk + c.prevDiskTime = curDiskTime + + return m +} + +// FormatConsole returns a compact human-readable string for console output. +func (m SysMetrics) FormatConsole() string { + return fmt.Sprintf( + "CPU: %5.1f%% | Mem: %5.1f%% (%s / %s) | Disk R/W: %s/s / %s/s", + m.CPUPercent, + m.MemPercent, + formatBytes(m.MemUsedBytes), + formatBytes(m.MemTotalBytes), + formatBytes(uint64(m.DiskReadBytesPerSec)), + formatBytes(uint64(m.DiskWriteBytesPerSec)), + ) +} + +// formatBytes converts a byte count to a human-readable IEC string (KiB, MiB, GiB). +func formatBytes(b uint64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%dB", b) + } + div, exp := uint64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f%ciB", float64(b)/float64(div), "KMGTPE"[exp]) +} + +// readCPUStat parses the first "cpu" aggregate line from /proc/stat. +func readCPUStat() (cpuStat, error) { + f, err := os.Open("/proc/stat") + if err != nil { + return cpuStat{}, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "cpu ") { + continue + } + fields := strings.Fields(line) + // fields[0] = "cpu", then user nice system idle iowait irq softirq steal ... + if len(fields) < 9 { + break + } + nums := make([]uint64, 8) + for i := 0; i < 8; i++ { + nums[i], _ = strconv.ParseUint(fields[i+1], 10, 64) + } + return cpuStat{ + user: nums[0], + nice: nums[1], + system: nums[2], + idle: nums[3], + iowait: nums[4], + irq: nums[5], + softirq: nums[6], + steal: nums[7], + }, nil + } + return cpuStat{}, fmt.Errorf("cpu line not found in /proc/stat") +} + +// readMemInfo parses /proc/meminfo and returns (total, used, usedPercent, error). +func readMemInfo() (total, used uint64, pct float64, err error) { + f, err := os.Open("/proc/meminfo") + if err != nil { + return + } + defer f.Close() + + var memTotal, memAvailable uint64 + found := 0 + scanner := bufio.NewScanner(f) + for scanner.Scan() && found < 2 { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + val, _ := strconv.ParseUint(fields[1], 10, 64) + switch fields[0] { + case "MemTotal:": + memTotal = val * 1024 // kB → bytes + found++ + case "MemAvailable:": + memAvailable = val * 1024 + found++ + } + } + total = memTotal + if memTotal > memAvailable { + used = memTotal - memAvailable + } + if memTotal > 0 { + pct = 100.0 * float64(used) / float64(memTotal) + } + return +} + +// readDiskStats parses /proc/diskstats and returns per-device sector counts along +// with the current time for elapsed-time calculation. +func readDiskStats() (map[string]diskStat, time.Time) { + now := time.Now() + result := make(map[string]diskStat) + + f, err := os.Open("/proc/diskstats") + if err != nil { + return result, now + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + // /proc/diskstats fields (0-indexed): + // 0: major, 1: minor, 2: name, + // 3: reads_completed, 4: reads_merged, 5: sectors_read, 6: time_reading_ms + // 7: writes_completed, 8: writes_merged, 9: sectors_written, 10: time_writing_ms + if len(fields) < 10 { + continue + } + name := fields[2] + // Skip partition entries (e.g. sda1, nvme0n1p1); keep whole-disk devices. + if isPartition(name) { + continue + } + var ds diskStat + ds.readSectors, _ = strconv.ParseUint(fields[5], 10, 64) + ds.writeSectors, _ = strconv.ParseUint(fields[9], 10, 64) + result[name] = ds + } + return result, now +} + +// isPartition returns true if the device name looks like a partition rather than +// a whole disk (e.g. sda1, nvme0n1p2) so we avoid double-counting. +func isPartition(name string) bool { + if len(name) == 0 { + return false + } + last := name[len(name)-1] + if last < '0' || last > '9' { + return false + } + // nvme devices use the form nvme0n1 (whole disk) vs nvme0n1p1 (partition). + if strings.Contains(name, "nvme") { + return strings.Contains(name, "p") + } + // For sd*, vd*, hd*, xvd* etc. a trailing digit means partition. + return true +} diff --git a/tools/adventure/utils/tps.go b/tools/adventure/utils/tps.go index ee45fcc4..0c529ba0 100644 --- a/tools/adventure/utils/tps.go +++ b/tools/adventure/utils/tps.go @@ -79,6 +79,12 @@ func initBenchmarkCSVWriter() (*tpsCSVWriter, error) { "max_tps", "min_tps", "time_last_seconds", + "cpu_percent", + "mem_percent", + "mem_used_bytes", + "mem_total_bytes", + "disk_read_bytes_per_sec", + "disk_write_bytes_per_sec", }); err != nil { file.Close() return nil, fmt.Errorf("failed to write tps csv header: %w", err) @@ -95,7 +101,7 @@ func initBenchmarkCSVWriter() (*tpsCSVWriter, error) { return csvWriter, nil } -func (w *tpsCSVWriter) WriteRecord(startBlockNum, newBlockNum, totalTxCount uint64, avgTPS, maxTPS, minTPS float64, elapsedSeconds int64) error { +func (w *tpsCSVWriter) WriteRecord(startBlockNum, newBlockNum, totalTxCount uint64, avgTPS, maxTPS, minTPS float64, elapsedSeconds int64, sys SysMetrics) error { if w == nil { return nil } @@ -114,6 +120,12 @@ func (w *tpsCSVWriter) WriteRecord(startBlockNum, newBlockNum, totalTxCount uint fmt.Sprintf("%.2f", maxTPS), fmt.Sprintf("%.2f", minTPS), strconv.FormatInt(elapsedSeconds, 10), + fmt.Sprintf("%.2f", sys.CPUPercent), + fmt.Sprintf("%.2f", sys.MemPercent), + strconv.FormatUint(sys.MemUsedBytes, 10), + strconv.FormatUint(sys.MemTotalBytes, 10), + fmt.Sprintf("%.0f", sys.DiskReadBytesPerSec), + fmt.Sprintf("%.0f", sys.DiskWriteBytesPerSec), } if err := w.writer.Write(record); err != nil { return err @@ -207,6 +219,8 @@ func (tpsman *SimpleTPSManager) TPSDisplay() { } }() + sysCollector := NewSysMetricsCollector() + var initHeight uint64 var totalTxCount uint64 var initTime time.Time @@ -256,11 +270,15 @@ func (tpsman *SimpleTPSManager) TPSDisplay() { } } elapsedSeconds := int64(time.Since(initTime).Seconds()) + + sys := sysCollector.Sample() + fmt.Println("========================================================") fmt.Printf("[TPS log] StartBlock Num: %d, NewBlockNum: %d, totalTxCount:%d\n", initHeight+1, lastHeight, totalTxCount) fmt.Printf("[Summary] Average BTPS: %5.2f, Max TPS: %5.2f, Min TPS: %5.2f, Time Last: %ds\n", avgTPS, maxTps, minTps, elapsedSeconds) + fmt.Printf("[SysMon] %s\n", sys.FormatConsole()) fmt.Println("========================================================") - if err := csvWriter.WriteRecord(initHeight+1, lastHeight, totalTxCount, avgTPS, maxTps, minTps, elapsedSeconds); err != nil { + if err := csvWriter.WriteRecord(initHeight+1, lastHeight, totalTxCount, avgTPS, maxTps, minTps, elapsedSeconds, sys); err != nil { log.Printf("⚠️ Failed to write TPS CSV record: %v\n", err) } From 27e650d7ff7852183c56c00e9d9b5d8dd9b7cf60 Mon Sep 17 00:00:00 2001 From: Dumi Loghin Date: Fri, 6 Mar 2026 15:28:58 +0800 Subject: [PATCH 3/3] metrics filename as parameter --- tools/adventure/Makefile | 4 ++-- tools/adventure/bench/erc20.go | 2 +- tools/adventure/bench/native.go | 2 +- tools/adventure/main.go | 27 ++++++++++++++++++++++----- tools/adventure/utils/tps.go | 17 +++++++++++------ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/tools/adventure/Makefile b/tools/adventure/Makefile index 7a9436c0..1c4c91d9 100644 --- a/tools/adventure/Makefile +++ b/tools/adventure/Makefile @@ -32,7 +32,7 @@ erc20: build @echo "🚀 Starting ERC20 test..." @adventure erc20-init $(INIT_AMOUNT) -f $(CONFIG_FILE) 2>&1 | tee /tmp/erc20-init.log @sleep 10 - @adventure erc20-bench -f $(CONFIG_FILE) --contract $$(grep "ERC20 Address:" /tmp/erc20-init.log | awk '{print $$NF}') --csv-report + @adventure erc20-bench -f $(CONFIG_FILE) --contract $$(grep "ERC20 Address:" /tmp/erc20-init.log | awk '{print $$NF}') --csv-report "" @echo "✅ ERC20 test completed!" # Native Token stress test (init + bench) @@ -40,5 +40,5 @@ native: build @echo "🚀 Starting Native Token test..." @adventure native-init $(INIT_AMOUNT) -f $(CONFIG_FILE) @sleep 10 - @adventure native-bench -f $(CONFIG_FILE) --csv-report + @adventure native-bench -f $(CONFIG_FILE) --csv-report "" @echo "✅ Native Token test completed!" diff --git a/tools/adventure/bench/erc20.go b/tools/adventure/bench/erc20.go index 25c56b36..4402561f 100644 --- a/tools/adventure/bench/erc20.go +++ b/tools/adventure/bench/erc20.go @@ -169,7 +169,7 @@ func Erc20Init(amountStr, configPath string) error { // ======================================== // Erc20Bench runs ERC20 transfer benchmark -func Erc20Bench(configPath, contractAddr string, csvReport bool) error { +func Erc20Bench(configPath, contractAddr string, csvReport string) error { if configPath == "" { return errors.New("configPath must not be empty") } diff --git a/tools/adventure/bench/native.go b/tools/adventure/bench/native.go index 528d9b58..e86d2278 100644 --- a/tools/adventure/bench/native.go +++ b/tools/adventure/bench/native.go @@ -105,7 +105,7 @@ func NativeInit(amountStr, configPath string) error { } // NativeBench runs native token transfer benchmark -func NativeBench(configPath string, csvReport bool) error { +func NativeBench(configPath string, csvReport string) error { amount := new(big.Int).SetUint64(1) if configPath == "" { diff --git a/tools/adventure/main.go b/tools/adventure/main.go index 5c2bbfc1..00303790 100644 --- a/tools/adventure/main.go +++ b/tools/adventure/main.go @@ -24,9 +24,24 @@ const ( var ( configPath string contractAddr string - csvReport bool ) +// resolveCSVReportFlag converts the --csv-report flag value into the string passed +// to EnableBenchmarkCSVReport: +// - flag not set → "" (disabled) +// - flag set to "" → "-" (use default timestamped filename) +// - flag set to "foo.csv" → "foo.csv" +func resolveCSVReportFlag(cmd *cobra.Command, flagName string) string { + if !cmd.Flags().Changed(flagName) { + return "" + } + val, _ := cmd.Flags().GetString(flagName) + if val == "" { + return "-" + } + return val +} + func main() { rootCmd := &cobra.Command{ Use: "adventure", @@ -94,7 +109,8 @@ Example: os.Exit(1) } - if err := bench.Erc20Bench(configPath, contractAddr, csvReport); err != nil { + reportFile := resolveCSVReportFlag(cmd, FlagCSVReport) + if err := bench.Erc20Bench(configPath, contractAddr, reportFile); err != nil { fmt.Printf("ERC20 benchmark failed: %v\n", err) os.Exit(1) } @@ -103,7 +119,7 @@ Example: cmd.Flags().StringVarP(&configPath, FlagConfigFile, "f", "", "Path to the benchmark configuration file") cmd.Flags().StringVar(&contractAddr, FlagContract, "", "ERC20 contract address") - cmd.Flags().BoolVar(&csvReport, FlagCSVReport, false, "Save benchmark stats to CSV report (benchmark_report_.csv)") + cmd.Flags().String(FlagCSVReport, "", "Save benchmark stats to a CSV file; provide a filename or leave empty for a timestamped default (benchmark_report_.csv)") return cmd } @@ -151,7 +167,8 @@ Example: os.Exit(1) } - if err := bench.NativeBench(configPath, csvReport); err != nil { + reportFile := resolveCSVReportFlag(cmd, FlagCSVReport) + if err := bench.NativeBench(configPath, reportFile); err != nil { fmt.Printf("Native token benchmark failed: %v\n", err) os.Exit(1) } @@ -159,7 +176,7 @@ Example: } cmd.Flags().StringVarP(&configPath, FlagConfigFile, "f", "", "Path to the benchmark configuration file") - cmd.Flags().BoolVar(&csvReport, FlagCSVReport, false, "Save benchmark stats to CSV report (benchmark_report_.csv)") + cmd.Flags().String(FlagCSVReport, "", "Save benchmark stats to a CSV file; provide a filename or leave empty for a timestamped default (benchmark_report_.csv)") return cmd } diff --git a/tools/adventure/utils/tps.go b/tools/adventure/utils/tps.go index 0c529ba0..0f0f64be 100644 --- a/tools/adventure/utils/tps.go +++ b/tools/adventure/utils/tps.go @@ -36,11 +36,13 @@ type SimpleTPSManager struct { url string } -var tpsCSVReportEnabled bool +var tpsCSVReportFile string -// EnableBenchmarkCSVReport controls whether TPS summaries are also written to CSV. -func EnableBenchmarkCSVReport(enabled bool) { - tpsCSVReportEnabled = enabled +// EnableBenchmarkCSVReport sets the CSV report filename. An empty string disables +// reporting. A non-empty string enables it; if the value equals "-", the default +// timestamped filename is used instead. +func EnableBenchmarkCSVReport(filename string) { + tpsCSVReportFile = filename } type tpsCSVWriter struct { @@ -59,11 +61,14 @@ func buildBenchmarkCSVReportPath() string { } func initBenchmarkCSVWriter() (*tpsCSVWriter, error) { - if !tpsCSVReportEnabled { + if tpsCSVReportFile == "" { return nil, nil } - reportPath := buildBenchmarkCSVReportPath() + reportPath := tpsCSVReportFile + if reportPath == "-" { + reportPath = buildBenchmarkCSVReportPath() + } file, err := os.OpenFile(reportPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return nil, fmt.Errorf("failed to open tps csv file: %w", err)