netty/netty — pull request #390 by gbehrmann (2012-06-11 15:59:24+00:00)
Title: ZeroCopyFrameDecoder to avoid excessive buffer copies in FrameDecoder when using large frames

Hi,

We use Netty in a project to implement the xrootd file transfer protocol. When upgrading from Netty 3.2.7 to 3.5.0 we observed a dramatic drop in upload performance. I tracked down the problem to the following piece of code in FrameDecoder:

try {
callDecode(ctx, e.getChannel(), buf, e.getRemoteAddress());
} finally {
if (!buf.readable()) {
// nothing readable left so reset the state
cumulation = null;
} else {
// create a new buffer and copy the readable buffer into it
cumulation = newCumulationBuffer(ctx, buf.readableBytes());
cumulation.writeBytes(buf);
}
}

If the frame is incomplete, this code will always copy the buffer. For a large frame the data will be copied over and over until the frame is finally complete.

In our case the client (which is not under our control) uses 8MB frames during upload. I know I could work around the problem by introducing a chunking mechanism like the one used in the HttpMessageDecoder, but I felt compelled to attempt solve the problem in FrameDecoder instead.

The commit introduces ZeroCopyFrameDecoder. ZeroCopyFrameDecoder avoids copying incomplete frames. Instead the handler maintains a list of buffers, composing them as needed and discarding those that have been read.

I am submitting this for the purpose of discussion, as I am not 100% certain about the rules pertaining to ownership of buffers. I checked the NioWorker code and it appears to allocate a fresh buffer for each fragment it reads from the socket. At least in that case, the logic I use in ZeroCopyFrameDecoder seems valid. I would however like the input from the experts on this matter. Does what I do make sense or are there situations in which it would break horribly?

I chose to introduce a modified copy of FrameDecoder because the behaviour on which subclasses like ReplayDecoder rely has changed. The documented behaviour is the same and I did not need to make any modifications in my own decoder. ReplayDecoder however relies on undocumented behaviour of FrameDecoder and would need to be updated if we chose to modify FrameDecoder rather than introduce a new class.

For your information, the upload performance with 3.2.7 was:

$ time xrdcp /tmp/big root://localhost/xrd-big1
[xrootd] Total 95.37 MB	|====================| 100.00 % [340.1 MB/s]

real	0m0.489s
user	0m0.028s
sys	0m0.148s

With Netty 3.5.0 throughput dropped significantly:

$ time xrdcp /tmp/big root://localhost/xrd-big2
[xrootd] Total 95.37 MB	|====================| 100.00 % [26.5 MB/s]

real	0m4.185s
user	0m0.064s
sys	0m0.196s

With ZeroCopyFrameDecoder throughput is restored to the previous level:

$ time xrdcp /tmp/big root://localhost/xrd-big3
[xrootd] Total 95.37 MB	|====================| 100.00 % [261.1 MB/s]

real	0m0.561s
user	0m0.012s
sys	0m0.176s
$ time xrdcp /tmp/big root://localhost/xrd-big4
[xrootd] Total 95.37 MB	|====================| 100.00 % [380.2 MB/s]

real	0m0.346s
user	0m0.016s
sys	0m0.176s