From a4d979c49cc134b405b87cb8d56a1a2be66bbcac Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 25 Jul 2026 16:32:11 -0400 Subject: [PATCH] test(thumbhash): benchmark against blurhash at the pipeline input size --- core/artwork/blurhash/blurhash_bench_test.go | 24 +++++++ .../artwork/thumbhash/thumbhash_bench_test.go | 66 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 core/artwork/thumbhash/thumbhash_bench_test.go diff --git a/core/artwork/blurhash/blurhash_bench_test.go b/core/artwork/blurhash/blurhash_bench_test.go index d26a59905..9e393d58f 100644 --- a/core/artwork/blurhash/blurhash_bench_test.go +++ b/core/artwork/blurhash/blurhash_bench_test.go @@ -25,6 +25,30 @@ func benchImage(size int) image.Image { return img } +// BenchmarkEncodeAtInputSize mirrors the thumbhash bench of the same name. *image.RGBA, matching +// makeThumbnail's output, so neither package is measured with a conversion the other avoids. +func BenchmarkEncodeAtInputSize(b *testing.B) { + const size = 100 + img := image.NewRGBA(image.Rect(0, 0, size, size)) + for y := range size { + for x := range size { + img.SetRGBA(x, y, color.RGBA{ + R: uint8(255 * x / size), + G: uint8(255 * y / size), + B: uint8((x + y) * 255 / (2 * size)), + A: 255, + }) + } + } + x, y := blurhash.Components(size, size) + b.ReportAllocs() + for range b.N { + if _, err := blurhash.Encode(img, x, y); err != nil { + b.Fatal(err) + } + } +} + func BenchmarkEncode(b *testing.B) { for _, size := range []int{100, 300, 600, 900, 1200, 1500} { img := benchImage(size) diff --git a/core/artwork/thumbhash/thumbhash_bench_test.go b/core/artwork/thumbhash/thumbhash_bench_test.go new file mode 100644 index 000000000..fb62078c7 --- /dev/null +++ b/core/artwork/thumbhash/thumbhash_bench_test.go @@ -0,0 +1,66 @@ +package thumbhash_test + +import ( + "fmt" + "image" + "image/color" + "testing" + + "github.com/navidrome/navidrome/core/artwork/thumbhash" +) + +// benchImage builds the same deterministic gradient the blurhash bench uses, as *image.RGBA — +// the concrete type makeThumbnail hands both encoders in production. +func benchImage(size int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, size, size)) + for y := range size { + for x := range size { + img.SetRGBA(x, y, color.RGBA{ + R: uint8(255 * x / size), + G: uint8(255 * y / size), + B: uint8((x + y) * 255 / (2 * size)), + A: 255, + }) + } + } + return img +} + +// BenchmarkEncodeAtInputSize is the bar: 100x100 is exactly what the artwork pipeline feeds. +func BenchmarkEncodeAtInputSize(b *testing.B) { + img := benchImage(100) + b.ReportAllocs() + for range b.N { + if _, err := thumbhash.Encode(img); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkEncode(b *testing.B) { + for _, size := range []int{100, 300, 600, 900, 1200, 1500} { + img := benchImage(size) + b.Run(fmt.Sprintf("%dx%d", size, size), func(b *testing.B) { + b.ReportAllocs() + for range b.N { + if _, err := thumbhash.Encode(img); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkReferenceEncode is the naive baseline: four w*h scratch arrays and 40 pixel re-reads. +func BenchmarkReferenceEncode(b *testing.B) { + img := benchImage(100) + w, h := 100, 100 + pix := make([]byte, 0, w*h*4) + for y := range h { + pix = append(pix, img.Pix[y*img.Stride:y*img.Stride+w*4]...) + } + b.ReportAllocs() + for range b.N { + _ = referenceEncode(w, h, pix) + } +}