-
-
Notifications
You must be signed in to change notification settings - Fork 560
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
Changing the binary message format? #2496
Comments
@twittner thank you for starting this discussion and writing such an insightful analysis ❤️ I'll spend more time with your notes soon, but here's one idea that we should consider ... Certain messages on the routing/messaging layer (e.g TransportMessage or SecureChannel's data) are wrapped inside each other multiple times .. to support tunneling. For such messages ... overheads compound quickly, and compactness is more important .. especially to maintain support for low-power wireless protocols. It would be okay to compromise on ease of use at this low-level- if we have to. Safety is critical at this layer but the surface we will have to check is relatively small. As we move up layers to protocols that are end-to-end, compactness is less important because the cost is not compounding. Ease of use is significantly more important. Safety critical surface is relatively large. I think we should consider CBOR at these higher level layers - but keep BARE (or something similarly compact) at the routing/messaging layer. |
Fascinating -- love that you actually approached this by measuring, and the change looks very nice. I also wasn't aware of https://docs.rs/minicbor-derive/latest/minicbor_derive/index.html and impressed by the protobuf-style forward/backward compatibility -- very cool. (Honestly, I wasn't aware that you could replace the field names with indices at all -- that seems very useful). Some questions I have are:
Also, I must admit, I'm a little unsure what point you're getting at with the two examples. Can you explain them a bit more? Some other points:
Yeah, this is a big issue in practice -- as it's very easy to |
This is a great analysis @twittner, I hadn't come across CBOR before. I'd also be interested to see numbers for encoding efficiency with nested messages as @mrinalwadhwa mentioned! My biggest outstanding question is around implementing CBOR on embedded targets. |
Has anyone looked at RION? Their design goals look tempting |
Thank you all for your comments! I am going to reply in one go:
CBOR is less good than BARE as the structural overhead is repeated, but it depends on how much the structure dominates the message relative to plain data. To give a contrived example, let us consider this type: #[derive(Encode, Decode, Serialize, Deserialize)]
struct S {
#[cbor(n(0), with = "minicbor::bytes")] a: Vec<u8>
} and use fn wrap(rem: usize, to_vec: &impl Fn(S) -> Vec<u8>) -> S {
if rem == 0 {
S { a: Vec::new() }
} else {
S { a: to_vec(wrap(rem - 1, to_vec)) }
}
} BARE only encodes the vector length and its bytes whereas CBOR adds information about the structure itself. The byte lengths for the number of recursion steps are:
The only thing we encode here is essentially (serialised) structural information and clearly BARE is more compact than CBOR. However I do maintain that types more commonly used, which also contain plain data such as strings are less pathological. If we change
In https://github.com/twittner/encoding-size/blob/master/src/main.rs#L13-L39 I tried to use a fairly general and recursive type that also uses a
I think you understood correctly. I have not used packed CBOR here, but simply indices which in this case are mapped to array positions, but could also be used as keys in CBOR maps if a type is expected to have many gaps.
Example 1 was meant to show how easy it is to be unaware of the boundaries between types. I stumbled across this when replacing BARE with CBOR because I got an error at https://github.com/ockam-network/ockam/blob/47a29d20081b52aca162aba9c3d19c88a69f6165/implementations/rust/ockam/ockam_transport_tcp/src/workers/sender.rs#L221 where the payload is decoded as a Example 2 is about the void type and my confusion because I think
Yes. The crate
So far I have not but it seems to be similar to CBOR and MessagePack. I could not find a Rust implementation though. Do you know if there is one?
Yes, protocol buffers are nice in a way. I am not too fond of the code that is generated from the external schema and the schema language is quite restrictive compared to what is possible within programming languages, e.g. when it comes to generic types.
👍 I did not focus on cross-language communication so far but it is absolutely important and with protocol buffers the schema usually comes first which takes care of that. With serialisation attributes on the other hand, be it |
Thank you @twittner for starting this discussion. We definitely have a bunch of foot-guns in the Rust code where we hack around weird BARE encoding mechanisms. We're also currently figuring out how to do encoding of metadata for higher level protocols so the timing for this discussion is impeccable 🙂 I haven't worked with CBOR before but it reminds me of msgpack a lot. Having a self-describing encoding format imo only has upsides. The differences in size for simple datastructures are trivial in most cases. We will want to investigate the overhead this causes on embedded systems. But I also think that this is memory worth spending. Maybe instead we need to focus on optimising the runtime/ routing mechanisms themselves to reduce our memory footprint.
While this is a pretty big limitation, I don't think we need to worry about it too much since a lot of our message nesting actually re-encodes payloads. We do this to create decoupling between workers to make the Rust type system happy, but it comes with the added benefit that, while we have deeply nested structures, encoding and decoding don't have to be fully aware of it. Lastly: my experience with protobuf is that in most cases using it is overkill and causes more problems than it solves. We may want to investigate it nonetheless, but personally I think going with something like CBOR, which is well standardised and has implementations in many languages without relying on code-generators (meaning that writing a new implementation is also less daunting than writing a protobuf implementation), is the better way to go. |
Absolutely, there are far larger embedded memory optimizations we can perform before we even begin to approach message format contributions! 👍 |
I think we will need to revive that discussion as we need to start implementing OpenTelemetry #2319 integration between Rust and Elixir as we need to accomodate telemetry metadata in the message format. |
Currently Ockam messages are encoded with BARE, a binary data format that optimises for compactness and simplicity. It does that by requiring external schema information which allows it to not encode a lot of structural information. Apart from actual data such as string values or numbers it only adds some length information where necessary, but not much else. Insofar the format has only minimal overhead. The flip side of this approach is that without the correct schema, BARE messages can not be decoded. This prevents generic message processing and makes it difficult to have support for forward and backward compatibility1 as it is not possible to skip over message parts without knowing their meaning. The ambiguities that may arise surface in functions like
ockam_node::parser::message
which retries message decoding should the first decoding attempt fail, by modifying the message. The appendix below contains further examples that highlight the importance of accurate schema information.Many formats work with schema information and it is a great way to save precious bytes, however it would be nice if in addition, messages have enough embedded structure so that forward and backward compatibility is possible and maybe also generic processing which can help with debugging or logging.
Are there alternatives to BARE which offer this without sacrificing any of the other important requirements, i.e.
I would like to argue for CBOR here which is not as compact as BARE, but should otherwise offer all of this.
In contrast to BARE, CBOR does encode more structural information which increases message size. Every CBOR data item is prefixed with a byte that encodes the item type as well as additional information (mostly about how to load the data, e.g. length information). This enables generic processing of arbitrary data items, for example to map them to a human readable format. This also makes it possible to skip over message parts which in turn allows for forward and backward compatibility. The interesting question is how much overhead does this cost in practice? In https://github.com/twittner/encoding-size I attempt to do a comparison between BARE and CBOR. As data I use randomly generated trees of structs and enums with additional data items such as strings and numbers. Here is an example run:
The labels "TINY", "MEDIUM", etc. classify the ratio of plain data sizes (e.g. the length of strings) to the overall structure, for example TINY means that the message contains very short strings and vectors. As expected the overhead of CBOR is higher the more structural information is encoded relative to plain data items. However I think the differences are fairly small, especially when considering absolute numbers.
More interesting than this is how this would work out for the ockam repository. In here I moved most of the message types from BARE to CBOR and printed the message lengths from
ockam_core::message::Encodable::encode
for thedevelop
branch as well as for my CBOR branch while runningcargo test --all -j 1 -- --test-threads=1 --nocapture
. On average, BARE encoded messages are 256 bytes in size, whereas the CBOR encoded ones measure 285 bytes on average, confirming the above results (89%, 111%). (The CSV with all printed sizes can be found here).If the increase in size is acceptable and there is agreement that it would be desirable to have generic message processing and the possibility of forward and backward compatibility, the branch could be polished and become a pull request. However I am just exploring a possibility here and would like to discuss this first before investing more time.
For further information about CBOR see https://cbor.io. Forward and backward compatibility with application to CBOR is described in more detail here.
Appendix
Example 1
Flattened structs
Example 2
The void type
Footnotes
Section 4 of the RFC draft of BARE mentions schema evolution in a backward and forward compatible way by using tagged unions with explicit tag numbers. While adding another tagged branch is backward compatible because newer software understands the messages of older software, the reverse is not true: a message using this new tag will be rejected by older software, hence forward compatibility is not realised. ↩
The text was updated successfully, but these errors were encountered: