-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
POC: outbound protobuf zero-copy take 2 #7369
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* | ||
* Copyright 2020 The gRPC Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.grpc; | ||
|
||
import java.io.IOException; | ||
import java.nio.BufferOverflowException; | ||
import java.nio.ByteBuffer; | ||
|
||
/** | ||
* Extension to an {@link java.io.InputStream} or alike by adding a method that | ||
* allows transferring directly to an underlying {@link ByteBuffer} | ||
*/ | ||
public interface BufferDrainable { | ||
|
||
/** | ||
* Transfers all content to a target {@link ByteBuffer} | ||
* | ||
* @param dest the destination buffer to receive the bytes. | ||
* @throws BufferOverflowException if destination buffer has insufficient remaining bytes | ||
*/ | ||
int drainTo(ByteBuffer dest) throws IOException; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ | |
|
||
package io.grpc.internal; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
|
||
/** | ||
* An interface for a byte buffer that can only be written to. | ||
* {@link WritableBuffer}s are a generic way to transfer bytes to | ||
|
@@ -38,7 +41,14 @@ public interface WritableBuffer { | |
/** | ||
* Appends a single byte to the buffer. This is slow so don't call it. | ||
*/ | ||
void write(byte b); | ||
void write(int b); | ||
|
||
/** | ||
* Write entire {@link InputStream} to the buffer. | ||
* | ||
* @return number of bytes written | ||
*/ | ||
int write(InputStream stream) throws IOException; | ||
|
||
/** | ||
* Returns the number of bytes one can write to the buffer. | ||
|
@@ -54,5 +64,5 @@ public interface WritableBuffer { | |
* Releases the buffer, indicating to the {@link WritableBufferAllocator} that | ||
* this buffer is no longer used and its resources can be reused. | ||
*/ | ||
void release(); | ||
void close(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes in this interface and MessageFramer aren't directly related to the goal of this PR. Also, I'd vote for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean changes like s/release/close/g, casting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You seem to be arguing for both sides of this in the same paragraph? :)
I just explained these in the preceding comment, they are in fact linked to this PR.
Sure I can remove this if you prefer |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,13 +16,20 @@ | |
|
||
package io.grpc.netty; | ||
|
||
import io.grpc.BufferDrainable; | ||
import io.grpc.Drainable; | ||
import io.grpc.internal.WritableBuffer; | ||
import io.netty.buffer.ByteBuf; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.nio.BufferOverflowException; | ||
import java.nio.ByteBuffer; | ||
|
||
/** | ||
* The {@link WritableBuffer} used by the Netty transport. | ||
*/ | ||
class NettyWritableBuffer implements WritableBuffer { | ||
class NettyWritableBuffer extends OutputStream implements WritableBuffer { | ||
|
||
private final ByteBuf bytebuf; | ||
|
||
|
@@ -36,10 +43,36 @@ public void write(byte[] src, int srcIndex, int length) { | |
} | ||
|
||
@Override | ||
public void write(byte b) { | ||
public void write(int b) { | ||
bytebuf.writeByte(b); | ||
} | ||
|
||
@Override | ||
public int write(InputStream stream) throws IOException { | ||
if (!bytebuf.hasArray()) { | ||
if (stream instanceof BufferDrainable && bytebuf.nioBufferCount() == 1) { | ||
ByteBuffer bb = bytebuf.internalNioBuffer(bytebuf.writerIndex(), bytebuf.writableBytes()); | ||
int positionBefore = bb.position(); | ||
((BufferDrainable) stream).drainTo(bb); | ||
int written = bb.position() - positionBefore; | ||
bytebuf.writerIndex(bytebuf.writerIndex() + written); | ||
return written; | ||
// Could potentially also include composite here if stream is known length | ||
// and less than first ByteBuf size. But shouldn't encounter those since | ||
// output buffers come straight from the allocator | ||
} | ||
if (stream instanceof Drainable) { | ||
return ((Drainable) stream).drainTo(this); | ||
} | ||
} | ||
int writable = bytebuf.writableBytes(); | ||
int written = bytebuf.writeBytes(stream, writable); | ||
if (written == writable && stream.available() != 0) { | ||
throw new BufferOverflowException(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to check writable capacity at this level? Since the WritableBuffer is a wrapper backed by some real buffer and all the operations are delegated to the underlaying buffer, we should just let the underlaying buffer handle the case and such an exception should be thrown from the underneath. |
||
} | ||
return written; | ||
} | ||
|
||
@Override | ||
public int writableBytes() { | ||
return bytebuf.writableBytes(); | ||
|
@@ -51,7 +84,7 @@ public int readableBytes() { | |
} | ||
|
||
@Override | ||
public void release() { | ||
public void close() { | ||
bytebuf.release(); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,7 +40,7 @@ public void setup() { | |
|
||
@After | ||
public void teardown() { | ||
buffer.release(); | ||
buffer.close(); | ||
} | ||
|
||
@Override | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of calling the method "drain" would it be more apt to call it "read"? That would make the logic very similar to
read(byte[] b, int off, int len)
.I'm not wild about the BufferOverflowException and transferring of all bytes. I see little reason to limit the API to reading all the bytes and it means data in invalided if an exception is thrown. While having to call read() multiple times is annoying, it is that way for a reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't disagree and this is actually what I started with for exactly the reasons you described, but I was thinking that the target impl doesn't actually work that way so this has the side effect that it will only be used when beneficial. And at least it's kind of similar to both
ReadableBuffer#readBytes(ByteBuffer)
which requires the target buffer to be large enough to hold the entire source, and theDrainable
interface which has to drain the entire InputStream.Having
ProtoInputStream
fall back to useByteArrayInputStream
in the case that the providedByteBuffer
isn't big enough would be worse than the current path, and this would not be obvious from the interface. WDYT?It's also why I changed the name to
drainTo
, agreeread
more appropriate if just filled theByteBuffer
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Beneficial" is in the eye of the beholder. Yes, with the current code that is true. If this was in an API we could readily change, I'd agree. But since this is a public API that 1) will last a long time and 2) have multiple users I'd much rather favor the more generic method definition. I'm optimizing the API for longevity while making sure it allows optimizing the implementation.
As things change, my preference order would be:
Since
drainTo(ByteBuffer)
would requireKnownLength
, I'm not as fond of it. It could be useful to drain a compressed message to aByteBuffer
or some such.Interfaces generally don't define guaranteed performance levels. List.get() may be fast or slow. We will consider performance when making changes, but at times we may need to hurt performance of one particular method for some other goal. So I'm okay with the varying level of performance. If you are benchmarking then you don't care about performance, and such a regression would show up on a benchmark.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about just moving it to
internal
package then (alongsideWritableBuffer
)?Probably this is clear but I meant that performance-wise the only implementation of this would actually involve more copying than less in the case that the stream didn't fit in the buffer i.e. worse than it is now and having the opposite than intended effect of this PR.