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
Move hosting from GitHub to private Gitea instance.
241 lines
6.3 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|