diff --git a/core/artwork/blurhash/blurhash_bench_test.go b/core/artwork/blurhash/blurhash_bench_test.go deleted file mode 100644 index 6adaece53..000000000 --- a/core/artwork/blurhash/blurhash_bench_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package blurhash_test - -import ( - "fmt" - "image" - "image/color" - "testing" - - "github.com/navidrome/navidrome/core/artwork/blurhash" -) - -// benchImage builds a deterministic gradient so runs are comparable across revisions. -func benchImage(size int) image.Image { - img := image.NewNRGBA(image.Rect(0, 0, size, size)) - for y := 0; y < size; y++ { - for x := 0; x < size; x++ { - img.SetNRGBA(x, y, color.NRGBA{ - R: uint8(255 * x / size), - G: uint8(255 * y / size), - B: uint8((x + y) * 255 / (2 * size)), - A: 255, - }) - } - } - return img -} - -// BenchmarkEncodeAtInputSize mirrors the thumbhash bench of the same name. *image.NRGBA, 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.NewNRGBA(image.Rect(0, 0, size, size)) - for y := range size { - for x := range size { - img.SetNRGBA(x, y, color.NRGBA{ - R: uint8(255 * x / size), - G: uint8(255 * y / size), - B: uint8((x + y) * 255 / (2 * size)), - A: 255, - }) - } - } - b.ReportAllocs() - for range b.N { - if _, err := blurhash.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 := blurhash.Encode(img); err != nil { - b.Fatal(err) - } - } - }) - } -} diff --git a/core/artwork/hash_encoder_bench_test.go b/core/artwork/hash_encoder_bench_test.go new file mode 100644 index 000000000..b2cda4215 --- /dev/null +++ b/core/artwork/hash_encoder_bench_test.go @@ -0,0 +1,52 @@ +package artwork + +import ( + "fmt" + "image" + "testing" + + "github.com/navidrome/navidrome/core/artwork/blurhash" + "github.com/navidrome/navidrome/core/artwork/thumbhash" + "github.com/navidrome/navidrome/tests" +) + +// hashEncoders are the two placeholder hashes decodeArtwork computes from one shared thumbnail. +var hashEncoders = []struct { + name string + encode func(image.Image) error +}{ + {"blurhash", func(img image.Image) error { _, err := blurhash.Encode(img); return err }}, + {"thumbhash", func(img image.Image) error { _, err := thumbhash.Encode(img); return err }}, +} + +func benchEncoder(b *testing.B, encode func(image.Image) error, img image.Image) { + b.Helper() + b.ReportAllocs() + for b.Loop() { + if err := encode(img); err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkHashEncodersAtInputSize is the bar: both encoders are handed the identical image +// makeThumbnail produces, so neither is measured with a conversion the other avoids. +func BenchmarkHashEncodersAtInputSize(b *testing.B) { + img := tests.GradientImage(thumbnailSize) + for _, e := range hashEncoders { + b.Run(e.name, func(b *testing.B) { benchEncoder(b, e.encode, img) }) + } +} + +// BenchmarkHashEncoders sweeps past the pipeline's input size, where each package's own defensive +// downscale starts to dominate. +func BenchmarkHashEncoders(b *testing.B) { + for _, size := range []int{100, 300, 600, 900, 1200, 1500} { + img := tests.GradientImage(size) + for _, e := range hashEncoders { + b.Run(fmt.Sprintf("%s/%dx%d", e.name, size, size), func(b *testing.B) { + benchEncoder(b, e.encode, img) + }) + } + } +} diff --git a/core/artwork/thumbhash/thumbhash_bench_test.go b/core/artwork/thumbhash/thumbhash_bench_test.go index 6d3dfcc32..ba31cfae9 100644 --- a/core/artwork/thumbhash/thumbhash_bench_test.go +++ b/core/artwork/thumbhash/thumbhash_bench_test.go @@ -1,66 +1,36 @@ package thumbhash_test import ( - "fmt" - "image" - "image/color" "testing" "github.com/navidrome/navidrome/core/artwork/thumbhash" + "github.com/navidrome/navidrome/tests" ) -// benchImage builds the same deterministic gradient the blurhash bench uses, as *image.NRGBA — -// the concrete type makeThumbnail hands both encoders in production. -func benchImage(size int) *image.NRGBA { - img := image.NewNRGBA(image.Rect(0, 0, size, size)) +// BenchmarkEncodeVsReference pins the two-pass separable encoder against the naive reference port +// it replaced. referenceEncode is test-only, so this cannot live beside the cross-encoder +// benchmark in core/artwork. +func BenchmarkEncodeVsReference(b *testing.B) { + const size = 100 + img := tests.GradientImage(size) + pix := make([]byte, 0, size*size*4) for y := range size { - for x := range size { - img.SetNRGBA(x, y, color.NRGBA{ - R: uint8(255 * x / size), - G: uint8(255 * y / size), - B: uint8((x + y) * 255 / (2 * size)), - A: 255, - }) - } + pix = append(pix, img.Pix[y*img.Stride:y*img.Stride+size*4]...) } - 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) - } + b.Run("shipped", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + 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) - } + b.Run("reference", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + _ = referenceEncode(size, size, pix) + } + }) } diff --git a/tests/images.go b/tests/images.go new file mode 100644 index 000000000..43fce969e --- /dev/null +++ b/tests/images.go @@ -0,0 +1,23 @@ +package tests + +import ( + "image" + "image/color" +) + +// GradientImage builds a deterministic square gradient, so benchmark runs are comparable across +// revisions. NRGBA is the type the artwork pipeline's makeThumbnail hands its hash encoders. +func GradientImage(size int) *image.NRGBA { + img := image.NewNRGBA(image.Rect(0, 0, size, size)) + for y := range size { + for x := range size { + img.SetNRGBA(x, y, color.NRGBA{ + R: uint8(255 * x / size), + G: uint8(255 * y / size), + B: uint8((x + y) * 255 / (2 * size)), + A: 255, + }) + } + } + return img +}