mirror of https://github.com/golang/go.git
image/gif: avoid setting defers in the decode loop
decoder.decode() was defering close of lzw.decoders created for each frame in a loop, thus increasing heap usage (referenced object + defered function) until decode() returns. Memory increased proportionally to the number of frames. Fix this by moving the sImageDescriptor case block into its own method. Fixes #22237 Change-Id: I819617ea7e539e13c04bc11112f339645391ddb9 Reviewed-on: https://go-review.googlesource.com/70370 Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com>
This commit is contained in:
parent
0c5b00d0cd
commit
54fa10a98e
|
|
@ -244,109 +244,9 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
case sImageDescriptor:
|
case sImageDescriptor:
|
||||||
m, err := d.newImageFromDescriptor()
|
if err = d.readImageDescriptor(keepAllFrames); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
useLocalColorTable := d.imageFields&fColorTable != 0
|
|
||||||
if useLocalColorTable {
|
|
||||||
m.Palette, err = d.readColorTable(d.imageFields)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if d.globalColorTable == nil {
|
|
||||||
return errors.New("gif: no color table")
|
|
||||||
}
|
|
||||||
m.Palette = d.globalColorTable
|
|
||||||
}
|
|
||||||
if d.hasTransparentIndex {
|
|
||||||
if !useLocalColorTable {
|
|
||||||
// Clone the global color table.
|
|
||||||
m.Palette = append(color.Palette(nil), d.globalColorTable...)
|
|
||||||
}
|
|
||||||
if ti := int(d.transparentIndex); ti < len(m.Palette) {
|
|
||||||
m.Palette[ti] = color.RGBA{}
|
|
||||||
} else {
|
|
||||||
// The transparentIndex is out of range, which is an error
|
|
||||||
// according to the spec, but Firefox and Google Chrome
|
|
||||||
// seem OK with this, so we enlarge the palette with
|
|
||||||
// transparent colors. See golang.org/issue/15059.
|
|
||||||
p := make(color.Palette, ti+1)
|
|
||||||
copy(p, m.Palette)
|
|
||||||
for i := len(m.Palette); i < len(p); i++ {
|
|
||||||
p[i] = color.RGBA{}
|
|
||||||
}
|
|
||||||
m.Palette = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
litWidth, err := readByte(d.r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("gif: reading image data: %v", err)
|
|
||||||
}
|
|
||||||
if litWidth < 2 || litWidth > 8 {
|
|
||||||
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
|
|
||||||
}
|
|
||||||
// A wonderfully Go-like piece of magic.
|
|
||||||
br := &blockReader{d: d}
|
|
||||||
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
|
|
||||||
defer lzwr.Close()
|
|
||||||
if err = readFull(lzwr, m.Pix); err != nil {
|
|
||||||
if err != io.ErrUnexpectedEOF {
|
|
||||||
return fmt.Errorf("gif: reading image data: %v", err)
|
|
||||||
}
|
|
||||||
return errNotEnough
|
|
||||||
}
|
|
||||||
// In theory, both lzwr and br should be exhausted. Reading from them
|
|
||||||
// should yield (0, io.EOF).
|
|
||||||
//
|
|
||||||
// The spec (Appendix F - Compression), says that "An End of
|
|
||||||
// Information code... must be the last code output by the encoder
|
|
||||||
// for an image". In practice, though, giflib (a widely used C
|
|
||||||
// library) does not enforce this, so we also accept lzwr returning
|
|
||||||
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
|
|
||||||
// before the LZW decoder saw an explicit end code), provided that
|
|
||||||
// the io.ReadFull call above successfully read len(m.Pix) bytes.
|
|
||||||
// See https://golang.org/issue/9856 for an example GIF.
|
|
||||||
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("gif: reading image data: %v", err)
|
|
||||||
}
|
|
||||||
return errTooMuch
|
|
||||||
}
|
|
||||||
|
|
||||||
// In practice, some GIFs have an extra byte in the data sub-block
|
|
||||||
// stream, which we ignore. See https://golang.org/issue/16146.
|
|
||||||
if err := br.close(); err == errTooMuch {
|
|
||||||
return errTooMuch
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("gif: reading image data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the color indexes are inside the palette.
|
|
||||||
if len(m.Palette) < 256 {
|
|
||||||
for _, pixel := range m.Pix {
|
|
||||||
if int(pixel) >= len(m.Palette) {
|
|
||||||
return errBadPixel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo the interlacing if necessary.
|
|
||||||
if d.imageFields&fInterlace != 0 {
|
|
||||||
uninterlace(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
if keepAllFrames || len(d.image) == 0 {
|
|
||||||
d.image = append(d.image, m)
|
|
||||||
d.delay = append(d.delay, d.delayTime)
|
|
||||||
d.disposal = append(d.disposal, d.disposalMethod)
|
|
||||||
}
|
|
||||||
// The GIF89a spec, Section 23 (Graphic Control Extension) says:
|
|
||||||
// "The scope of this extension is the first graphic rendering block
|
|
||||||
// to follow." We therefore reset the GCE fields to zero.
|
|
||||||
d.delayTime = 0
|
|
||||||
d.hasTransparentIndex = false
|
|
||||||
|
|
||||||
case sTrailer:
|
case sTrailer:
|
||||||
if len(d.image) == 0 {
|
if len(d.image) == 0 {
|
||||||
|
|
@ -470,6 +370,113 @@ func (d *decoder) readGraphicControl() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *decoder) readImageDescriptor(keepAllFrames bool) error {
|
||||||
|
m, err := d.newImageFromDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
useLocalColorTable := d.imageFields&fColorTable != 0
|
||||||
|
if useLocalColorTable {
|
||||||
|
m.Palette, err = d.readColorTable(d.imageFields)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if d.globalColorTable == nil {
|
||||||
|
return errors.New("gif: no color table")
|
||||||
|
}
|
||||||
|
m.Palette = d.globalColorTable
|
||||||
|
}
|
||||||
|
if d.hasTransparentIndex {
|
||||||
|
if !useLocalColorTable {
|
||||||
|
// Clone the global color table.
|
||||||
|
m.Palette = append(color.Palette(nil), d.globalColorTable...)
|
||||||
|
}
|
||||||
|
if ti := int(d.transparentIndex); ti < len(m.Palette) {
|
||||||
|
m.Palette[ti] = color.RGBA{}
|
||||||
|
} else {
|
||||||
|
// The transparentIndex is out of range, which is an error
|
||||||
|
// according to the spec, but Firefox and Google Chrome
|
||||||
|
// seem OK with this, so we enlarge the palette with
|
||||||
|
// transparent colors. See golang.org/issue/15059.
|
||||||
|
p := make(color.Palette, ti+1)
|
||||||
|
copy(p, m.Palette)
|
||||||
|
for i := len(m.Palette); i < len(p); i++ {
|
||||||
|
p[i] = color.RGBA{}
|
||||||
|
}
|
||||||
|
m.Palette = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
litWidth, err := readByte(d.r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gif: reading image data: %v", err)
|
||||||
|
}
|
||||||
|
if litWidth < 2 || litWidth > 8 {
|
||||||
|
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
|
||||||
|
}
|
||||||
|
// A wonderfully Go-like piece of magic.
|
||||||
|
br := &blockReader{d: d}
|
||||||
|
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
|
||||||
|
defer lzwr.Close()
|
||||||
|
if err = readFull(lzwr, m.Pix); err != nil {
|
||||||
|
if err != io.ErrUnexpectedEOF {
|
||||||
|
return fmt.Errorf("gif: reading image data: %v", err)
|
||||||
|
}
|
||||||
|
return errNotEnough
|
||||||
|
}
|
||||||
|
// In theory, both lzwr and br should be exhausted. Reading from them
|
||||||
|
// should yield (0, io.EOF).
|
||||||
|
//
|
||||||
|
// The spec (Appendix F - Compression), says that "An End of
|
||||||
|
// Information code... must be the last code output by the encoder
|
||||||
|
// for an image". In practice, though, giflib (a widely used C
|
||||||
|
// library) does not enforce this, so we also accept lzwr returning
|
||||||
|
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
|
||||||
|
// before the LZW decoder saw an explicit end code), provided that
|
||||||
|
// the io.ReadFull call above successfully read len(m.Pix) bytes.
|
||||||
|
// See https://golang.org/issue/9856 for an example GIF.
|
||||||
|
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gif: reading image data: %v", err)
|
||||||
|
}
|
||||||
|
return errTooMuch
|
||||||
|
}
|
||||||
|
|
||||||
|
// In practice, some GIFs have an extra byte in the data sub-block
|
||||||
|
// stream, which we ignore. See https://golang.org/issue/16146.
|
||||||
|
if err := br.close(); err == errTooMuch {
|
||||||
|
return errTooMuch
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("gif: reading image data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the color indexes are inside the palette.
|
||||||
|
if len(m.Palette) < 256 {
|
||||||
|
for _, pixel := range m.Pix {
|
||||||
|
if int(pixel) >= len(m.Palette) {
|
||||||
|
return errBadPixel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo the interlacing if necessary.
|
||||||
|
if d.imageFields&fInterlace != 0 {
|
||||||
|
uninterlace(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keepAllFrames || len(d.image) == 0 {
|
||||||
|
d.image = append(d.image, m)
|
||||||
|
d.delay = append(d.delay, d.delayTime)
|
||||||
|
d.disposal = append(d.disposal, d.disposalMethod)
|
||||||
|
}
|
||||||
|
// The GIF89a spec, Section 23 (Graphic Control Extension) says:
|
||||||
|
// "The scope of this extension is the first graphic rendering block
|
||||||
|
// to follow." We therefore reset the GCE fields to zero.
|
||||||
|
d.delayTime = 0
|
||||||
|
d.hasTransparentIndex = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) {
|
func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) {
|
||||||
if err := readFull(d.r, d.tmp[:9]); err != nil {
|
if err := readFull(d.r, d.tmp[:9]); err != nil {
|
||||||
return nil, fmt.Errorf("gif: can't read image descriptor: %s", err)
|
return nil, fmt.Errorf("gif: can't read image descriptor: %s", err)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ import (
|
||||||
"compress/lzw"
|
"compress/lzw"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"image/color/palette"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -351,6 +354,36 @@ func TestUnexpectedEOF(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See golang.org/issue/22237
|
||||||
|
func TestDecodeMemoryConsumption(t *testing.T) {
|
||||||
|
const frames = 3000
|
||||||
|
img := image.NewPaletted(image.Rectangle{Max: image.Point{1, 1}}, palette.WebSafe)
|
||||||
|
hugeGIF := &GIF{
|
||||||
|
Image: make([]*image.Paletted, frames),
|
||||||
|
Delay: make([]int, frames),
|
||||||
|
Disposal: make([]byte, frames),
|
||||||
|
}
|
||||||
|
for i := 0; i < frames; i++ {
|
||||||
|
hugeGIF.Image[i] = img
|
||||||
|
hugeGIF.Delay[i] = 60
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := EncodeAll(buf, hugeGIF); err != nil {
|
||||||
|
t.Fatal("EncodeAll:", err)
|
||||||
|
}
|
||||||
|
s0, s1 := new(runtime.MemStats), new(runtime.MemStats)
|
||||||
|
runtime.GC()
|
||||||
|
defer debug.SetGCPercent(debug.SetGCPercent(5))
|
||||||
|
runtime.ReadMemStats(s0)
|
||||||
|
if _, err := Decode(buf); err != nil {
|
||||||
|
t.Fatal("Decode:", err)
|
||||||
|
}
|
||||||
|
runtime.ReadMemStats(s1)
|
||||||
|
if heapDiff := int64(s1.HeapAlloc - s0.HeapAlloc); heapDiff > 30<<20 {
|
||||||
|
t.Fatalf("Decode of %d frames increased heap by %dMB", frames, heapDiff>>20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkDecode(b *testing.B) {
|
func BenchmarkDecode(b *testing.B) {
|
||||||
data, err := ioutil.ReadFile("../testdata/video-001.gif")
|
data, err := ioutil.ReadFile("../testdata/video-001.gif")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue