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

Remove buffer pool for HTML render #86

Closed
wants to merge 1 commit into from
Closed

Remove buffer pool for HTML render #86

wants to merge 1 commit into from

Conversation

nsmith5
Copy link
Contributor

@nsmith5 nsmith5 commented Apr 13, 2021

http.ResponseWriter (the most common io.Writer passed to Render) is already buffered and this buffer pool simply lead to memory bloat under certain conditions. This work fixes #85.

As an example of the memory savings here is memory usage on my Gitea instance before and after this change:

Before
gitea-ram-usage

After
gitea-no-bloat

http.ResponseWriter is already buffered and this buffer pool simply
lead to memory bloat under certain conditions
Copy link
Contributor

@lunny lunny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer should be the io.Writer's responsibility. As a common usage, bufio.NewWriter(w), but this also should be invoked out of Render.

And in fact, the whole buffer.go could be removed.

@nsmith5
Copy link
Contributor Author

nsmith5 commented Apr 13, 2021

Agreed. I left the buffer.go because the bufferpool's implementation is exported so it would be break the library API to remove, but could remove as well if its desired.

Copy link
Contributor

@zeripath zeripath left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it should be up to the end-user if they want to buffer.

// Retrieve a buffer from the pool to write to.
out := bufPool.Get()
err := h.Templates.ExecuteTemplate(out, h.Name, binding)
err := h.Templates.ExecuteTemplate(w, h.Name, binding)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change prevents the HTTP header from being set. If w is a http.ResponseWriter the first call to Write inside h.Templates.ExecuteTemplate(w, h.Name, binding) will set and write the header data. So h.Head.Write(hw) does nothing afterwards.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's up to the end user surely?

This function is not accepting a http.ResponseWriter it's accepting an io.Writer - the semantics of an http.ResponseWriter requiring WriteHeader() before Write(...) are not expressed in this and assuming that every io.Writer is or could be an http.ResponseWriter is a bit different.

This buffer is unbounded and worse the unbounded buffer is then kept around - potentially to be created 64 times


If you want to buffer before sending the status you should buffer. However, there are essentially two options:

  • Size limit the results from this HTML function when buffering
  • Only buffer the first n bytes, send the header if the first n bytes have succeeded then the buffer contents and reset buffer and so on. If an error occurs you then need to handle dangling markup.

Buffering needs to be size limited especially if you're rendering uncontrolled content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh I see. The buffer pool isn't about write buffering, its about setting status after templating. I guess this makes the options more tricky. The buffer pool leads to growing allocations, but if a new bytes.Buffer is allocated for each template call the GC cost might get spendy...

Not sure the best option here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance I was excited as this would be a great memory saver! But I don't think we can remove the buffer pool without breaking existing functionality.

We could add a new option that allows the Render func to skip the buffer and go strait into the provided io.Writer... something users can opt into? In the future we can roll a new version with breaking changes and enable the skip buffer option by default.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this buffer pooling is kept I can think of a couple improvements

  • Use a sized buffer pool like this one to keep memory consumption fixed and to GC buffers that exceed the size limit after use
  • Allow the bufferpool to be configurable so that the hidden default buffer pool can be modified (i.e if I know I'm most of my templates are under 100KiB, then I'd like to use a smaller max buffer size. If I know I've got a large system load and can increase the size of the pool to limit gc events under high concurrency)

More broadly though, I think this library might be improved by seperating these HTTP concerns from templating concerns. This might sacrifice the nice one-liners in the examples, but the behaviour would likely be less surprising.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the improvements! Do you want to try implementing the changes in this PR?

I also agree that separating the concerns would improve the library and should be done. I'm going to start a "Version 2" ticket where we can document ideas and goals for the next version. I've been wanting to clean this package up for a while now... some of work-around code for blocks needs to go 🥾

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet happy to implement those :D I'll probably just open up a new PR if that works for you. I think we can close this one

@nsmith5
Copy link
Contributor Author

nsmith5 commented Apr 15, 2021

This change breaks HTTP status updates. Going to implement a better and more configurable buffer pool instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BufferPool leads to wasted memory
5 participants