Skip to content

Commit

Permalink
add PR patch from kolesa-team#29
Browse files Browse the repository at this point in the history
  • Loading branch information
lyoneel committed Jul 7, 2024
1 parent b964732 commit 34de940
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 14 deletions.
26 changes: 14 additions & 12 deletions decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package decoder
*/
import "C"
import (
"bytes"
"errors"
"fmt"
"image"
Expand All @@ -47,22 +48,26 @@ type Decoder struct {
sPtr C.size_t
}

// NewDecoder return new decoder instance
func NewDecoder(r io.Reader, options *Options) (d *Decoder, err error) {
var data []byte
if options == nil {
options = &Options{}
}

if options.ImageFactory == nil {
options.ImageFactory = &DefaultImageFactory{}
}

if data, err = io.ReadAll(r); err != nil {
buf := bytes.NewBuffer(options.Buffer)

if _, err = io.Copy(buf, r); err != nil {
return nil, err
}

if len(data) == 0 {
if len(buf.Bytes()) == 0 {
return nil, errors.New("data is empty")
}

if options == nil {
options = &Options{}
}
d = &Decoder{data: data, options: options}
d = &Decoder{data: buf.Bytes(), options: options}

if d.config, err = d.options.GetConfig(); err != nil {
return nil, err
Expand All @@ -87,10 +92,7 @@ func (d *Decoder) Decode() (image.Image, error) {
d.config.output.colorspace = C.MODE_RGBA
d.config.output.is_external_memory = 1

img := image.NewNRGBA(image.Rectangle{Max: image.Point{
X: int(d.config.output.width),
Y: int(d.config.output.height),
}})
img := d.options.ImageFactory.Get(int(d.config.output.width), int(d.config.output.height))

buff := (*C.WebPRGBABuffer)(unsafe.Pointer(&d.config.output.u[0]))
buff.stride = C.int(img.Stride)
Expand Down
60 changes: 60 additions & 0 deletions decoder/decoder_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package decoder

import (
"bytes"
"image"
"os"
"testing"
)

func loadImage(b *testing.B) []byte {
filename := "../test_data/images/100x150_lossless.webp"
data, err := os.ReadFile(filename)
if err != nil {
b.Fatal(err)
}
return data
}

func BenchmarkDecodePooled(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()

data := loadImage(b)

imagePool := NewImagePool()
bufferPool := NewBufferPool()

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := bufferPool.Get()
decoder, err := NewDecoder(bytes.NewReader(data), &Options{ImageFactory: imagePool, Buffer: buf})
img, err := decoder.Decode()
if err != nil {
b.Fatal(err)
}

// put everything back
imagePool.Put(img.(*image.NRGBA))
bufferPool.Put(buf)
}
})
}

func BenchmarkDecodeUnPooled(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()

data := loadImage(b)

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
decoder, err := NewDecoder(bytes.NewReader(data), &Options{})
img, err := decoder.Decode()
if err != nil {
b.Fatal(err)
}
_ = img
}
})
}
5 changes: 3 additions & 2 deletions decoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
package decoder

import (
"github.com/lyoneel/go-webp/utils"
"github.com/stretchr/testify/require"
"image"
"os"
"testing"

"github.com/lyoneel/go-webp/utils"
"github.com/stretchr/testify/require"
)

func TestNewDecoder(t *testing.T) {
Expand Down
20 changes: 20 additions & 0 deletions decoder/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ type Options struct {
Flip bool
DitheringStrength int
AlphaDitheringStrength int

// These two are optimizations that require a little extra work on the caller side.

// if nil, DefaultImageFactory will be used. If non-nil, decode will return an image that must be put back into the pool
// when you're done with it
ImageFactory ImageFactory
// if nil, a default buffer will be used. If non-nil, decode will use this buffer to store data from the reader.
// The idea is that this buffer be reused, so either pass this back in next time you call decode, or put it back into
// a pool when you're done with it.
Buffer []byte
}

// GetConfig build WebPDecoderConfig for libwebp
Expand Down Expand Up @@ -89,3 +99,13 @@ func (o *Options) GetConfig() (*C.WebPDecoderConfig, error) {

return &config, nil
}

type ImageFactory interface {
Get(width, height int) *image.NRGBA
}

type DefaultImageFactory struct{}

func (d *DefaultImageFactory) Get(width, height int) *image.NRGBA {
return image.NewNRGBA(image.Rect(0, 0, width, height))
}
75 changes: 75 additions & 0 deletions decoder/pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package decoder

import (
"image"
"sync"
"sync/atomic"
)

type ImagePool struct {
poolMap map[int]*sync.Pool
lock *sync.Mutex
Count int64
}

func NewImagePool() *ImagePool {
return &ImagePool{
poolMap: make(map[int]*sync.Pool),
lock: &sync.Mutex{},
}
}

func (n *ImagePool) Get(width, height int) *image.NRGBA {
dimPool := n.getPool(width, height)

img := dimPool.Get().(*image.NRGBA)
img.Rect.Max.X = width
img.Rect.Max.Y = height
return img
}

func (n *ImagePool) getPool(width int, height int) *sync.Pool {
dim := width * height

n.lock.Lock()
dimPool, ok := n.poolMap[dim]
if !ok {
atomic.AddInt64(&n.Count, 1)
dimPool = &sync.Pool{
New: func() interface{} {
return image.NewNRGBA(image.Rect(0, 0, width, height))
},
}
n.poolMap[dim] = dimPool
}
n.lock.Unlock()
return dimPool
}

func (n *ImagePool) Put(img *image.NRGBA) {
dimPool := n.getPool(img.Rect.Dx(), img.Rect.Dy())
dimPool.Put(img)
}

type BufferPool struct {
pool *sync.Pool // pointer because noCopy
}

func NewBufferPool() *BufferPool {
return &BufferPool{
pool: &sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024)
},
},
}
}

func (b *BufferPool) Get() []byte {
return b.pool.Get().([]byte)
}

func (b *BufferPool) Put(buf []byte) {
buf = buf[:0]
b.pool.Put(buf)
}

0 comments on commit 34de940

Please sign in to comment.