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

Issue #5499 - Reduce buffer allocations and copying from ByteAccumulator #5574

Merged
merged 23 commits into from Dec 15, 2020

Conversation

lachlan-roberts
Copy link
Contributor

@lachlan-roberts lachlan-roberts commented Nov 5, 2020

Issue #5499

  • Add utility classes to jetty-io, ByteBufferOutputStream2 and ByteBufferAccumulator.
  • Implement ByteAccumulator with the ByteBufferAccumulator and make it AutoCloseable.
  • Add getBuffer method to ByteAccumulator, allowing you to write directly to the internal buffer instead of copying.

leonchen83 and others added 5 commits October 31, 2020 00:08
this PR let the ByteAccumulator recyclable. after invoke ByteAccumulator.transferTo method
we can invoke ByteAccumulator.recycle method to reuse byte[] via ByteAccumulator.newByteBuffer method

Signed-off-by: Baoyi Chen <chen.bao.yi@qq.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
@lachlan-roberts
Copy link
Contributor Author

@gregw @leonchen83 This branch tries to combine the work from PRs #5520 and #5536. I haven't written much testing or javadoc yet, just want to make sure we agree on the API before continuing.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Copy link
Contributor

@gregw gregw left a comment

Choose a reason for hiding this comment

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

Looking good. I've added a bunch of comments but nothing way out of order... just suggestions really.

{
private final ByteBufferAccumulator _accumulator;
private final ByteBufferPool _bufferPool;
private ByteBuffer _combinedByteBuffer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there really a need to remember the combined ByteBuffer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to remember it so that we release it when close() is called.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is wrong for this class to manage the releasing of the combined buffer. Isn't that mostly passed along a chain and will be released when processing is complete. I still lean towards takeBuffer semantics.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this behaviour can be implemented in ByteBufferAccumulator with 2 methods:

  • toBuffer will create a single buffer of all the contents, it will release any buffers in the list and the list will end up containing just the new single buffer. Any subsequent calls to toBuffer will get the same buffer back. Any calls to add more content will just allocate another buffer in the list. Buffer will be release with close
  • takeBuffer will create a single buffer of all the contents, it will release any buffers in the list and clear it. It is the callers responsibility to release the returned buffer.

{
private final ByteBufferAccumulator _accumulator;
private final ByteBufferPool _bufferPool;
private ByteBuffer _combinedByteBuffer;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to remember it so that we release it when close() is called.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
@gregw
Copy link
Contributor

gregw commented Nov 6, 2020

@sbordet can you tune into this PR whilst in draft form? It really has the feel of one of those that I push @lachlan-roberts all the way in one direction, only for you to come in during review with a totally different view point :)

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Copy link
Contributor

@gregw gregw left a comment

Choose a reason for hiding this comment

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

closer!

Comment on lines 80 to 89
public ByteBuffer takeByteBuffer()
{
int length = getLength();
ByteBuffer combinedBuffer = _bufferPool.acquire(length, false);
for (ByteBuffer buffer : _buffers)
{
combinedBuffer.put(buffer);
}
return combinedBuffer;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public ByteBuffer takeByteBuffer()
{
int length = getLength();
ByteBuffer combinedBuffer = _bufferPool.acquire(length, false);
for (ByteBuffer buffer : _buffers)
{
combinedBuffer.put(buffer);
}
return combinedBuffer;
}
public ByteBuffer takeByteBuffer()
{
int length = getLength();
switch(length)
{
case 0:
return null; // TODO or empty buffer from pool?
case 1:
return _buffers.remove(0);
default:
  ByteBuffer combinedBuffer = _bufferPool.acquire(length, false);
  for (ByteBuffer buffer : _buffers)
  {
  combinedBuffer.put(buffer);
  _bufferPool.release(buffer);
  }
  _buffers.clear();
  return combinedBuffer;
 }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't really like the idea of returning null from here, it will introduce null checks everywhere this is used.
How can we get empty buffer from pool, will _bufferPool.aquire(0, false) work, because that's what we are currently doing for 0 length.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also makes it so you can only call takeBuffer() once, but you can call toBuffer() multiple times and get the same buffer.

Why wouldn't we just release these buffers on close() like we would do in other cases?

Copy link
Contributor

Choose a reason for hiding this comment

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

Because the idea of takeBuffer is that you take the buffer and all it's data, hence it can't be called again.
The intent is that once you call takeBuffer you will pass the resulting buffer along some further chain of events and then only release it once it is free.
freeing the buffer on close makes no sense for most uses, as we want to have a accumulator that lives as long as the connection, during which time it may create many buffers that are taken and passed on.

Happy not to return null.

Comment on lines 97 to 98
_buffers.forEach(_bufferPool::release);
_buffers.clear();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
_buffers.forEach(_bufferPool::release);
_buffers.clear();

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
@lachlan-roberts lachlan-roberts marked this pull request as ready for review November 13, 2020 04:44
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Copy link
Contributor

@gregw gregw left a comment

Choose a reason for hiding this comment

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

one final niggle...

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
@lachlan-roberts
Copy link
Contributor Author

@sbordet you wanted me to keep these ByteBufferAccumulator classes as internal websocket classes, but not sure how to do this as I can't make them package private as they are needed in multiple packages.

I can also see some potential to use them in other places, for example ByteBufferOutputStream2 could easily be used in MultiPart if we could get the BufferPool there. Should I put them as public websocket classes or can I keep them in jetty-io?

Copy link
Contributor

@lorban lorban left a comment

Choose a reason for hiding this comment

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

This looks alright to me, but I can't help thinking that this could have been a lot simpler and more efficient if we didn't directly use ByteBuffers but some wrapper around them.

* This will add up the remaining of each buffer in the accumulator.
* @return the total length of the content in the accumulator.
*/
public int getLength()
Copy link
Contributor

Choose a reason for hiding this comment

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

This length accumulation could potentially overflow Integer.MAX_VALUE. We should handle this by either returning a long from here, or detecting the integer overflow and throwing an exception.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the accumulation needs to eventually fit into a single buffer, the int should do with overflow detection elsewhere.
An ultimate buffer abstraction could allow for chains of buffers that can be gather written for a long length, but not this abstraction

@gregw
Copy link
Contributor

gregw commented Dec 1, 2020

I'll be +1 once @lorban's overflow concerns are addressed

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
BufferUtil.flipToFlush(buffer, position);

accumulator.writeTo(buffer);
close();
Copy link
Contributor

Choose a reason for hiding this comment

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

close() method invoked by DeflateFrameExtension.java#L64
and PerMessageDeflateExtension.java#L81
transferTo method no need add extra close method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this to preserve the behaviour of this class with the previous usage.
Before these changes it was not AutoCloseable, so it would have been used like this with no closing:

ByteAccumulator acc = new ByteAccumulator();
acc.copyChunk(something);
...
acc.transferTo(combinedBuffer);

So in this way we can be more lenient on the usage of ByteAccumulator by supporting the deprecated way of using it, and now we have the replacement ByteBufferAccumulator which is more strict and must be used with an explicit call to close().

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.

None yet

4 participants