diff --git a/buffer.go b/buffer.go index cdc92ff..804d775 100644 --- a/buffer.go +++ b/buffer.go @@ -2,9 +2,6 @@ package render import "bytes" -// bufPool represents a reusable buffer pool for executing templates into. -var bufPool *BufferPool - // BufferPool implements a pool of bytes.Buffers in the form of a bounded channel. // Pulled from the github.com/oxtoacart/bpool package (Apache licensed). type BufferPool struct { @@ -39,8 +36,3 @@ func (bp *BufferPool) Put(b *bytes.Buffer) { default: // Discard the buffer if the pool is full. } } - -// Initialize buffer pool for writing templates into. -func init() { - bufPool = NewBufferPool(64) -} diff --git a/engine.go b/engine.go index cdf1a1b..283a377 100644 --- a/engine.go +++ b/engine.go @@ -30,6 +30,8 @@ type HTML struct { Head Name string Templates *template.Template + + bp GenericBufferPool } // JSON built-in renderer. @@ -82,9 +84,14 @@ func (d Data) Render(w io.Writer, v interface{}) error { // Render a HTML response. func (h HTML) Render(w io.Writer, binding interface{}) error { - // Retrieve a buffer from the pool to write to. - out := bufPool.Get() - err := h.Templates.ExecuteTemplate(out, h.Name, binding) + var buf *bytes.Buffer + if h.bp != nil { + // If we have a bufferpool, allocate from it + buf = h.bp.Get() + defer h.bp.Put(buf) + } + + err := h.Templates.ExecuteTemplate(buf, h.Name, binding) if err != nil { return err } @@ -92,10 +99,8 @@ func (h HTML) Render(w io.Writer, binding interface{}) error { if hw, ok := w.(http.ResponseWriter); ok { h.Head.Write(hw) } - out.WriteTo(w) + buf.WriteTo(w) - // Return the buffer to the pool. - bufPool.Put(out) return nil } diff --git a/genericbufferpool.go b/genericbufferpool.go new file mode 100644 index 0000000..7e8d342 --- /dev/null +++ b/genericbufferpool.go @@ -0,0 +1,9 @@ +package render + +import "bytes" + +// GenericBufferPool abstracts buffer pool implementations +type GenericBufferPool interface { + Get() *bytes.Buffer + Put(*bytes.Buffer) +} diff --git a/render.go b/render.go index 3259f62..4cd11cd 100644 --- a/render.go +++ b/render.go @@ -103,6 +103,10 @@ type Options struct { // Enables using partials without the current filename suffix which allows use of the same template in multiple files. e.g {{ partial "carosuel" }} inside the home template will match carosel-home or carosel. // ***NOTE*** - This option should be named RenderPartialsWithoutSuffix as that is what it does. "Prefix" is a typo. Maintaining the existing name for backwards compatibility. RenderPartialsWithoutPrefix bool + + // BufferPool to use when rendering HTML templates. If none is supplied + // defaults to SizedBufferPool of size 32 with 512KiB buffers. + BufferPool GenericBufferPool } // HTMLOptions is a struct for overriding some rendering Options for specific HTML call. @@ -176,6 +180,10 @@ func (r *Render) prepareOptions() { if len(r.opt.XMLContentType) == 0 { r.opt.XMLContentType = ContentXML } + if r.opt.BufferPool == nil { + // 32 buffers of size 512KiB each + r.opt.BufferPool = NewSizedBufferPool(32, 1<<19) + } } func (r *Render) compileTemplates() { @@ -410,6 +418,7 @@ func (r *Render) HTML(w io.Writer, status int, name string, binding interface{}, Head: head, Name: name, Templates: r.templates, + bp: r.opt.BufferPool, } return r.Render(w, h, binding) diff --git a/render_html_test.go b/render_html_test.go index 0a0c0c6..f1c4c6b 100644 --- a/render_html_test.go +++ b/render_html_test.go @@ -1,6 +1,7 @@ package render import ( + "bytes" "errors" "html/template" "net/http" @@ -67,6 +68,38 @@ func TestHTMLBasic(t *testing.T) { expect(t, res.Body.String(), "

Hello gophers

\n") } +func BenchmarkBigHTMLBuffers(b *testing.B) { + b.ReportAllocs() + + render := New(Options{ + Directory: "fixtures/basic", + }) + + var buf = new(bytes.Buffer) + for i := 0; i < b.N; i++ { + render.HTML(buf, http.StatusOK, "hello", "gophers") + buf.Reset() + } +} + +func BenchmarkSmallHTMLBuffers(b *testing.B) { + b.ReportAllocs() + + render := New(Options{ + Directory: "fixtures/basic", + + // Tiny 8 bytes buffers -> should lead to allocations + // on every template render + BufferPool: NewSizedBufferPool(32, 8), + }) + + var buf = new(bytes.Buffer) + for i := 0; i < b.N; i++ { + render.HTML(buf, http.StatusOK, "hello", "gophers") + buf.Reset() + } +} + func TestHTMLXHTML(t *testing.T) { render := New(Options{ Directory: "fixtures/basic", diff --git a/sizedbufferpool.go b/sizedbufferpool.go new file mode 100644 index 0000000..901c983 --- /dev/null +++ b/sizedbufferpool.go @@ -0,0 +1,62 @@ +package render + +import ( + "bytes" +) + +// Pulled from the github.com/oxtoacart/bpool package (Apache licensed). + +// SizedBufferPool implements a pool of bytes.Buffers in the form of a bounded +// channel. Buffers are pre-allocated to the requested size. +type SizedBufferPool struct { + c chan *bytes.Buffer + a int +} + +// NewSizedBufferPool creates a new BufferPool bounded to the given size. +// size defines the number of buffers to be retained in the pool and alloc sets +// the initial capacity of new buffers to minimize calls to make(). +// +// The value of alloc should seek to provide a buffer that is representative of +// most data written to the the buffer (i.e. 95th percentile) without being +// overly large (which will increase static memory consumption). You may wish to +// track the capacity of your last N buffers (i.e. using an []int) prior to +// returning them to the pool as input into calculating a suitable alloc value. +func NewSizedBufferPool(size int, alloc int) (bp *SizedBufferPool) { + return &SizedBufferPool{ + c: make(chan *bytes.Buffer, size), + a: alloc, + } +} + +// Get gets a Buffer from the SizedBufferPool, or creates a new one if none are +// available in the pool. Buffers have a pre-allocated capacity. +func (bp *SizedBufferPool) Get() (b *bytes.Buffer) { + select { + case b = <-bp.c: + // reuse existing buffer + default: + // create new buffer + b = bytes.NewBuffer(make([]byte, 0, bp.a)) + } + return +} + +// Put returns the given Buffer to the SizedBufferPool. +func (bp *SizedBufferPool) Put(b *bytes.Buffer) { + b.Reset() + + // Release buffers over our maximum capacity and re-create a pre-sized + // buffer to replace it. + // Note that the cap(b.Bytes()) provides the capacity from the read off-set + // only, but as we've called b.Reset() the full capacity of the underlying + // byte slice is returned. + if cap(b.Bytes()) > bp.a { + b = bytes.NewBuffer(make([]byte, 0, bp.a)) + } + + select { + case bp.c <- b: + default: // Discard the buffer if the pool is full. + } +}