Files
go-jdenticon/cmd/jdenticon/batch_bench_test.go
Kevin McIntyre f1544ef49c
Some checks failed
CI / Test (Go 1.24.x, ubuntu-latest) (push) Successful in 1m53s
CI / Code Quality (push) Failing after 26s
CI / Security Scan (push) Failing after 11s
CI / Test Coverage (push) Successful in 1m13s
CI / Benchmarks (push) Failing after 10m22s
CI / Build CLI (push) Failing after 8s
Benchmarks / Run Benchmarks (push) Failing after 10m13s
Release / Test (push) Successful in 55s
Release / Build (amd64, darwin, ) (push) Failing after 12s
Release / Build (amd64, linux, ) (push) Failing after 6s
Release / Build (amd64, windows, .exe) (push) Failing after 12s
Release / Build (arm64, darwin, ) (push) Failing after 12s
Release / Build (arm64, linux, ) (push) Failing after 12s
Release / Release (push) Has been skipped
CI / Test (Go 1.24.x, macos-latest) (push) Has been cancelled
CI / Test (Go 1.24.x, windows-latest) (push) Has been cancelled
chore: update module path to gitea.dockr.co/kev/go-jdenticon
Move hosting from GitHub to private Gitea instance.
2026-02-10 10:07:57 -05:00

241 lines
6.3 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gitea.dockr.co/kev/go-jdenticon/jdenticon"
)
// benchmarkSizes defines different test scenarios for batch processing
var benchmarkSizes = []struct {
name string
count int
}{
{"Small", 50},
{"Medium", 200},
{"Large", 1000},
}
// BenchmarkBatchProcessing_Sequential benchmarks sequential processing (concurrency=1)
func BenchmarkBatchProcessing_Sequential(b *testing.B) {
for _, size := range benchmarkSizes {
b.Run(size.name, func(b *testing.B) {
benchmarkBatchWithConcurrency(b, size.count, 1)
})
}
}
// BenchmarkBatchProcessing_Concurrent benchmarks concurrent processing with different worker counts
func BenchmarkBatchProcessing_Concurrent(b *testing.B) {
concurrencyLevels := []int{2, 4, runtime.NumCPU(), runtime.NumCPU() * 2}
for _, size := range benchmarkSizes {
for _, concurrency := range concurrencyLevels {
b.Run(fmt.Sprintf("%s_Workers%d", size.name, concurrency), func(b *testing.B) {
benchmarkBatchWithConcurrency(b, size.count, concurrency)
})
}
}
}
// benchmarkBatchWithConcurrency runs a benchmark with specific parameters
func benchmarkBatchWithConcurrency(b *testing.B, iconCount, concurrency int) {
// Create temporary directory for test
tempDir := b.TempDir()
inputFile := filepath.Join(tempDir, "test-input.txt")
outputDir := filepath.Join(tempDir, "output")
// Generate test input file
createTestInputFile(b, inputFile, iconCount)
// Create generator for testing with complexity limits disabled for consistent benchmarks
config, err := jdenticon.Configure(jdenticon.WithMaxComplexity(-1))
if err != nil {
b.Fatalf("Failed to create config: %v", err)
}
generator, err := jdenticon.NewGeneratorWithConfig(config, concurrency*100)
if err != nil {
b.Fatalf("Failed to create generator: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Clean and recreate output directory for each iteration
os.RemoveAll(outputDir)
if err := os.MkdirAll(outputDir, 0755); err != nil {
b.Fatalf("Failed to create output directory: %v", err)
}
// Measure processing time
start := time.Now()
// Execute batch processing
jobs, total, err := prepareJobs(inputFile, outputDir, FormatSVG, 64)
if err != nil {
b.Fatalf("Failed to prepare jobs: %v", err)
}
if total != iconCount {
b.Fatalf("Expected %d jobs, got %d", iconCount, total)
}
// Process jobs (simplified version without progress bar for benchmarking)
stats := processBenchmarkJobs(jobs, generator, FormatSVG, concurrency)
duration := time.Since(start)
// Verify all jobs completed successfully
processed := atomic.LoadInt64(&stats.processed)
failed := atomic.LoadInt64(&stats.failed)
if processed != int64(iconCount) {
b.Fatalf("Expected %d processed, got %d", iconCount, processed)
}
if failed > 0 {
b.Fatalf("Expected 0 failures, got %d", failed)
}
// Report custom metrics
b.ReportMetric(float64(iconCount)/duration.Seconds(), "icons/sec")
b.ReportMetric(float64(concurrency), "workers")
}
}
// processBenchmarkJobs executes jobs for benchmarking (without context cancellation)
func processBenchmarkJobs(jobs []batchJob, generator *jdenticon.Generator, format FormatFlag, concurrency int) *batchStats {
stats := &batchStats{}
jobChan := make(chan batchJob, len(jobs))
// Start workers
done := make(chan struct{})
for i := 0; i < concurrency; i++ {
go func() {
defer func() { done <- struct{}{} }()
for job := range jobChan {
if err := processJob(context.Background(), job, generator, format); err != nil {
atomic.AddInt64(&stats.failed, 1)
} else {
atomic.AddInt64(&stats.processed, 1)
}
}
}()
}
// Send jobs
go func() {
defer close(jobChan)
for _, job := range jobs {
jobChan <- job
}
}()
// Wait for completion
for i := 0; i < concurrency; i++ {
<-done
}
return stats
}
// createTestInputFile generates a test input file with specified number of entries
func createTestInputFile(b *testing.B, filename string, count int) {
file, err := os.Create(filename)
if err != nil {
b.Fatalf("Failed to create test input file: %v", err)
}
defer file.Close()
var builder strings.Builder
for i := 0; i < count; i++ {
builder.WriteString(fmt.Sprintf("user%d@example.com\n", i))
}
if _, err := file.WriteString(builder.String()); err != nil {
b.Fatalf("Failed to write test input file: %v", err)
}
}
// BenchmarkJobPreparation benchmarks the job preparation phase
func BenchmarkJobPreparation(b *testing.B) {
for _, size := range benchmarkSizes {
b.Run(size.name, func(b *testing.B) {
tempDir := b.TempDir()
inputFile := filepath.Join(tempDir, "test-input.txt")
outputDir := filepath.Join(tempDir, "output")
createTestInputFile(b, inputFile, size.count)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
jobs, total, err := prepareJobs(inputFile, outputDir, FormatSVG, 64)
if err != nil {
b.Fatalf("Failed to prepare jobs: %v", err)
}
if total != size.count {
b.Fatalf("Expected %d jobs, got %d", size.count, total)
}
// Prevent compiler optimization
_ = jobs
}
})
}
}
// BenchmarkSingleJobProcessing benchmarks individual job processing
func BenchmarkSingleJobProcessing(b *testing.B) {
tempDir := b.TempDir()
config, err := jdenticon.Configure(jdenticon.WithMaxComplexity(-1))
if err != nil {
b.Fatalf("Failed to create config: %v", err)
}
generator, err := jdenticon.NewGeneratorWithConfig(config, 100)
if err != nil {
b.Fatalf("Failed to create generator: %v", err)
}
job := batchJob{
value: "test@example.com",
outputPath: filepath.Join(tempDir, "test.svg"),
size: 64,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := processJob(context.Background(), job, generator, FormatSVG); err != nil {
b.Fatalf("Failed to process job: %v", err)
}
// Clean up for next iteration
os.Remove(job.outputPath)
}
}
// BenchmarkConcurrencyScaling analyzes how performance scales with worker count
func BenchmarkConcurrencyScaling(b *testing.B) {
const iconCount = 500
maxWorkers := runtime.NumCPU() * 2
for workers := 1; workers <= maxWorkers; workers *= 2 {
b.Run(fmt.Sprintf("Workers%d", workers), func(b *testing.B) {
benchmarkBatchWithConcurrency(b, iconCount, workers)
})
}
}