summaryrefslogtreecommitdiff
path: root/internal/tools/common
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/common
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/common')
-rw-r--r--internal/tools/common/data_generator.go248
-rw-r--r--internal/tools/common/utils.go213
2 files changed, 461 insertions, 0 deletions
diff --git a/internal/tools/common/data_generator.go b/internal/tools/common/data_generator.go
new file mode 100644
index 0000000..f9c4e5e
--- /dev/null
+++ b/internal/tools/common/data_generator.go
@@ -0,0 +1,248 @@
+package common
+
+import (
+ "bufio"
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// DataFormat represents the format of generated data
+type DataFormat string
+
+const (
+ FormatLog DataFormat = "log"
+ FormatCSV DataFormat = "csv"
+ FormatDTail DataFormat = "dtail"
+ FormatMapReduce DataFormat = "mapreduce"
+)
+
+// DataGenerator generates test data for profiling and benchmarking
+type DataGenerator struct {
+ rand *rand.Rand
+}
+
+// NewDataGenerator creates a new data generator
+func NewDataGenerator() *DataGenerator {
+ return &DataGenerator{
+ rand: rand.New(rand.NewSource(time.Now().UnixNano())),
+ }
+}
+
+// GenerateFile generates a test data file of the specified size and format
+func (g *DataGenerator) GenerateFile(filename string, sizeStr string, format DataFormat) error {
+ size, err := ParseSize(sizeStr)
+ if err != nil {
+ return fmt.Errorf("invalid size: %w", err)
+ }
+
+ // Create directory if needed
+ dir := filepath.Dir(filename)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("failed to create directory: %w", err)
+ }
+
+ // Check if file already exists
+ if _, err := os.Stat(filename); err == nil {
+ return nil // File exists, skip generation
+ }
+
+ switch format {
+ case FormatLog:
+ return g.generateLogFile(filename, size)
+ case FormatCSV:
+ return g.generateCSVFile(filename, size)
+ case FormatDTail, FormatMapReduce:
+ return g.generateDTailFormatFile(filename, size)
+ default:
+ return fmt.Errorf("unsupported format: %s", format)
+ }
+}
+
+// GenerateLogFileWithLines generates a log file with specific number of lines
+func (g *DataGenerator) GenerateLogFileWithLines(filename string, lines int, format DataFormat) error {
+ // Create directory if needed
+ dir := filepath.Dir(filename)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("failed to create directory: %w", err)
+ }
+
+ // Check if file already exists
+ if _, err := os.Stat(filename); err == nil {
+ return nil // File exists, skip generation
+ }
+
+ switch format {
+ case FormatDTail, FormatMapReduce:
+ return g.generateDTailFormatFileWithLines(filename, lines)
+ default:
+ return fmt.Errorf("line-based generation only supported for dtail/mapreduce format")
+ }
+}
+
+func (g *DataGenerator) generateLogFile(filename string, targetSize int64) error {
+ file, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ writer := bufio.NewWriter(file)
+ defer writer.Flush()
+
+ var currentSize int64
+ lineNum := 0
+ levels := []string{"INFO", "DEBUG", "WARN", "ERROR"}
+ users := []string{"user1", "user2", "user3", "user4", "user5", "admin", "guest", "service", "monitor", "test"}
+ actions := []string{"login", "logout", "query", "update", "delete", "create", "read", "write", "sync", "backup"}
+
+ for currentSize < targetSize {
+ lineNum++
+ timestamp := time.Now().Add(time.Duration(-lineNum) * time.Second).Format("2006-01-02 15:04:05")
+ level := levels[g.rand.Intn(len(levels))]
+ user := users[g.rand.Intn(len(users))]
+ action := actions[g.rand.Intn(len(actions))]
+ duration := g.rand.Intn(5000) + 100
+ status := "success"
+ if g.rand.Float32() < 0.1 {
+ status = "failure"
+ }
+
+ line := fmt.Sprintf("[%s] %s - User %s performed %s action (duration: %dms, status: %s)\n",
+ timestamp, level, user, action, duration, status)
+
+ n, err := writer.WriteString(line)
+ if err != nil {
+ return err
+ }
+ currentSize += int64(n)
+ }
+
+ return nil
+}
+
+func (g *DataGenerator) generateCSVFile(filename string, targetSize int64) error {
+ file, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ writer := bufio.NewWriter(file)
+ defer writer.Flush()
+
+ // Write header
+ header := "timestamp,user,action,duration,status\n"
+ n, err := writer.WriteString(header)
+ if err != nil {
+ return err
+ }
+ currentSize := int64(n)
+
+ lineNum := 0
+ users := []string{"user1", "user2", "user3", "user4", "user5", "admin", "guest", "service", "monitor", "test"}
+ actions := []string{"login", "logout", "query", "update", "delete", "create", "read", "write", "sync", "backup"}
+
+ for currentSize < targetSize {
+ lineNum++
+ timestamp := time.Now().Add(time.Duration(-lineNum) * time.Second).Format("2006-01-02 15:04:05")
+ user := users[g.rand.Intn(len(users))]
+ action := actions[g.rand.Intn(len(actions))]
+ duration := g.rand.Intn(5000) + 100
+ status := "success"
+ if g.rand.Float32() < 0.1 {
+ status = "failure"
+ }
+
+ line := fmt.Sprintf("%s,%s,%s,%d,%s\n", timestamp, user, action, duration, status)
+
+ n, err := writer.WriteString(line)
+ if err != nil {
+ return err
+ }
+ currentSize += int64(n)
+ }
+
+ return nil
+}
+
+func (g *DataGenerator) generateDTailFormatFile(filename string, targetSize int64) error {
+ file, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ writer := bufio.NewWriter(file)
+ defer writer.Flush()
+
+ var currentSize int64
+ lineNum := 0
+ hostnames := []string{"server01", "server02", "server03", "server04", "server05",
+ "server06", "server07", "server08", "server09", "server10"}
+
+ for currentSize < targetSize {
+ lineNum++
+ hostname := hostnames[lineNum%len(hostnames)]
+ timestamp := fmt.Sprintf("%02d%02d-%02d%02d%02d",
+ 10+(lineNum/86400)%12, (lineNum/3600)%30+1,
+ (lineNum/3600)%24, (lineNum/60)%60, lineNum%60)
+ goroutines := 10 + (lineNum % 50)
+ cgocalls := lineNum % 100
+ cpus := 1 + (lineNum % 8)
+ loadavg := float64(lineNum%100) / 100.0
+ uptime := fmt.Sprintf("%dh%dm%ds", lineNum/3600, (lineNum/60)%60, lineNum%60)
+ currentConnections := lineNum % 20
+ lifetimeConnections := 1000 + lineNum
+
+ line := fmt.Sprintf("INFO|%s|1|stats.go:56|%d|%d|%d|%.2f|%s|MAPREDUCE:STATS|hostname=%s|currentConnections=%d|lifetimeConnections=%d\n",
+ timestamp, cpus, goroutines, cgocalls, loadavg, uptime, hostname, currentConnections, lifetimeConnections)
+
+ n, err := writer.WriteString(line)
+ if err != nil {
+ return err
+ }
+ currentSize += int64(n)
+ }
+
+ return nil
+}
+
+func (g *DataGenerator) generateDTailFormatFileWithLines(filename string, lines int) error {
+ file, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ writer := bufio.NewWriter(file)
+ defer writer.Flush()
+
+ hostnames := []string{"server01", "server02", "server03", "server04", "server05",
+ "server06", "server07", "server08", "server09", "server10"}
+
+ for i := 1; i <= lines; i++ {
+ hostname := hostnames[i%len(hostnames)]
+ timestamp := fmt.Sprintf("%02d%02d-%02d%02d%02d",
+ 10+(i/86400)%12, (i/3600)%30+1,
+ (i/3600)%24, (i/60)%60, i%60)
+ goroutines := 10 + (i % 50)
+ cgocalls := i % 100
+ cpus := 1 + (i % 8)
+ loadavg := float64(i%100) / 100.0
+ uptime := fmt.Sprintf("%dh%dm%ds", i/3600, (i/60)%60, i%60)
+ currentConnections := i % 20
+ lifetimeConnections := 1000 + i
+
+ line := fmt.Sprintf("INFO|%s|1|stats.go:56|%d|%d|%d|%.2f|%s|MAPREDUCE:STATS|hostname=%s|currentConnections=%d|lifetimeConnections=%d\n",
+ timestamp, cpus, goroutines, cgocalls, loadavg, uptime, hostname, currentConnections, lifetimeConnections)
+
+ if _, err := writer.WriteString(line); err != nil {
+ return err
+ }
+ }
+
+ return nil
+} \ No newline at end of file
diff --git a/internal/tools/common/utils.go b/internal/tools/common/utils.go
new file mode 100644
index 0000000..37f115a
--- /dev/null
+++ b/internal/tools/common/utils.go
@@ -0,0 +1,213 @@
+package common
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// ParseSize parses a size string like "10MB", "1GB" into bytes
+func ParseSize(sizeStr string) (int64, error) {
+ originalStr := sizeStr
+ sizeStr = strings.ToUpper(strings.TrimSpace(sizeStr))
+
+ // Handle single-letter suffixes (K, M, G, T) by adding B
+ if len(sizeStr) > 1 {
+ lastChar := sizeStr[len(sizeStr)-1]
+ secondLastChar := byte('0')
+ if len(sizeStr) > 1 {
+ secondLastChar = sizeStr[len(sizeStr)-2]
+ }
+
+ // If ends with K, M, G, or T and the character before it is a digit, add B
+ if (lastChar == 'K' || lastChar == 'M' || lastChar == 'G' || lastChar == 'T') &&
+ (secondLastChar >= '0' && secondLastChar <= '9') {
+ sizeStr = sizeStr + "B"
+ }
+ }
+
+ // Order matters - check longer suffixes first
+ suffixes := []struct {
+ suffix string
+ multiplier int64
+ }{
+ {"TB", 1024 * 1024 * 1024 * 1024},
+ {"GB", 1024 * 1024 * 1024},
+ {"MB", 1024 * 1024},
+ {"KB", 1024},
+ {"B", 1},
+ }
+
+ for _, s := range suffixes {
+ if strings.HasSuffix(sizeStr, s.suffix) {
+ numStr := strings.TrimSuffix(sizeStr, s.suffix)
+ numStr = strings.TrimSpace(numStr)
+ if numStr == "" {
+ return 0, fmt.Errorf("no number before size suffix")
+ }
+ num, err := strconv.ParseFloat(numStr, 64)
+ if err != nil {
+ return 0, fmt.Errorf("invalid size number: %s (original: %s, processed: %s)", numStr, originalStr, sizeStr)
+ }
+ return int64(num * float64(s.multiplier)), nil
+ }
+ }
+
+ // Try parsing as plain number (assume bytes)
+ num, err := strconv.ParseInt(sizeStr, 10, 64)
+ if err != nil {
+ return 0, fmt.Errorf("invalid size format: %s", sizeStr)
+ }
+ return num, nil
+}
+
+// FormatSize formats bytes into human-readable size
+func FormatSize(bytes int64) string {
+ const unit = 1024
+ if bytes < unit {
+ return fmt.Sprintf("%d B", bytes)
+ }
+ div, exp := int64(unit), 0
+ for n := bytes / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
+}
+
+// BuildCommand builds a dtail command if it doesn't exist
+func BuildCommand(cmd string) error {
+ // Check if binary exists
+ if _, err := os.Stat(cmd); err == nil {
+ return nil // Already exists
+ }
+
+ // Build the command
+ cmdName := filepath.Base(cmd)
+ buildCmd := exec.Command("go", "build", "-o", cmd, fmt.Sprintf("./cmd/%s/main.go", cmdName))
+ buildCmd.Stdout = os.Stdout
+ buildCmd.Stderr = os.Stderr
+
+ fmt.Printf("Building %s...\n", cmdName)
+ return buildCmd.Run()
+}
+
+// BuildCommands builds multiple dtail commands
+func BuildCommands(commands ...string) error {
+ for _, cmd := range commands {
+ if err := BuildCommand(cmd); err != nil {
+ return fmt.Errorf("failed to build %s: %w", cmd, err)
+ }
+ }
+ return nil
+}
+
+// EnsureDirectory creates a directory if it doesn't exist
+func EnsureDirectory(dir string) error {
+ return os.MkdirAll(dir, 0755)
+}
+
+// FileExists checks if a file exists
+func FileExists(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+}
+
+// GetTimestamp returns a timestamp string for file naming
+func GetTimestamp() string {
+ return time.Now().Format("20060102_150405")
+}
+
+// GetGitCommit returns the current git commit hash (short form)
+func GetGitCommit() string {
+ cmd := exec.Command("git", "rev-parse", "--short", "HEAD")
+ output, err := cmd.Output()
+ if err != nil {
+ return "unknown"
+ }
+ return strings.TrimSpace(string(output))
+}
+
+// RunCommandWithTimeout runs a command with a timeout
+func RunCommandWithTimeout(timeout time.Duration, name string, args ...string) error {
+ cmd := exec.Command(name, args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+
+ done := make(chan error, 1)
+ go func() {
+ done <- cmd.Wait()
+ }()
+
+ select {
+ case <-time.After(timeout):
+ if err := cmd.Process.Kill(); err != nil {
+ return fmt.Errorf("failed to kill process: %w", err)
+ }
+ return fmt.Errorf("command timed out after %v", timeout)
+ case err := <-done:
+ return err
+ }
+}
+
+// CleanupFiles removes temporary files matching patterns
+func CleanupFiles(patterns ...string) error {
+ for _, pattern := range patterns {
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("invalid pattern %s: %w", pattern, err)
+ }
+ for _, match := range matches {
+ if err := os.Remove(match); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to remove %s: %w", match, err)
+ }
+ }
+ }
+ return nil
+}
+
+// Colors for terminal output
+const (
+ ColorReset = "\033[0m"
+ ColorRed = "\033[0;31m"
+ ColorGreen = "\033[0;32m"
+ ColorYellow = "\033[1;33m"
+ ColorBlue = "\033[0;34m"
+ ColorPurple = "\033[0;35m"
+ ColorCyan = "\033[0;36m"
+ ColorWhite = "\033[0;37m"
+)
+
+// PrintColored prints colored text to stdout
+func PrintColored(color, format string, args ...interface{}) {
+ fmt.Printf(color+format+ColorReset, args...)
+}
+
+// PrintSection prints a section header
+func PrintSection(title string) {
+ PrintColored(ColorGreen, "%s\n", title)
+ fmt.Println(strings.Repeat("=", len(title)))
+}
+
+// PrintInfo prints an info message
+func PrintInfo(format string, args ...interface{}) {
+ PrintColored(ColorYellow, format, args...)
+}
+
+// PrintError prints an error message
+func PrintError(format string, args ...interface{}) {
+ PrintColored(ColorRed, format, args...)
+}
+
+// PrintSuccess prints a success message
+func PrintSuccess(format string, args ...interface{}) {
+ PrintColored(ColorGreen, format, args...)
+} \ No newline at end of file