-
Notifications
You must be signed in to change notification settings - Fork 156
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
Refactor NatsMessage to use ByteBuffer internally. #349
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 |
---|---|---|
|
@@ -15,8 +15,8 @@ | |
|
||
import java.nio.ByteBuffer; | ||
import java.nio.CharBuffer; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Arrays; | ||
|
||
import io.nats.client.Connection; | ||
import io.nats.client.Message; | ||
|
@@ -26,103 +26,74 @@ class NatsMessage implements Message { | |
private String sid; | ||
private String subject; | ||
private String replyTo; | ||
private byte[] data; | ||
private byte[] protocolBytes; | ||
private ByteBuffer data; | ||
private ByteBuffer protocolBytes; | ||
private NatsSubscription subscription; | ||
private long sizeInBytes; | ||
|
||
NatsMessage next; // for linked list | ||
|
||
static final byte[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; | ||
|
||
static int copy(byte[] dest, int pos, String toCopy) { | ||
for (int i=0, max=toCopy.length(); i<max ;i++) { | ||
dest[pos] = (byte) toCopy.charAt(i); | ||
pos++; | ||
} | ||
private Integer protocolLength = null; | ||
|
||
return pos; | ||
} | ||
|
||
private static String PUB_SPACE = NatsConnection.OP_PUB + " "; | ||
private static String SPACE = " "; | ||
NatsMessage next; // for linked list | ||
|
||
// Create a message to publish | ||
NatsMessage(String subject, String replyTo, byte[] data, boolean utf8mode) { | ||
NatsMessage(String subject, String replyTo, ByteBuffer data, boolean utf8mode) { | ||
this.subject = subject; | ||
this.replyTo = replyTo; | ||
this.data = data; | ||
|
||
if (utf8mode) { | ||
int subjectSize = subject.length() * 2; | ||
int replySize = (replyTo != null) ? replyTo.length() * 2 : 0; | ||
StringBuilder protocolStringBuilder = new StringBuilder(4 + subjectSize + 1 + replySize + 1); | ||
protocolStringBuilder.append(PUB_SPACE); | ||
protocolStringBuilder.append(subject); | ||
protocolStringBuilder.append(SPACE); | ||
|
||
if (replyTo != null) { | ||
protocolStringBuilder.append(replyTo); | ||
protocolStringBuilder.append(SPACE); | ||
} | ||
|
||
protocolStringBuilder.append(String.valueOf(data.length)); | ||
Charset charset; | ||
|
||
this.protocolBytes = protocolStringBuilder.toString().getBytes(StandardCharsets.UTF_8); | ||
} else { | ||
// Convert the length to bytes | ||
byte[] lengthBytes = new byte[12]; | ||
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. Better to write this directly to |
||
int idx = lengthBytes.length; | ||
int size = (data != null) ? data.length : 0; | ||
|
||
if (size > 0) { | ||
for (int i = size; i > 0; i /= 10) { | ||
idx--; | ||
lengthBytes[idx] = digits[i % 10]; | ||
} | ||
// Calculate the length in bytes | ||
int size = (data != null) ? data.limit() : 0; | ||
int len = fastIntLength(size) + 4; | ||
if (replyTo != null) { | ||
if (utf8mode) { | ||
len += fastUtf8Length(replyTo) + 1; | ||
} else { | ||
idx--; | ||
lengthBytes[idx] = digits[0]; | ||
} | ||
|
||
// Build the array | ||
int len = 4 + subject.length() + 1 + (lengthBytes.length - idx); | ||
|
||
if (replyTo != null) { | ||
len += replyTo.length() + 1; | ||
} | ||
} | ||
if (utf8mode) { | ||
len += fastUtf8Length(subject) + 1; | ||
charset = StandardCharsets.UTF_8; | ||
} else { | ||
len += subject.length() + 1; | ||
charset = StandardCharsets.US_ASCII; | ||
} | ||
this.protocolBytes = ByteBuffer.allocate(len); | ||
protocolBytes.put((byte)'P').put((byte)'U').put((byte)'B').put((byte)' '); | ||
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. Would it be faster to use a backing array, initialized with 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 recall this was faster, and it will have better interoperability with the next round of changes which use |
||
protocolBytes.put(subject.getBytes(charset)); | ||
protocolBytes.put((byte)' '); | ||
|
||
this.protocolBytes = new byte[len]; | ||
|
||
// Copy everything | ||
int pos = 0; | ||
protocolBytes[0] = 'P'; | ||
protocolBytes[1] = 'U'; | ||
protocolBytes[2] = 'B'; | ||
protocolBytes[3] = ' '; | ||
pos = 4; | ||
pos = copy(protocolBytes, pos, subject); | ||
protocolBytes[pos] = ' '; | ||
pos++; | ||
|
||
if (replyTo != null) { | ||
pos = copy(protocolBytes, pos, replyTo); | ||
protocolBytes[pos] = ' '; | ||
pos++; | ||
} | ||
if (replyTo != null) { | ||
protocolBytes.put(replyTo.getBytes(charset)); | ||
protocolBytes.put((byte)' '); | ||
} | ||
|
||
System.arraycopy(lengthBytes, idx, protocolBytes, pos, lengthBytes.length - idx); | ||
if (size > 0) { | ||
int base = protocolBytes.limit(); | ||
for (int i = size; i > 0; i /= 10) { | ||
base--; | ||
protocolBytes.put(base, (byte)(i % 10 + (byte)'0')); | ||
} | ||
} else { | ||
protocolBytes.put((byte)'0'); | ||
} | ||
protocolBytes.clear(); | ||
} | ||
|
||
this.sizeInBytes = this.protocolBytes.length + data.length + 4;// for 2x \r\n | ||
NatsMessage(String subject, String replyTo, byte[] data, boolean utf8mode) { | ||
this(subject, replyTo, ByteBuffer.wrap(data), utf8mode); | ||
} | ||
|
||
// Create a protocol only message to publish | ||
NatsMessage(CharBuffer protocol) { | ||
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(protocol); | ||
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. This does not allocate a correctly sized 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. good catch! 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. Yep, this sort of issue is one of the reasons I'm trying to move away from any low level char/string based protocol encoding/decoding to UTF-8/ASCII byte/bytebuffer based encoding/decoding as much as possible. |
||
this.protocolBytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); | ||
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data | ||
this.sizeInBytes = this.protocolBytes.length + 2;// for \r\n | ||
if (protocol.remaining() == 0) { | ||
this.protocolBytes = ByteBuffer.allocate(0); | ||
} else { | ||
protocol.mark(); | ||
this.protocolBytes = ByteBuffer.allocate(fastUtf8Length(protocol)); | ||
protocol.reset(); | ||
StandardCharsets.UTF_8.newEncoder().encode(protocol, this.protocolBytes, true); | ||
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. Could you use a static instance rather than allocating every new msg? 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 recall I tested that and this was a good bit faster in benchmarks, not 100% sure why, best guess is having multiple threads accessing the same static encoder instance causes cache flushing or something. I plan to refactor this anyways down the line once I refactor some prerequisite code. 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. Yep, verified this has a major performance impact.
non-static(current) implementation:
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. Wow, would have lost a bet on that one. Thanks! 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. Also noticed this here:
|
||
protocolBytes.clear(); | ||
} | ||
} | ||
|
||
// Create an incoming message for a subscriber | ||
|
@@ -133,24 +104,94 @@ static int copy(byte[] dest, int pos, String toCopy) { | |
if (replyTo != null) { | ||
this.replyTo = replyTo; | ||
} | ||
this.sizeInBytes = protocolLength + 2; | ||
this.protocolLength = protocolLength; | ||
this.data = null; // will set data and size after we read it | ||
} | ||
|
||
private static int fastUtf8Length(CharSequence cs) { | ||
int count = 0; | ||
for (int i = 0, len = cs.length(); i < len; i++) { | ||
char ch = cs.charAt(i); | ||
if (ch <= 0x7F) { | ||
count++; | ||
} else if (ch <= 0x7FF) { | ||
count += 2; | ||
} else if (Character.isHighSurrogate(ch)) { | ||
count += 4; | ||
++i; | ||
} else { | ||
count += 3; | ||
} | ||
} | ||
return count; | ||
} | ||
|
||
private static int fastIntLength(int number) { | ||
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. This implementation while a bit verbose is significantly faster than a naive implementation and allows us to eliminate a separate length buffer which lets us write directly to |
||
if (number < 100000) { | ||
if (number < 100) { | ||
if (number < 10) { | ||
return 1; | ||
} else { | ||
return 2; | ||
} | ||
} else { | ||
if (number < 1000) { | ||
return 3; | ||
} else { | ||
if (number < 10000) { | ||
return 4; | ||
} else { | ||
return 5; | ||
} | ||
} | ||
} | ||
} else { | ||
if (number < 10000000) { | ||
if (number < 1000000) { | ||
return 6; | ||
} else { | ||
return 7; | ||
} | ||
} else { | ||
if (number < 100000000) { | ||
return 8; | ||
} else { | ||
if (number < 1000000000) { | ||
return 9; | ||
} else { | ||
return 10; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
boolean isProtocol() { | ||
return this.subject == null; | ||
} | ||
|
||
// Will be null on an incoming message | ||
byte[] getProtocolBytes() { | ||
return this.protocolBytes; | ||
return this.protocolBytes.array(); | ||
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. Since all |
||
} | ||
|
||
int getControlLineLength() { | ||
return (this.protocolBytes != null) ? this.protocolBytes.length + 2 : -1; | ||
return (this.protocolBytes != null) ? this.protocolBytes.limit() + 2 : -1; | ||
} | ||
|
||
long getSizeInBytes() { | ||
long sizeInBytes = 0; | ||
if (this.protocolBytes != null) { | ||
sizeInBytes += this.protocolBytes.limit(); | ||
} | ||
if (this.protocolLength != null){ | ||
sizeInBytes += this.protocolLength; | ||
} | ||
if (data != null) { | ||
sizeInBytes += data.limit() + 4;// for 2x \r\n | ||
} else { | ||
sizeInBytes += 2; | ||
} | ||
return sizeInBytes; | ||
} | ||
|
||
|
@@ -160,8 +201,7 @@ public String getSID() { | |
|
||
// Only for incoming messages, with no protocol bytes | ||
void setData(byte[] data) { | ||
this.data = data; | ||
this.sizeInBytes += data.length + 2;// for \r\n, we already set the length for the protocol bytes in the constructor | ||
this.data = ByteBuffer.wrap(data); | ||
} | ||
|
||
void setSubscription(NatsSubscription sub) { | ||
|
@@ -189,7 +229,7 @@ public String getReplyTo() { | |
} | ||
|
||
public byte[] getData() { | ||
return this.data; | ||
return this.data.array(); | ||
} | ||
|
||
public Subscription getSubscription() { | ||
|
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.
This isn't needed, should be faster to just add
0x30
to convert a byte to ascii.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.
add to the PR, should be ok - although i might - since i am paranoid - convert 0 to a byte to get the value :-)
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.
ASCII "0" to byte gives you
0x30
, I'm doing this in integer mode then casting tobyte
when writing to the buffer, at least I think that's probably the most efficient way to do it.