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

Improved, configurable buffer pools #87

Merged
merged 2 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
17 changes: 11 additions & 6 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type HTML struct {
Head
Name string
Templates *template.Template

bp GenericBufferPool
}

// JSON built-in renderer.
Expand Down Expand Up @@ -82,20 +84,23 @@ 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
}

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
}

Expand Down
9 changes: 9 additions & 0 deletions genericbufferpool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package render

import "bytes"

// GenericBufferPool abstracts buffer pool implementations
type GenericBufferPool interface {
Get() *bytes.Buffer
Put(*bytes.Buffer)
}
9 changes: 9 additions & 0 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions render_html_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package render

import (
"bytes"
"errors"
"html/template"
"net/http"
Expand Down Expand Up @@ -67,6 +68,38 @@ func TestHTMLBasic(t *testing.T) {
expect(t, res.Body.String(), "<h1>Hello gophers</h1>\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",
Expand Down
62 changes: 62 additions & 0 deletions sizedbufferpool.go
Original file line number Diff line number Diff line change
@@ -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.
}
}