Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ImageFactory and Buffer to Options #29

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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/kolesa-team/go-webp/utils"
"github.com/stretchr/testify/require"
"image"
"os"
"testing"

"github.com/kolesa-team/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)
}