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.
545 lines
14 KiB
Go
545 lines
14 KiB
Go
package renderer
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dockr.co/kev/go-jdenticon/internal/engine"
|
|
)
|
|
|
|
// ============================================================================
|
|
// RENDERER MICRO-BENCHMARKS FOR MEMORY ALLOCATION ANALYSIS
|
|
// ============================================================================
|
|
|
|
var (
|
|
// Test data for renderer benchmarks
|
|
benchTestPoints = []engine.Point{
|
|
{X: 0.0, Y: 0.0},
|
|
{X: 10.5, Y: 0.0},
|
|
{X: 10.5, Y: 10.5},
|
|
{X: 0.0, Y: 10.5},
|
|
}
|
|
benchTestColors = []string{
|
|
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
|
|
}
|
|
benchTestSizes = []int{32, 64, 128, 256}
|
|
)
|
|
|
|
// ============================================================================
|
|
// SVG STRING BUILDING MICRO-BENCHMARKS
|
|
// ============================================================================
|
|
|
|
// BenchmarkSVGStringBuilding tests different string building patterns in SVG generation
|
|
func BenchmarkSVGStringBuilding(b *testing.B) {
|
|
points := benchTestPoints
|
|
|
|
b.Run("svgValue_formatting", func(b *testing.B) {
|
|
values := []float64{0.0, 10.5, 15.75, 100.0, 256.5}
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
value := values[i%len(values)]
|
|
_ = svgValue(value)
|
|
}
|
|
})
|
|
|
|
b.Run("strconv_FormatFloat", func(b *testing.B) {
|
|
value := 10.5
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = strconv.FormatFloat(value, 'f', 1, 64)
|
|
}
|
|
})
|
|
|
|
b.Run("strconv_Itoa", func(b *testing.B) {
|
|
value := 10
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = strconv.Itoa(value)
|
|
}
|
|
})
|
|
|
|
b.Run("polygon_path_building", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
var buf strings.Builder
|
|
buf.Grow(50) // Estimate capacity
|
|
|
|
// Simulate polygon path building
|
|
buf.WriteString("M")
|
|
buf.WriteString(svgValue(points[0].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[0].Y))
|
|
|
|
for j := 1; j < len(points); j++ {
|
|
buf.WriteString("L")
|
|
buf.WriteString(svgValue(points[j].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[j].Y))
|
|
}
|
|
buf.WriteString("Z")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkSVGPathOperations tests SVGPath struct operations
|
|
func BenchmarkSVGPathOperations(b *testing.B) {
|
|
points := benchTestPoints
|
|
|
|
b.Run("SVGPath_AddPolygon", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
path := &SVGPath{}
|
|
path.AddPolygon(points)
|
|
}
|
|
})
|
|
|
|
b.Run("SVGPath_AddCircle", func(b *testing.B) {
|
|
topLeft := engine.Point{X: 5.0, Y: 5.0}
|
|
size := 10.0
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
path := &SVGPath{}
|
|
path.AddCircle(topLeft, size, false)
|
|
}
|
|
})
|
|
|
|
b.Run("SVGPath_DataString", func(b *testing.B) {
|
|
path := &SVGPath{}
|
|
path.AddPolygon(points)
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = path.DataString()
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkStringBuilderPooling tests the efficiency of string builder pooling
|
|
func BenchmarkStringBuilderPooling(b *testing.B) {
|
|
points := benchTestPoints
|
|
|
|
b.Run("direct_builder", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
// Use direct string builder (pool eliminated for direct writing)
|
|
var buf strings.Builder
|
|
|
|
// Build polygon path
|
|
buf.WriteString("M")
|
|
buf.WriteString(svgValue(points[0].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[0].Y))
|
|
for j := 1; j < len(points); j++ {
|
|
buf.WriteString("L")
|
|
buf.WriteString(svgValue(points[j].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[j].Y))
|
|
}
|
|
buf.WriteString("Z")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
|
|
b.Run("without_pool", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
// Create new buffer each time
|
|
var buf strings.Builder
|
|
buf.Grow(50)
|
|
|
|
// Build polygon path
|
|
buf.WriteString("M")
|
|
buf.WriteString(svgValue(points[0].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[0].Y))
|
|
for j := 1; j < len(points); j++ {
|
|
buf.WriteString("L")
|
|
buf.WriteString(svgValue(points[j].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[j].Y))
|
|
}
|
|
buf.WriteString("Z")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
|
|
b.Run("reused_builder", func(b *testing.B) {
|
|
var buf strings.Builder
|
|
buf.Grow(100) // Pre-allocate larger buffer
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
buf.Reset()
|
|
|
|
// Build polygon path
|
|
buf.WriteString("M")
|
|
buf.WriteString(svgValue(points[0].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[0].Y))
|
|
for j := 1; j < len(points); j++ {
|
|
buf.WriteString("L")
|
|
buf.WriteString(svgValue(points[j].X))
|
|
buf.WriteString(" ")
|
|
buf.WriteString(svgValue(points[j].Y))
|
|
}
|
|
buf.WriteString("Z")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// SVG RENDERER MICRO-BENCHMARKS
|
|
// ============================================================================
|
|
|
|
// BenchmarkSVGRendererOperations tests SVG renderer creation and operations
|
|
func BenchmarkSVGRendererOperations(b *testing.B) {
|
|
sizes := benchTestSizes
|
|
colors := benchTestColors
|
|
|
|
b.Run("NewSVGRenderer", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
size := sizes[i%len(sizes)]
|
|
_ = NewSVGRenderer(size)
|
|
}
|
|
})
|
|
|
|
b.Run("BeginShape", func(b *testing.B) {
|
|
renderer := NewSVGRenderer(64)
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
color := colors[i%len(colors)]
|
|
renderer.BeginShape(color)
|
|
}
|
|
})
|
|
|
|
b.Run("AddPolygon", func(b *testing.B) {
|
|
renderer := NewSVGRenderer(64)
|
|
renderer.BeginShape(colors[0])
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer.AddPolygon(benchTestPoints)
|
|
}
|
|
})
|
|
|
|
b.Run("AddCircle", func(b *testing.B) {
|
|
renderer := NewSVGRenderer(64)
|
|
renderer.BeginShape(colors[0])
|
|
topLeft := engine.Point{X: 5.0, Y: 5.0}
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer.AddCircle(topLeft, 10.0, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkSVGGeneration tests full SVG generation with different scenarios
|
|
func BenchmarkSVGGeneration(b *testing.B) {
|
|
colors := benchTestColors[:3] // Use fewer colors for cleaner tests
|
|
|
|
b.Run("empty_renderer", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer := NewSVGRenderer(64)
|
|
_ = renderer.ToSVG()
|
|
}
|
|
})
|
|
|
|
b.Run("single_shape", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer := NewSVGRenderer(64)
|
|
renderer.BeginShape(colors[0])
|
|
renderer.AddPolygon(benchTestPoints)
|
|
renderer.EndShape()
|
|
_ = renderer.ToSVG()
|
|
}
|
|
})
|
|
|
|
b.Run("multiple_shapes", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer := NewSVGRenderer(64)
|
|
for j, color := range colors {
|
|
renderer.BeginShape(color)
|
|
// Offset points for each shape
|
|
offsetPoints := make([]engine.Point, len(benchTestPoints))
|
|
for k, point := range benchTestPoints {
|
|
offsetPoints[k] = engine.Point{
|
|
X: point.X + float64(j*12),
|
|
Y: point.Y + float64(j*12),
|
|
}
|
|
}
|
|
renderer.AddPolygon(offsetPoints)
|
|
renderer.EndShape()
|
|
}
|
|
_ = renderer.ToSVG()
|
|
}
|
|
})
|
|
|
|
b.Run("with_background", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
renderer := NewSVGRenderer(64)
|
|
renderer.SetBackground("#ffffff", 1.0)
|
|
renderer.BeginShape(colors[0])
|
|
renderer.AddPolygon(benchTestPoints)
|
|
renderer.EndShape()
|
|
_ = renderer.ToSVG()
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkSVGSizeEstimation tests SVG capacity estimation
|
|
func BenchmarkSVGSizeEstimation(b *testing.B) {
|
|
colors := benchTestColors
|
|
|
|
b.Run("capacity_estimation", func(b *testing.B) {
|
|
renderer := NewSVGRenderer(64)
|
|
for _, color := range colors {
|
|
renderer.BeginShape(color)
|
|
renderer.AddPolygon(benchTestPoints)
|
|
renderer.EndShape()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
// Simulate capacity estimation logic from ToSVG
|
|
capacity := svgBaseOverheadBytes
|
|
capacity += svgBackgroundRectBytes // Assume background
|
|
|
|
// Estimate path data size
|
|
for _, color := range renderer.colorOrder {
|
|
path := renderer.pathsByColor[color]
|
|
if path != nil {
|
|
capacity += svgPathOverheadBytes + path.data.Len()
|
|
}
|
|
}
|
|
_ = capacity
|
|
}
|
|
})
|
|
|
|
b.Run("strings_builder_with_estimation", func(b *testing.B) {
|
|
renderer := NewSVGRenderer(64)
|
|
for _, color := range colors {
|
|
renderer.BeginShape(color)
|
|
renderer.AddPolygon(benchTestPoints)
|
|
renderer.EndShape()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
// Test strings.Builder with capacity estimation
|
|
capacity := svgBaseOverheadBytes + svgBackgroundRectBytes
|
|
for _, color := range renderer.colorOrder {
|
|
path := renderer.pathsByColor[color]
|
|
if path != nil {
|
|
capacity += svgPathOverheadBytes + path.data.Len()
|
|
}
|
|
}
|
|
|
|
var svg strings.Builder
|
|
svg.Grow(capacity)
|
|
svg.WriteString(`<svg xmlns="http://www.w3.org/2000/svg">`)
|
|
svg.WriteString("</svg>")
|
|
_ = svg.String()
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP OPERATIONS MICRO-BENCHMARKS
|
|
// ============================================================================
|
|
|
|
// BenchmarkMapOperations tests map operations used in renderer
|
|
func BenchmarkMapOperations(b *testing.B) {
|
|
colors := benchTestColors
|
|
|
|
b.Run("map_creation", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
m := make(map[string]*SVGPath)
|
|
_ = m
|
|
}
|
|
})
|
|
|
|
b.Run("map_insertion", func(b *testing.B) {
|
|
m := make(map[string]*SVGPath)
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
color := colors[i%len(colors)]
|
|
m[color] = &SVGPath{}
|
|
}
|
|
})
|
|
|
|
b.Run("map_lookup", func(b *testing.B) {
|
|
m := make(map[string]*SVGPath)
|
|
for _, color := range colors {
|
|
m[color] = &SVGPath{}
|
|
}
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
color := colors[i%len(colors)]
|
|
_ = m[color]
|
|
}
|
|
})
|
|
|
|
b.Run("map_existence_check", func(b *testing.B) {
|
|
m := make(map[string]*SVGPath)
|
|
for _, color := range colors {
|
|
m[color] = &SVGPath{}
|
|
}
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
color := colors[i%len(colors)]
|
|
_, exists := m[color]
|
|
_ = exists
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// SLICE OPERATIONS MICRO-BENCHMARKS
|
|
// ============================================================================
|
|
|
|
// BenchmarkSliceOperations tests slice operations for color ordering
|
|
func BenchmarkSliceOperations(b *testing.B) {
|
|
colors := benchTestColors
|
|
|
|
b.Run("slice_append", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
var colorOrder []string
|
|
//lint:ignore S1011 Intentionally benchmarking individual appends vs batch
|
|
//nolint:gosimple // Intentionally benchmarking individual appends vs batch
|
|
for _, color := range colors {
|
|
colorOrder = append(colorOrder, color)
|
|
}
|
|
_ = colorOrder
|
|
}
|
|
})
|
|
|
|
b.Run("slice_with_capacity", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
colorOrder := make([]string, 0, len(colors))
|
|
//lint:ignore S1011 Intentionally benchmarking individual appends with pre-allocation
|
|
//nolint:gosimple // Intentionally benchmarking individual appends with pre-allocation
|
|
for _, color := range colors {
|
|
colorOrder = append(colorOrder, color)
|
|
}
|
|
_ = colorOrder
|
|
}
|
|
})
|
|
|
|
b.Run("slice_iteration", func(b *testing.B) {
|
|
colorOrder := make([]string, len(colors))
|
|
copy(colorOrder, colors)
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
for _, color := range colorOrder {
|
|
_ = color
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// COORDINATE TRANSFORMATION MICRO-BENCHMARKS
|
|
// ============================================================================
|
|
|
|
// BenchmarkCoordinateTransforms tests coordinate transformation patterns
|
|
func BenchmarkCoordinateTransforms(b *testing.B) {
|
|
points := benchTestPoints
|
|
|
|
b.Run("point_creation", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = engine.Point{X: 10.5, Y: 20.5}
|
|
}
|
|
})
|
|
|
|
b.Run("point_slice_creation", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
pointsCopy := make([]engine.Point, len(points))
|
|
copy(pointsCopy, points)
|
|
_ = pointsCopy
|
|
}
|
|
})
|
|
|
|
b.Run("point_transformation", func(b *testing.B) {
|
|
transform := func(x, y float64) (float64, float64) {
|
|
return x * 2.0, y * 2.0
|
|
}
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
transformedPoints := make([]engine.Point, len(points))
|
|
for j, point := range points {
|
|
newX, newY := transform(point.X, point.Y)
|
|
transformedPoints[j] = engine.Point{X: newX, Y: newY}
|
|
}
|
|
_ = transformedPoints
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// MEMORY ALLOCATION PATTERN COMPARISONS
|
|
// ============================================================================
|
|
|
|
// BenchmarkAllocationPatterns compares different allocation patterns used in rendering
|
|
func BenchmarkAllocationPatterns(b *testing.B) {
|
|
b.Run("string_concatenation", func(b *testing.B) {
|
|
base := "test"
|
|
suffix := "value"
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = base + suffix
|
|
}
|
|
})
|
|
|
|
b.Run("sprintf_formatting", func(b *testing.B) {
|
|
value := 10.5
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = strconv.FormatFloat(value, 'f', 1, 64)
|
|
}
|
|
})
|
|
|
|
b.Run("builder_small_capacity", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
var buf strings.Builder
|
|
buf.Grow(10)
|
|
buf.WriteString("test")
|
|
buf.WriteString("value")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
|
|
b.Run("builder_large_capacity", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
var buf strings.Builder
|
|
buf.Grow(100)
|
|
buf.WriteString("test")
|
|
buf.WriteString("value")
|
|
_ = buf.String()
|
|
}
|
|
})
|
|
}
|