diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-25 23:10:24 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-25 23:10:24 +0300 |
| commit | 41ec9cf2942edc7be58d78e49a050131bb2faf8c (patch) | |
| tree | a3f9dbd423c120f76e629f06524381476e948e9a /benchmarks/benchmark_helpers.go | |
| parent | 281360144171c98641f50e938c439915c9b2580a (diff) | |
Add comprehensive benchmarking framework for DTail
- Create benchmark framework to measure performance of dcat, dgrep, and dmap
- Generate test files of 10MB, 100MB, and 1GB with configurable patterns
- Support benchmarking with gzip and zstd compressed files
- Implement tool-specific benchmarks:
* DCat: Simple reading, multiple files, compressed files
* DGrep: Pattern matching, regex complexity, context lines, inverted grep
* DMap: Aggregations, group by operations, complex queries, time intervals
- Track performance metrics: throughput (MB/sec), lines/sec, memory usage
- Save results in multiple formats: JSON, CSV, and Markdown reports
- Add Makefile targets: benchmark, benchmark-quick, benchmark-full
- Support environment variables for configuration (sizes, timeouts, etc.)
- Automatically clean up temporary .tmp files after benchmarks
The framework provides consistent performance testing across the DTail toolset
and enables tracking performance regressions between commits.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'benchmarks/benchmark_helpers.go')
| -rw-r--r-- | benchmarks/benchmark_helpers.go | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/benchmarks/benchmark_helpers.go b/benchmarks/benchmark_helpers.go new file mode 100644 index 0000000..0177809 --- /dev/null +++ b/benchmarks/benchmark_helpers.go @@ -0,0 +1,261 @@ +package benchmarks + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +// BenchmarkResult captures performance metrics from a benchmark run +type BenchmarkResult struct { + Timestamp time.Time + Tool string // dcat, dgrep, dmap + Operation string // specific benchmark name + FileSize int64 + Duration time.Duration + Throughput float64 // MB/sec + LinesPerSec float64 + MemoryUsage int64 + CPUTime time.Duration + ExitCode int + Error error + GitCommit string + GoVersion string +} + +// CommandResult captures the output and metrics from running a command +type CommandResult struct { + Stdout string + Stderr string + Duration time.Duration + ExitCode int + MemoryUsage int64 + Error error +} + +// RunBenchmarkCommand executes a DTail command and captures metrics +func RunBenchmarkCommand(b *testing.B, cmd string, args ...string) (*CommandResult, error) { + b.Helper() + + // Look for command in parent directory (from benchmarks/ to ../) + cmdPath := filepath.Join("..", cmd) + if _, err := os.Stat(cmdPath); err != nil { + return nil, fmt.Errorf("command %s not found: %w", cmdPath, err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + command := exec.CommandContext(ctx, cmdPath, args...) + + var stdout, stderr bytes.Buffer + command.Stdout = &stdout + command.Stderr = &stderr + + startTime := time.Now() + err := command.Run() + duration := time.Since(startTime) + + result := &CommandResult{ + Stdout: stdout.String(), + Stderr: stderr.String(), + Duration: duration, + Error: err, + } + + if exitErr, ok := err.(*exec.ExitError); ok { + result.ExitCode = exitErr.ExitCode() + } else if err == nil { + result.ExitCode = 0 + } else { + result.ExitCode = -1 + } + + // Note: Memory usage tracking would require platform-specific code + // or running under a profiler. For now, we'll leave it as 0. + result.MemoryUsage = 0 + + return result, nil +} + +// CalculateThroughput computes MB/sec from file size and duration +func CalculateThroughput(fileSize int64, duration time.Duration) float64 { + if duration == 0 { + return 0 + } + megabytes := float64(fileSize) / (1024 * 1024) + seconds := duration.Seconds() + return megabytes / seconds +} + +// CalculateLinesPerSecond computes lines/sec from line count and duration +func CalculateLinesPerSecond(lineCount int, duration time.Duration) float64 { + if duration == 0 { + return 0 + } + return float64(lineCount) / duration.Seconds() +} + +// CountFileLines counts the number of lines in a file +func CountFileLines(filename string) (int, error) { + file, err := os.Open(filename) + if err != nil { + return 0, err + } + defer file.Close() + + // Use wc -l equivalent for efficiency + cmd := exec.Command("wc", "-l", filename) + output, err := cmd.Output() + if err != nil { + return 0, err + } + + var lines int + fmt.Sscanf(string(output), "%d", &lines) + return lines, nil +} + +// GetFileSize returns the size of a file in bytes +func GetFileSize(filename string) (int64, error) { + info, err := os.Stat(filename) + if err != nil { + return 0, err + } + return info.Size(), nil +} + +// SetupBenchmark prepares the benchmark environment +func SetupBenchmark(b *testing.B) func() { + b.Helper() + + // Store original working directory + originalWd, err := os.Getwd() + if err != nil { + b.Fatalf("Failed to get working directory: %v", err) + } + + // Ensure we're in the benchmarks directory + if !strings.HasSuffix(originalWd, "benchmarks") { + benchDir := filepath.Join(originalWd, "benchmarks") + if err := os.Chdir(benchDir); err != nil { + b.Fatalf("Failed to change to benchmarks directory: %v", err) + } + } + + // Clean up any leftover files + if err := CleanupBenchmarkFiles(""); err != nil { + b.Logf("Warning: failed to cleanup old files: %v", err) + } + + // Return cleanup function + return func() { + // Clean up benchmark files + if keepFiles := os.Getenv("DTAIL_BENCH_KEEP_FILES"); keepFiles != "true" { + if err := CleanupBenchmarkFiles(""); err != nil { + b.Logf("Warning: failed to cleanup files: %v", err) + } + } + + // Restore working directory + os.Chdir(originalWd) + } +} + +// ReportBenchmarkMetrics adds custom metrics to benchmark results +func ReportBenchmarkMetrics(b *testing.B, result *BenchmarkResult) { + b.Helper() + + if result.Throughput > 0 { + b.ReportMetric(result.Throughput, "MB/sec") + } + + if result.LinesPerSec > 0 { + b.ReportMetric(result.LinesPerSec, "lines/sec") + } + + if result.MemoryUsage > 0 { + b.ReportMetric(float64(result.MemoryUsage)/(1024*1024), "MB_memory") + } +} + +// GetBenchmarkSizes returns the file sizes to test based on environment +func GetBenchmarkSizes() []FileSize { + sizesEnv := os.Getenv("DTAIL_BENCH_SIZES") + if sizesEnv == "" { + // Default to all sizes + return []FileSize{Small, Medium, Large} + } + + var sizes []FileSize + for _, sizeStr := range strings.Split(sizesEnv, ",") { + switch strings.ToLower(strings.TrimSpace(sizeStr)) { + case "small", "10mb": + sizes = append(sizes, Small) + case "medium", "100mb": + sizes = append(sizes, Medium) + case "large", "1gb": + sizes = append(sizes, Large) + } + } + + if len(sizes) == 0 { + // Fallback to small if nothing valid specified + return []FileSize{Small} + } + + return sizes +} + +// IsQuickMode checks if we should run quick benchmarks only +func IsQuickMode() bool { + return os.Getenv("DTAIL_BENCH_QUICK") == "true" +} + +// GetBenchmarkTimeout returns the timeout for benchmark operations +func GetBenchmarkTimeout() time.Duration { + timeoutStr := os.Getenv("DTAIL_BENCH_TIMEOUT") + if timeoutStr == "" { + return 30 * time.Minute + } + + timeout, err := time.ParseDuration(timeoutStr) + if err != nil { + return 30 * time.Minute + } + + return timeout +} + +// GetGitCommit returns the current git commit hash +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)) +} + +// GetGoVersion returns the Go version +func GetGoVersion() string { + return runtime.Version() +} + +// WarmupCommand runs a command once to warm up caches +func WarmupCommand(b *testing.B, cmd string, args ...string) { + b.Helper() + + // Run once without timing + _, err := RunBenchmarkCommand(b, cmd, args...) + if err != nil { + b.Logf("Warmup run failed (this may be expected): %v", err) + } +}
\ No newline at end of file |
