-
Notifications
You must be signed in to change notification settings - Fork 68
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
Performance is not ideal #333
Comments
Hi @dae -- I'm not developer for this project, but I'm a user and just saw your issue. Providing a concrete use case is always helpful in bug reports so I just thought I'd chime in and say yeah, that's useful. :) Especially if you can also provide minimal code to reproduce the performance issue. Then it becomes a de facto test case so any fixes can be tested with before/after code. |
Adding Protobuf-ES to |
I suspect any large message will reproduce the issue, but can produce a sample one if the devs request it. On a positive note, protobuf-es's code generation seems to be a lot faster than what we currently have with protobufjs, where we have to produce a static version, which is then used to generate the ts, and then a separate json version needs to be produced because the static version's resulting code is huge. protobuf-es appears to be clearly sub-second, when protobufjs was taking around 5 seconds to rebuild each time a proto file was changed. |
Reference how |
Protons appears to use protobuf.js's UTF-8 decoder/encoder which has correctness bugs and likely the reason protobuf-es uses the native TextEncoder/Decoder. If you're using the NodeJS runtime for your benchmarks you can comment on this bug to ask for a faster TextEncoder/Decoder: nodejs/node#39879 as Bun and Deno appear to have much faster implementations. Chrome, Firefox, and Safari are also generally much faster using the native TextEncoder/Decoder vs. protobuf.js's implementation (especially once you get above 32 characters) timostamm/protobuf-ts#184 (comment) |
Just to add some context also - It was a design choice to focus on usability, developer experience, and conformance when creating Protobuf-ES. Consequently, Protobuf-ES is nearly 100% compliant with the Protobuf conformance test suite. To further illustrate this, we've created this Protobuf Conformance repo which shows how other implementations fare against this test suite. You can see that other implementations might be super performant, but the conformance scores are not great. Nevertheless, we have some ideas for improving performance that are on our roadmap for the future. |
Any chance you could share some more context on the improvements you're considering, or any sense of a timeline on those improvements landing? We haven't done a rigorous benchmark but our initial tests have shown observable performance regressions if we migrate from protobuf.js. It would be helpful to know what's in progress (or done already, as @timostamm called out). |
Repo: https://github.com/bufbuild/protobuf-es Release: https://buf.build/blog/protobuf-es-the-protocol-buffers-typescript-javascript-runtime-we-all-deserve --- Relates to bufbuild/protobuf-es#333 --------- Co-authored-by: Alex Potsides <alex@achingbrain.net>
Hi @dimo41. We don't have any timeline to report but this is something we plan to address relatively soon. Some additional things we want to explore are first, investigating whether the changes made in #459 could also be applied to binary write performance as well as JSON read/write performance. In addition, we'd like to investigate potentially supporting the |
I was going to open a new issue for this but since writing / encoding performance is mentioned here latterly, I'll add to this discussion. We have an issue where we are encoding a large number of f64s (doubles). In a particular example we are overall encoding around 4 million floats across a number of protobuf messages (~100 messages so each one contains ~40,000 doubles) The proto definition is more or less (there are some other fields but the overwhelming amount of data in each message is the message MyMessage {
repeated double data = 1;
} Profiling In our case I'm able to show a ~30x encoding performance improvement (encoding takes arrayDouble(values: number[]): IBinaryWriter {
let chunk = new Uint8Array(8 * values.length);
const view = new DataView(chunk.buffer);
for (const [i, value] of values.entries()) {
view.setFloat64(8*i, value, true);
}
return this.raw(chunk);
} Then in export function writePacked(
writer: IBinaryWriter,
type: ScalarType,
fieldNo: number,
value: any[]
): void {
if (!value.length) {
return;
}
writer.tag(fieldNo, WireType.LengthDelimited).fork();
let [, method] = scalarTypeInfo(type);
if (method === "double") {
writer["arrayDouble"](value as number[]);
} else {
for (let i = 0; i < value.length; i++) {
(writer[method] as any)(value[i]);
}
}
writer.join();
} This seems to work in our case (I have a test where I encode a bunch of Assuming such a change is sensible this would make sense at least for some of the other primitive types, if not in general? |
Hey Malcolm, thanks for the comment! This could be applied to all packed protobuf types with fixed size. There's no free lunch though, the downsides are increased bundle size and breaking the IBinaryWriter interface. Editions will stir up the code paths a bit, so it does not make sense to pull in this performance improvement right now, but it does seem worthwhile to do so after we implemented support for editions. I wonder if an argument to fork() with a size to allocate in advance would be an alternative here. It's also likely that the perf improvement for parsing added in #459 applies to serializing as well. |
It looks like a similar perf improvement applied for binary read (see #333 (comment)) can also be applied for binary write. Some details we're noted in #674 (comment). |
Just to add a +1: this is the same for us, we can't switch over because the slowdown for encode and decode we would experience is about ~15x. This library is awesome though! You write very clean code, that's nice to interface with. |
I set up a repo for comparing encode speeds or protobufs: https://github.com/Ekrekr/prototiming.
It seems like protobufjs is a lot more efficient at writing directly to the buffer, rather than chunking and copying around array buffers. I'll dig into this a bit more. |
The most clear place to optimize seems to be in An array buffer is made during chunking:
And a new array buffer is made at finish, and the contents of the chunking copied over to it iteratively:
If instead of creating a new array buffer at finish, an array buffer could be made at the start and written to directly, this would prevent writing duplication. Edit: my experiments with this are in #964. |
We currently use protobufjs, and would love to switch over to protobuf-es for the typescript ergonomics and es6/tree shaking that it offers. Parts of our app deal with large messages, so I did a quick performance test, and unfortunately protobuf-es is rather behind at the moment. The test I used was decoding a 6MB binary message, it containing repeated fields with about 150k entries. Protobufjs parses it in about 100ms; protobuf-es takes about 350ms. For reference, the same message as JSON is about 27MB, and JSON.parse() parses it in about 100ms as well. If providing the message/proto would be helpful, please let me know.
The text was updated successfully, but these errors were encountered: