Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
This commit is contained in:
544
internal/renderer/micro_bench_test.go
Normal file
544
internal/renderer/micro_bench_test.go
Normal file
@@ -0,0 +1,544 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ungluedlabs/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()
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user