diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-26 22:19:22 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-26 22:19:22 +0300 |
| commit | 947e08e4f9e3c9c44b346adff4eb6d68fa79a726 (patch) | |
| tree | cfa94aba72f91d26657de09b7a5b6a23eff10fd7 /internal/tools/common | |
| parent | 1e643ac66765fc0ab4224335191731d8b77fece2 (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.go | 248 | ||||
| -rw-r--r-- | internal/tools/common/utils.go | 213 |
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 |
