perf: PBJReader/PbjWriter optimization branch - #883
Conversation
JUnit Test Report 521 files ± 0 521 suites ±0 31s ⏱️ ±0s Results for commit a30bb06. ± Comparison against base commit 8770b39. This pull request removes 6 and adds 21 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Integration Test Report1 files - 427 1 suites - 427 38s ⏱️ - 12m 49s Results for commit a30bb06. ± Comparison against base commit 8770b39. This pull request removes 115047 tests.♻️ This comment has been updated with latest results. |
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Signed-off-by: ldintr <levo.d@swirldslabs.com>
| .replace("$readMethod", field.type() == Field.FieldType.ENUM ? "value" : readMethod(field)) | ||
| .replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize") | ||
| .replace("$fieldName", field.name()) | ||
| .replace("$divideString", divideAmount == 1 ? "" : "/%s".formatted(divideAmount)) |
There was a problem hiding this comment.
%s is designed to handle Formattable objects, or it falls back to calling toString(). This means that it accepts an object as an argument. Which in turn means that the primitive integer variable divideAmount gets boxed into an Integer at runtime. This may be inefficient. Is there a reason not to use %d?
| * {@link #length()} bytes. Mutating the returned array breaks the immutability contract of this class. | ||
| * | ||
| * @return the internal backing byte array | ||
| */ | ||
| @NonNull | ||
| public byte[] array() { |
There was a problem hiding this comment.
It's nice to mention that the API is unsafe in the javadoc, but the method name still looks too very inviting. An engineer may not think it through when using an auto-complete in their favorite IDE and choose this unsafe version vs. the safe toByteArray. I suggest to rename this method to arrayUnsafe() instead.
There was a problem hiding this comment.
Could you provide more info when users may need the backing array, please? It would be great to avoid exposing the array in API in any way (as Bytes does)
There was a problem hiding this comment.
Oh, wait, this is Bytes, not PbjReader or PbjWriter. So exposing the backing array is not acceptable
| /** The raw backing byte array. */ | ||
| byte[] byteArray(); |
There was a problem hiding this comment.
Similar to above, this method is public and it may expose the underlying array which may be considered immutable by e.g. Bytes. Both the name of this method and its javadoc need to change to make it less likely to misuse it in applications. E.g. byteArrayUnsafe might be a better name.
There was a problem hiding this comment.
I am not sure this interface is the right way to provide access to a byte array to PbjReader. What is Bytes.toPbjReader() hands over its backing array to the created PbjReader without exposing any methods like array() or arrayOffset()? Also, need to make sure the array cannot be accessed in PbjReader in any legal way
| private final boolean useStacktrace = | ||
| !"false".equalsIgnoreCase(System.getProperty("pbj.ReaderWriter.useStackTrace")); |
There was a problem hiding this comment.
System properties cannot change at runtime. So reading it every time a new instance of the PbjReader is created is inefficient. You want to make this field static to read it just once during a JVM run.
There was a problem hiding this comment.
Good catch, I updated both places
| public static final int EOF = -1, | ||
| DataEncoding = 1, |
There was a problem hiding this comment.
Minor: per Java Coding Conventions, static fields should use UPPER_CASE_SNAKE_NAMES. The current naming looks unnatural to a Java programmer and causes a confusion because the names can be seen as inner class names instead. It would be nice to rename all the error codes accordingly.
The comment applies to any and all static fields, in this class and elsewhere in this fix.
There was a problem hiding this comment.
I'll probably change this to all caps. Does anyone prefer the way it is? If no one does Ill change
| int len = measureRecord(item); | ||
| PbjWriter writer = new PbjWriter(len, false); | ||
| write(item, writer); | ||
| return writer.internalArrayWrapped(); |
There was a problem hiding this comment.
Internal array in a writer is generally mutable, so wrapping it in Bytes might violate its mutability contract. You want to carefully describe the intended usage in the javadoc to avoid misuse of this method.
| writer.accept(out); | ||
| } | ||
|
|
||
| public static void writeDelimited( |
There was a problem hiding this comment.
Minor: for arrays, we've introduced a separate ProtoArrayWriterTools. The reason for that was because this ProtoWriterTools.java was already too long a file. I suggest to move all the PbjWriter-backed methods into a new class for the same reason because it's difficult to work with source files that are thousands lines long.
There was a problem hiding this comment.
I want them next to eachother for the moment so its easy to see theyre both the same implementation. I imagine we can't delete the WritableSequentialData version anytime soon? If we can I can move things elsewhere
| // return length or 0 on error | ||
| static int encodedLength(String sz) { |
There was a problem hiding this comment.
Minor: this API could be error-prone if sz.length() == 0. Please consider a different value to indicate an error.
There was a problem hiding this comment.
I was worried about someone assuming positive only values and getting a negative count losing data if they're summing things up. I believe elsewhere I also return empty string on error so its consistent. I'll see if I can move this and others into pbj writer (or reader) so the error can be found in one place
| } | ||
| } | ||
|
|
||
| static void WriteUTF8(String str, PbjWriter out) { |
There was a problem hiding this comment.
In Java, method names should start with a lower-case letter. Apart from being a part of Java Coding Conventions, in practice this helps avoid confusing them with class names and constructor names.
There was a problem hiding this comment.
Done. I missed that one :( most places had writeUTF8
| @@ -1 +1 @@ | |||
| 0.15.0-SNAPSHOT | |||
| 0.pbj.1 | |||
There was a problem hiding this comment.
This may be a part of your current testing/CN-repo-interoperability setup. However, we don't want to merge this change in version.txt into main.
| } | ||
|
|
||
| @NonNull | ||
| public PbjReader toPbjReader() { |
There was a problem hiding this comment.
Is this one really needed? There's clear way to create a PbjReader from Bytes, just use PbjReader constructor, no need to expose any Bytes internals to that class or whatever
There was a problem hiding this comment.
I did it to keep it consistent with how people use bytes with toReadableSequentialData. I think I should delete it because the parse have Bytes overload, but I dont know if deleting this will make it harder to switch to my new code.
| * | ||
| * @return start offset into the backing array | ||
| */ | ||
| public int arrayOffset() { |
There was a problem hiding this comment.
This looks even more internal (and therefore subject to removal) than array(). I've always found ByteBuffer.arrayOffset() confusing, and this one looks pretty similar to that
| boolean inUse = false; | ||
| } | ||
|
|
||
| ThreadLocal<WriteCache> tlsWriter = ThreadLocal.withInitial(WriteCache::new); |
There was a problem hiding this comment.
This means tons of thread locals, one per generated Codec class (there are many!)
There was a problem hiding this comment.
Thats true in C++, I asked claude and it said one per thread and mentioned type erasers. I just double checked now by writing code and it seems like test spawn more workers than threads I have on my machine. But I can see it's not making more than one per thread
| */ | ||
| public interface Codec<T> { | ||
| class WriteCache { | ||
| PbjWriter writer = new PbjWriter(); |
There was a problem hiding this comment.
This looks like an implementation optimization rather than a part of the contract. It would be better to place it to a subclass, e.g. introduce AbstractCodec implements Codec and change codec generation code to use AbstractCodec. This would not be perfect, either, but still slightly better than extra classes and instance variables in an interface, from my perspective
| throws ParseException { | ||
| ReadCache cache = tlsReader.get(); | ||
| if (cache.inUse) { | ||
| PbjReader reader = new PbjReader(input); |
There was a problem hiding this comment.
New PbjReader per recursive call. Is it possible to wrap a ReadableSequentialData input into a PbjReader just once and use it for all recursive calls?
In general, this parse/wrapParse approach looks somewhat confusing to me, especially at the interface level
| } | ||
|
|
||
| @NonNull | ||
| default T parse(@NonNull InputStream in, boolean strictMode, boolean parseUnknownFields, int maxDepth, int maxSize) |
There was a problem hiding this comment.
Are all these new parse() methods actually needed?
There was a problem hiding this comment.
There's code in CN that overloads just a few parameters, which is why I changed the generated code to realParse briefly, so I can have it run the expected code. I think having all these overloads would be better than rearchitecture the code
| return di; | ||
| } | ||
|
|
||
| public static int fromUTF8(char[] dst, byte[] src, int offset, int pos, int length) { |
| return bytes; | ||
| } | ||
|
|
||
| public static Bytes readBytes(PbjReader input, final long maxSize) { |
There was a problem hiding this comment.
JavaDoc, please.
Why isn't it consistent with the method above (doesn't throw ParseException, etc.)?
Signed-off-by: ldintr <levo.d@swirldslabs.com>
Description:
My branch for PBJ, it switches to PbjReader and PbjWriter instead of using an interface.
parseandwriteare significantly faster. Here are some notesPbjReader/PbjWriterPbjWriterhas atoPbjReadermethod which reuses the buffer. It can be used as long as you don't need to write to the writer object before the reader is done. Otherwise use toByteArray to clone the bufferarray()andarrayOffset()soPbjReaderdoesn't need to copy dataPbjReader/PbjWriterdoesn't throw checked exceptions. If you need them you can use throwOnError(), or useerror()to get an error codeReadableSequentialDatahave either the signature change toPbjReaderor had the function copied with the parameter changed toPbjReader. Same withWritableSequentialDataandPbjWriterRelated issue(s):
Fixes #
Notes for reviewer:
parse and write were changed to realParse and realWrite and changed back in the cleanup. That was to help me develop easier. hasMore changed back to hasRemaining
Checklist