summaryrefslogtreecommitdiff
path: root/internal/tools/benchmark
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-26 22:19:22 +0300
committerPaul Buetow <paul@buetow.org>2025-06-26 22:19:22 +0300
commit947e08e4f9e3c9c44b346adff4eb6d68fa79a726 (patch)
treecfa94aba72f91d26657de09b7a5b6a23eff10fd7 /internal/tools/benchmark
parent1e643ac66765fc0ab4224335191731d8b77fece2 (diff)
Refactor profiling and benchmarking tools from bash to Go
This major refactoring replaces all bash-based profiling and benchmarking scripts with a unified Go tool (dtail-tools) that provides: - Better cross-platform compatibility - Improved error handling and reliability - Structured data generation for test files - Consistent command-line interface - Easier maintenance and extensibility Key changes: - Created dtail-tools command with profile and benchmark subcommands - Implemented common utilities for data generation and file operations - Updated Makefile to use the new Go-based tools - Maintained backward compatibility with existing make targets - Fixed ParseSize to handle single-letter suffixes (10M, 1G, etc.) The new tool supports all previous functionality: - profile-quick, profile-all, profile-dmap - benchmark creation, comparison, and management - Test data generation with multiple formats - Profile analysis and listing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/tools/benchmark')
-rw-r--r--internal/tools/benchmark/benchmark.go385
1 files changed, 385 insertions, 0 deletions
diff --git a/internal/tools/benchmark/benchmark.go b/internal/tools/benchmark/benchmark.go
new file mode 100644
index 0000000..b728329
--- /dev/null
+++ b/internal/tools/benchmark/benchmark.go
@@ -0,0 +1,385 @@
+package benchmark
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/mimecast/dtail/internal/tools/common"
+)
+
+// Config holds benchmark configuration
+type Config struct {
+ Mode string
+ BaselineDir string
+ Tag string
+ Quick bool
+ Memory bool
+ OutputFile string
+ Verbose bool
+ Iterations string
+ BaselinePath string
+}
+
+// Run executes the benchmark command
+func Run() error {
+ cfg := parseFlags()
+
+ // Create baseline directory if needed
+ if err := common.EnsureDirectory(cfg.BaselineDir); err != nil {
+ return fmt.Errorf("failed to create baseline directory: %w", err)
+ }
+
+ switch cfg.Mode {
+ case "run":
+ return runBenchmarks(cfg)
+ case "baseline":
+ return createBaseline(cfg)
+ case "compare":
+ return compareWithBaseline(cfg)
+ case "list":
+ return listBaselines(cfg)
+ case "clean":
+ return cleanBaselines(cfg)
+ default:
+ return fmt.Errorf("unknown benchmark mode: %s", cfg.Mode)
+ }
+}
+
+func parseFlags() *Config {
+ cfg := &Config{
+ BaselineDir: "benchmarks/baselines",
+ Iterations: "1x",
+ }
+
+ flag.StringVar(&cfg.Mode, "mode", "run", "Benchmark mode: run, baseline, compare, list, clean")
+ flag.StringVar(&cfg.BaselineDir, "dir", cfg.BaselineDir, "Baseline directory")
+ flag.StringVar(&cfg.Tag, "tag", "", "Tag for baseline (e.g., 'before-optimization')")
+ flag.BoolVar(&cfg.Quick, "quick", false, "Run only quick benchmarks")
+ flag.BoolVar(&cfg.Memory, "memory", false, "Include memory profiling")
+ flag.StringVar(&cfg.OutputFile, "output", "", "Output file for results")
+ flag.BoolVar(&cfg.Verbose, "verbose", false, "Verbose output")
+ flag.StringVar(&cfg.Iterations, "iterations", cfg.Iterations, "Benchmark iterations (e.g., 3x)")
+ flag.StringVar(&cfg.BaselinePath, "baseline", "", "Baseline file for comparison")
+
+ flag.Parse()
+
+ // Handle positional arguments for compare mode
+ if cfg.Mode == "compare" && cfg.BaselinePath == "" {
+ args := flag.Args()
+ if len(args) > 0 {
+ cfg.BaselinePath = args[0]
+ }
+ }
+
+ return cfg
+}
+
+func runBenchmarks(cfg *Config) error {
+ common.PrintSection("Running DTail Benchmarks")
+
+ // Build binaries
+ common.PrintInfo("Building binaries...\n")
+ if err := common.BuildCommands("dcat", "dgrep", "dmap", "dtail", "dserver"); err != nil {
+ return fmt.Errorf("failed to build binaries: %w", err)
+ }
+
+ // Prepare benchmark command
+ args := []string{"test", "-bench=."}
+ if cfg.Quick {
+ args = append(args, "-bench=BenchmarkQuick")
+ }
+ if cfg.Memory {
+ args = append(args, "-benchmem")
+ }
+ if cfg.Iterations != "1x" {
+ args = append(args, fmt.Sprintf("-benchtime=%s", cfg.Iterations))
+ }
+ if cfg.Verbose {
+ args = append(args, "-v")
+ }
+ args = append(args, "./benchmarks")
+
+ // Run benchmarks
+ cmd := exec.Command("go", args...)
+
+ var output []byte
+ var err error
+
+ if cfg.OutputFile != "" {
+ // Capture output for file
+ output, err = cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("benchmark failed: %w\n%s", err, string(output))
+ }
+
+ // Write to file
+ if err := os.WriteFile(cfg.OutputFile, output, 0644); err != nil {
+ return fmt.Errorf("failed to write output file: %w", err)
+ }
+
+ // Also print to stdout
+ fmt.Print(string(output))
+ common.PrintSuccess("\nResults saved to: %s\n", cfg.OutputFile)
+ } else {
+ // Direct output to stdout
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("benchmark failed: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func createBaseline(cfg *Config) error {
+ if cfg.Tag == "" {
+ return fmt.Errorf("baseline tag is required (use -tag)")
+ }
+
+ common.PrintSection("Creating Benchmark Baseline")
+
+ // Generate filename
+ timestamp := time.Now().Format("20060102_150405")
+ safeTag := strings.ReplaceAll(cfg.Tag, " ", "_")
+ safeTag = strings.Map(func(r rune) rune {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
+ (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
+ return r
+ }
+ return '_'
+ }, safeTag)
+
+ filename := filepath.Join(cfg.BaselineDir,
+ fmt.Sprintf("baseline_%s_%s.txt", timestamp, safeTag))
+
+ // Create baseline file with metadata
+ file, err := os.Create(filename)
+ if err != nil {
+ return fmt.Errorf("failed to create baseline file: %w", err)
+ }
+ defer file.Close()
+
+ // Write metadata
+ fmt.Fprintf(file, "Git commit: %s\n", common.GetGitCommit())
+ fmt.Fprintf(file, "Date: %s\n", time.Now().Format(time.RFC3339))
+ fmt.Fprintf(file, "Tag: %s\n", cfg.Tag)
+ fmt.Fprintf(file, "----------------------------------------\n")
+
+ // Run benchmarks and capture output
+ args := []string{"test", "-bench=.", "-benchmem"}
+ if cfg.Quick {
+ args = append(args, "-bench=BenchmarkQuick")
+ }
+ if cfg.Iterations != "1x" && cfg.Iterations != "" {
+ args = append(args, fmt.Sprintf("-benchtime=%s", cfg.Iterations))
+ }
+ args = append(args, "./benchmarks")
+
+ cmd := exec.Command("go", args...)
+ cmd.Stdout = io.MultiWriter(file, os.Stdout)
+ cmd.Stderr = os.Stderr
+
+ common.PrintInfo("Running benchmarks for baseline...\n")
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("benchmark failed: %w", err)
+ }
+
+ common.PrintSuccess("\nBaseline saved to: %s\n", filename)
+ return nil
+}
+
+func compareWithBaseline(cfg *Config) error {
+ if cfg.BaselinePath == "" {
+ return fmt.Errorf("baseline file required (use -baseline or specify as argument)")
+ }
+
+ if !common.FileExists(cfg.BaselinePath) {
+ return fmt.Errorf("baseline file not found: %s", cfg.BaselinePath)
+ }
+
+ common.PrintSection("Comparing with Baseline")
+ fmt.Printf("Baseline: %s\n\n", cfg.BaselinePath)
+
+ // Run current benchmarks
+ currentFile := filepath.Join(cfg.BaselineDir, "current.txt")
+ args := []string{"test", "-bench=.", "-benchmem"}
+
+ // Check if baseline is quick mode
+ baselineContent, err := os.ReadFile(cfg.BaselinePath)
+ if err != nil {
+ return fmt.Errorf("failed to read baseline: %w", err)
+ }
+ if strings.Contains(string(baselineContent), "BenchmarkQuick") {
+ args = append(args, "-bench=BenchmarkQuick")
+ }
+
+ args = append(args, "./benchmarks")
+
+ cmd := exec.Command("go", args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("benchmark failed: %w\n%s", err, string(output))
+ }
+
+ // Save current results
+ if err := os.WriteFile(currentFile, output, 0644); err != nil {
+ return fmt.Errorf("failed to write current results: %w", err)
+ }
+
+ // Print current results
+ fmt.Println("Current benchmark results:")
+ fmt.Println(string(output))
+
+ common.PrintSection("Comparison Report")
+
+ // Try benchstat first
+ if err := runBenchstat(cfg.BaselinePath, currentFile); err != nil {
+ // Fall back to simple diff
+ common.PrintInfo("benchstat not found, showing simple diff:\n\n")
+ if err := showSimpleDiff(cfg.BaselinePath, currentFile); err != nil {
+ return fmt.Errorf("failed to show diff: %w", err)
+ }
+ }
+
+ // Save comparison report
+ reportFile := filepath.Join(cfg.BaselineDir,
+ fmt.Sprintf("comparison_%s.txt", time.Now().Format("20060102_150405")))
+
+ report := fmt.Sprintf("Comparison Report\n"+
+ "Generated: %s\n"+
+ "Baseline: %s\n"+
+ "Current: %s\n"+
+ "================================================================================\n\n",
+ time.Now().Format(time.RFC3339),
+ cfg.BaselinePath,
+ currentFile)
+
+ if err := os.WriteFile(reportFile, []byte(report), 0644); err != nil {
+ common.PrintError("Failed to save comparison report: %v\n", err)
+ } else {
+ common.PrintInfo("\nComparison report saved to: %s\n", reportFile)
+ }
+
+ return nil
+}
+
+func listBaselines(cfg *Config) error {
+ common.PrintSection("Available Baselines")
+
+ pattern := filepath.Join(cfg.BaselineDir, "baseline_*.txt")
+ files, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("failed to list baselines: %w", err)
+ }
+
+ if len(files) == 0 {
+ fmt.Printf("No baselines found in %s\n", cfg.BaselineDir)
+ return nil
+ }
+
+ // Sort by modification time (newest first)
+ sort.Slice(files, func(i, j int) bool {
+ fi, _ := os.Stat(files[i])
+ fj, _ := os.Stat(files[j])
+ return fi.ModTime().After(fj.ModTime())
+ })
+
+ // Display baselines
+ for _, file := range files {
+ info, err := os.Stat(file)
+ if err != nil {
+ continue
+ }
+
+ // Try to extract tag from file
+ tag := extractTagFromBaseline(file)
+
+ fmt.Printf(" %s %8s %-40s %s\n",
+ info.ModTime().Format("2006-01-02 15:04:05"),
+ common.FormatSize(info.Size()),
+ filepath.Base(file),
+ tag)
+ }
+
+ fmt.Printf("\nTotal: %d baselines\n", len(files))
+ fmt.Printf("\nUsage: dtail-tools benchmark -mode compare <baseline_file>\n")
+
+ return nil
+}
+
+func cleanBaselines(cfg *Config) error {
+ common.PrintSection("Cleaning Old Baselines")
+
+ pattern := filepath.Join(cfg.BaselineDir, "baseline_*.txt")
+ files, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("failed to list baselines: %w", err)
+ }
+
+ if len(files) <= 10 {
+ fmt.Println("No old baselines to clean (keeping last 10)")
+ return nil
+ }
+
+ // Sort by modification time (oldest first)
+ sort.Slice(files, func(i, j int) bool {
+ fi, _ := os.Stat(files[i])
+ fj, _ := os.Stat(files[j])
+ return fi.ModTime().Before(fj.ModTime())
+ })
+
+ // Remove old files
+ toRemove := files[:len(files)-10]
+ for _, file := range toRemove {
+ fmt.Printf("Removing: %s\n", filepath.Base(file))
+ if err := os.Remove(file); err != nil {
+ common.PrintError("Failed to remove %s: %v\n", file, err)
+ }
+ }
+
+ common.PrintSuccess("\nRemoved %d old baselines\n", len(toRemove))
+ return nil
+}
+
+func extractTagFromBaseline(filename string) string {
+ file, err := os.Open(filename)
+ if err != nil {
+ return ""
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "Tag: ") {
+ return strings.TrimPrefix(line, "Tag: ")
+ }
+ if strings.HasPrefix(line, "----") {
+ break
+ }
+ }
+ return ""
+}
+
+func runBenchstat(baseline, current string) error {
+ cmd := exec.Command("benchstat", baseline, current)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func showSimpleDiff(baseline, current string) error {
+ cmd := exec.Command("diff", "-u", baseline, current)
+ output, _ := cmd.CombinedOutput()
+ fmt.Print(string(output))
+ return nil
+} \ No newline at end of file