You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This topic is an excerpt of the discussion in #82 (comment)
In event-driven I/O, we call unpackXXX after we receive new data from a network (or another resource). That indicates unpackXXX might be called even if insufficient amount of packed data is available.
Because of this, behavior of unpackXXX (e.g., unpackInt, unpackString, etc.) would be as follows:
It returns the unpacked value and proceeds the cursor on success.
If it fails due to lack of the incoming data, it throws an InsufficientDataException. The cursor does not move in this case, so that the user can call unpackXXX after sufficient amount of data becomes available.
The previous version v06 implements getXXX(), which does not move the cursor, and advance() to support repeatable read of unpackXXX if we do not call advance().
But this interface was not good because buffering mechanism must be implemented in every adapter of MessageBufferInput interface. And also, only the unpacker knows the size of incoming data, so it was difficult to implement efficient buffering in the MessageBufferInput.
@frsyuki 's idea is to cache the header and data length information of message packed data to allow calling unpackXXX multiple times without making complex the implementation of MessageBufferInput.
Question:
The head and data length are at most 5 bytes. Now that we have MessageBuffer for caching the input data, what is the benefit of having variables to store these data?
Reading head and data length are cheap operations.
If the data is at the boundary of MessageBuffer, we eventually need to relocate the buffer contents or preserve two or more consecutive buffers.
Here is my idea to support event-driven IO efficiently:
publicshortunpackShort() throwsIOException {
finalbyteb = getHeadByte();
if (Code.isFixInt(b)) {
this.head = READ_NEXT_HEAD_BYTE;
return (short) b;
}
switch(b) {
caseCode.INT32:
intvalue = readInt(); // may throw InsufficientDataException (retryable)// readInt succeeded. Don't have to be retryable any morethis.head = READ_NEXT_HEAD_BYTE;
// ...break;
// more cases ...
}
}
privateMessageBufferextraBuffer = MessageBuffer.wrap(newbyte[8]);
privateintextraBufferFilledSize = 0;
privateintreadInt() throwsIOException {
if (extraBufferFilledSize > 0) {
if (extraBufferFilledSize == 4) {
intvalue = extraBuffer.getInt(0); // extraBuffer needs at most 8 bytes for readInt()extraBufferFilledSize = 0;
returnvalue;
}
} elseif (buffer.size() - position >= 4) {
// this is the fast-path which should be inlinedintvalue = buffer.getInt(position);
position += 4;
returnvalue;
}
// fill extraBuffer from buffer...if (buffer.size() <= position) {
// buffer is emptyrequireNextBuffer(); // may throw InsufficientDataException or IOException
}
// pseudo method MessageBuffer.get(MessageBuffer, srcOffset, dstOffset, size)// copies data from the receiver to the given buffer at most size bytes.intn = buffer.get(extraBufferFilledSize, position, extraBufferFilledSize, 4 - extraBufferFilledSize);
extraBufferFilledSize += n;
returnreadInt(); // tail call will be optimized by compiler
}
To make unpackShort repeatable, Unpacker needs 1 (head) + 8 (extraBuffer) bytes buffer (as commented in the code). This buffer can be one MessageBuffer as you suggest (9-byte MessageBuffer). 9-byte buffer (actually 13-byte because it needs to store size as well later) also works but byte + MessageBuffer is easier to implement, I guess.
We can optimize readInt as following by the way:
// this method should be inlined by JITprivateintreadInt() throwsIOException {
// requireBuffer() guarantees that buffer.size() == position if extraBufferFilledSize > 0.// requireBuffer() guarantees that buffer can't be null (use private static final EMPTY_BUFFER = MessageBuffer.wrap(new byte[0])) so that here doesn't have to check buffer != null.if (buffer.size() - position >= 4) {
intvalue = buffer.getInt(position);
position += 4;
returnvalue;
} else {
returnreadIntOverBufferBoundary();
}
}
// this method including tail call doesn't have to be inlined because this is rarely called.privateintreadIntOverBufferBoundary() throwsIOException {
if (extraBufferFilledSize == 4) {
intvalue = extraBuffer.getInt(0);
extraBufferFilledSize = 0;
returnvalue;
}
// fill extraBuffer from buffer...if (buffer.size() <= position) {
// buffer is emptyrequireNextBuffer(); // may throw InsufficientDataException or IOException
}
//// optional: possibly improve performance//if(extraBufferFilledSize == 0 && buffer.size() >= 4) {// int v = buffer.getInt(position);// position += 4;// return v;//}// pseudo method MessageBuffer.get(MessageBuffer, srcOffset, dstOffset, size)// copies data to the given buffer at most given size bytes.intn = buffer.get(extraBufferFilledSize, position, extraBufferFilledSize, 4 - extraBufferFilledSize);
position += n;
extraBufferFilledSize += n;
returnreadIntOverBufferBoundary(); // tail call
}
Another example is unpackArrayHeader:
publicintunpackArrayHeader() throwsIOException {
finalbyteb = getHeadByte();
if (Code.isFixedArray(b)) {
this.head = READ_NEXT_HEAD_BYTE;
returnb & 0x0f;
}
switch (b) {
caseCode.ARRAY32:
intu32 = readInt(); // may throw InsufficientDataException// readInt succeeded. Don't have to be retryable any morethis.head = READ_NEXT_HEAD_BYTE;
if (u32 < 0) {
throwoverflowU32Size(u32); // throws MessageFormatException (not retryable) but never throws InsufficientDataException (retryable)
}
returnu32;
// ...
}
}
Unpacker doesn't need to store size to make unpackArrayHeader, unapckStringHeader or unpackBinaryHeader repeateble. size is necessary to make unpackExtendedTypeHeader, unpackString, and unapckRawString repeatable. Here is an example implementation of unpackExtendedTypeHeader:
unpackRawString also needs size but more tricky than unpackExtendedTypeHeader because it needs to read variable-length data. There're 2 possible implementations as I suggested before:
allocate a new buffer to read the variable-length data. Unpacker can pre-allocate 8-byte (or 13-byte) extraBuffer because unpackRawString doesn't use extraBuffer.
use extraBuffer to read the variable-length data. Allocation, expansion, ownership management of extraBuffer could be complicated.
Here is an example implementation of idea 1):
privateRawStringreadingRawString = null; // RawString might be MessageBuffer or byte[]privateintreadingRawStringSize;
publicRawStringunpackRawString() throwsIOException {
finalbyteb = getHeadByte();
switch (b) {
caseCode.STR32:
intstrSize = getNextSize32(); // may throw InsufficientDataException (retryable)if(readingRawString == null) {
// Perhaps users might use a custom MessageBuffer allocator here.// Then unpackRawString() will be unpackRawString(BufferAllocator)readingRawString = MessageBuffer.allocate(size);
readingRawStringSize = 0;
}
Stringvalue = readRawStringCopy(strSize);
// readRawStringCopy succeeded. Don't have to be retryable any morethis.head = READ_NEXT_HEAD_BYTE;
this.size = READ_NEXT_SIZE;
returnvalue;
// ...
}
}
privateRawStringreadRawStringCopy(intsize) throwsIOException {
intremaining = size - readingRawStringSize;
// pseudo method readPayloadAtMost(size, RawString) reads at most given size bytes from// underlaying buffer or input stream to RawString.intreadBytes = readPayloadAtMost(remaining, readingRawString);
if(readBytes >= remaining) {
// completed// This is the fast-path. We might be able to create a optimized method like// readInt() + readIntOverBufferBoundary().RawStringv = readingRawString;
readingRawString = null;
returnv;
}
readingRawStringSize += readBytes;
// now buffer is emptyrequireNextBuffer(); // may throw InsufficientDataException (retryable)returnreadRawStringCopy(size); // tail call
}
Zero-copy (unpackRawStringReference) will be like this:
publicRawStringReferenceunpackRawStringReference() throwsIOException {
finalbyteb = getHeadByte();
switch (b) {
caseCode.STR32:
intstrSize = getNextSize32(); // may throw InsufficientDataException (retryable)if(readingRawString == null && buffer.size() - position >= strSize) {
RawStringReferencev = newRawStringReference(buffer.slice(position, strSize));
position += strSize;
returnv;
}
returnunpackRawStringCopy(); // this is valid because getHeadByte and getNextSize32 are repeatable// ...
}
}
unpackString can have different buffering mechanism:
publicStringunpackString() throwsIOException {
finalbyteb = getHeadByte();
switch (b) {
caseCode.STR32:
intstrSize = getNextSize32(); // may throw InsufficientDataException (retryable)if(decodingStringBuffer == null) {
decodingStringBuffer = newStringBuilder();
decodingStringByteSize = 0;
}
Stringvalue = readString(strSize);
// readString succeeded. Don't have to be retryable any morethis.head = READ_NEXT_HEAD_BYTE;
this.size = READ_NEXT_SIZE;
returnvalue;
// ...
}
}
privateStringBuilderdecodingStringBuffer = null;
privateintdecodingStringByteSize;
privateStringreadString(intsize) throwsIOException {
intremaining = size - decodingStringByteSize;
// pseudo method readPayloadAtMostAndDecodeUtf8(MessageBuffer, size, StringBuilder) reads at most given size bytes from// MessageBuffer and decode it to StringBuilder.intdecodedBytes = readPayloadAtMostAndDecodeUtf8(buffer, remaining, decodingStringBuffer);
if(decodedBytes >= remaining) {
// decoding completedStringv = decodingStringBuffer.toString();
decodingStringBuffer = null;
returnv;
}
decodingStringByteSize += decodedBytes;
// now buffer is emptyrequireNextBuffer(); // may throw InsufficientDataException (retryable)returnreadString(size); // tail call
}
If Unpacker supports unpackExtendedType (reads ExtendedTypeHeader and payload), Unpacker needs to store type as well as head and size.
invalid: readInt() -> getHeadByte() (readInt is not repeatable)
Generally, getXxx 0 or more times -> readXxx is valid. In other words, unpackXxx methods call readXxx at the end. So we can create following methods (like: readIntAndResetHeadByte():
int readIntAndResetHeadByte() {
int v = readInt();
headByte = READ_NEXT_HEAD_BYTE;
return v;
}
I'm developing a library that consumes msgpack streams(Fluentd's forward requests) but I have noticed that ChannelBufferInput doesn't work well on non-blocking IO.
I want stream deserializer too much……
Activity
xerial commentedon May 15, 2014
Here is a summary of comment from @frsyuki: #82 (comment)
getXXX(), which does not move the cursor, andadvance()to support repeatable read of unpackXXX if we do not calladvance().xerial commentedon May 15, 2014
@frsyuki 's idea is to cache the header and data length information of message packed data to allow calling unpackXXX multiple times without making complex the implementation of MessageBufferInput.
Question:
frsyuki commentedon May 15, 2014
Here is my idea to support event-driven IO efficiently:
To make unpackShort repeatable, Unpacker needs 1 (head) + 8 (extraBuffer) bytes buffer (as commented in the code). This buffer can be one MessageBuffer as you suggest (9-byte MessageBuffer). 9-byte buffer (actually 13-byte because it needs to store size as well later) also works but byte + MessageBuffer is easier to implement, I guess.
We can optimize
readIntas following by the way:Another example is
unpackArrayHeader:Unpacker doesn't need to store
sizeto makeunpackArrayHeader,unapckStringHeaderorunpackBinaryHeaderrepeateble.sizeis necessary to makeunpackExtendedTypeHeader,unpackString, andunapckRawStringrepeatable. Here is an example implementation ofunpackExtendedTypeHeader:unpackRawStringalso needssizebut more tricky thanunpackExtendedTypeHeaderbecause it needs to read variable-length data. There're 2 possible implementations as I suggested before:extraBufferbecauseunpackRawStringdoesn't useextraBuffer.extraBufferto read the variable-length data. Allocation, expansion, ownership management ofextraBuffercould be complicated.Here is an example implementation of idea 1):
Zero-copy (
unpackRawStringReference) will be like this:unpackStringcan have different buffering mechanism:If Unpacker supports
unpackExtendedType(reads ExtendedTypeHeader and payload), Unpacker needs to storetypeas well asheadandsize.frsyuki commentedon May 15, 2014
My concept is:
getHeadByte()->readInt()getHeadByte()->getNextSize32()->readLongreadInt()->getHeadByte()(readInt is not repeatable)Generally,
getXxx0 or more times ->readXxxis valid. In other words,unpackXxxmethods callreadXxxat the end. So we can create following methods (like: readIntAndResetHeadByte():okumin commentedon Jul 30, 2016
Do you have a plan to release this feature?
I'm developing a library that consumes msgpack streams(Fluentd's forward requests) but I have noticed that
ChannelBufferInputdoesn't work well on non-blocking IO.I want stream deserializer too much……